Sketcher2 source code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

172 lines
5.0 KiB

package com.jotuntech.sketcher.client.voice;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
public class VoiceMixer {
private SourceDataLine source;
private AudioFormat sourceFormat;
private int bufferSize;
private Map<Integer, ByteBuffer> channelMap;
private ByteBuffer mixBuffer;
public VoiceMixer(int bufferSize) {
sourceFormat = new AudioFormat(16000, 16, 1, true, false);
channelMap = new HashMap<Integer, ByteBuffer>();
this.bufferSize = bufferSize;
}
public void write(Integer channelKey, ByteBuffer data) {
ByteBuffer channel = channelMap.get(channelKey);
if(channel == null) {
/** Open new channel */
channel = ByteBuffer.allocate(bufferSize);
channel.order(ByteOrder.LITTLE_ENDIAN);
channelMap.put(channelKey, channel);
}
channel.put(data);
}
public int available(Integer channelKey) {
ByteBuffer channel = channelMap.get(channelKey);
if(channel == null) {
return bufferSize;
}
return channel.remaining();
}
public void drop(Integer channelKey) {
channelMap.remove(channelKey);
}
public boolean mix() {
if(channelMap.size() == 0) {
/** No mixing or playback takes place unless we have channels */
return true;
}
/** Attempt to mix when playback buffer is half empty */
if(source == null || source.available() >= source.getBufferSize() / 2) {
/** We can not mix more audio than we can write to the source */
int maximumWrite = source == null ? bufferSize : source.available();
int underflows = 0;
/** Scan available channels */
for(Map.Entry<Integer, ByteBuffer> e : channelMap.entrySet()) {
ByteBuffer channel = e.getValue();
/** Flip channel for reading */
channel.flip();
/** Shorten mix to shortest individual channel, excluding too short ones */
if(channel.remaining() <= bufferSize / 2) {
/** Channel is underflowing and we will not include it in the mix */
//System.err.println("Channel " + e.getKey() + " underflowing.");
++underflows;
} else if(channel.remaining() < maximumWrite) {
/** Channel has the least remaining audio and we must shorten the mix */
maximumWrite = channel.remaining();
}
}
if(underflows == channelMap.size()) {
/** Do not write anything if all channels are empty */
maximumWrite = 0;
}
if(maximumWrite > 0) {
/** Prepare the mix buffer */
if(mixBuffer == null) {
mixBuffer = ByteBuffer.allocate(bufferSize);
mixBuffer.order(ByteOrder.LITTLE_ENDIAN); // Speex uses 16-bit little-endian samples
}
mixBuffer.clear();
mixBuffer.limit(maximumWrite);
boolean firstChannel = true;
for(ByteBuffer channel : channelMap.values()) {
/** Ignore underflowing channels */
if(channel.remaining() > bufferSize / 2) {
/** Position buffer for mixing a new channel */
mixBuffer.position(0);
for(int i = 0; i < maximumWrite; i += 2) {
/** Mix samples additively */
int sample = channel.getShort();
if(firstChannel) {
mixBuffer.putShort((short) sample);
} else {
sample += mixBuffer.getShort(i);
/** Handle clipping */
if(sample > 32767) {
mixBuffer.putShort((short) 32767);
} else if(sample < -32768) {
mixBuffer.putShort((short) -32768);
} else {
/** Write mixed sample to mix buffer */
mixBuffer.putShort((short) sample);
}
}
}
firstChannel = false;
}
}
/** Flip mix buffer for reading (i.e. writing to source) */
mixBuffer.flip();
if(mixBuffer.remaining() > 0) {
if(source == null) {
try {
/** Start source data line */
System.err.println("Voice mixer opening source data line for playback.");
source = AudioSystem.getSourceDataLine(sourceFormat);
source.open(sourceFormat, bufferSize);
source.start();
bufferSize = source.getBufferSize();
} catch (LineUnavailableException e) {
System.err.println("Voice mixer unable to open target data line.");
System.err.println("Java exception:");
e.printStackTrace();
return false;
}
}
/** Write mix to source */
source.write(mixBuffer.array(), mixBuffer.arrayOffset(), mixBuffer.limit());
}
}
/** Prepare channels, previously flipped under scan, for writing again */
for(ByteBuffer channel : channelMap.values()) {
channel.compact();
}
}
return true;
}
public void close() {
if(source != null) {
source.stop();
source.close();
}
}
public int getBufferSize() {
if(source == null) {
return bufferSize;
} else {
return source.getBufferSize();
}
}
}