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
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();
|
|
}
|
|
}
|
|
}
|
|
|