/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.voiceconf.red5.media.transcoder;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.FloatBuffer;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.bigbluebutton.voiceconf.red5.media.FlashToSipAudioStream.TranscodedAudioListener;
import org.red5.logging.Red5LoggerFactory;
import org.red5.app.sip.codecs.Codec;
import org.red5.app.sip.codecs.asao.Decoder;
import org.red5.app.sip.codecs.asao.DecoderMap;
/**
* Transcodes audio from voice conferencing server to Flash.
* Specifically U-law to Nelly.
* @author Richard Alam
*
*/
public class NellyFlashToSipTranscoderImp implements FlashToSipTranscoder {
protected static Logger log = Red5LoggerFactory.getLogger( NellyFlashToSipTranscoderImp.class, "sip" );
private static final int NELLY_TO_L16_AUDIO_SIZE = 256;
private static final int NELLY_AUDIO_LENGTH = 64;
private static final int ULAW_AUDIO_LENGTH = 160;
/**
* Max buffer length when 5 Nelly/L16 packets equals 8 Ulaw packets.
*/
private static final int MAX_BUFFER_LENGTH = 1280;
/**
* Allocate a fixed buffer length so we don't have to copy elements around. We'll use the
* position, limit, mart attributes of the NIO Buffer.
*/
private final FloatBuffer l16Audio = FloatBuffer.allocate(MAX_BUFFER_LENGTH);
/**
* This is a view read-only copy of the buffer to track which byte are being transcoded from L16->Ulaw
*/
private FloatBuffer viewBuffer;
private Codec sipCodec = null;
private Decoder decoder;
private DecoderMap decoderMap;
private float[] tempL16Buffer = new float[NELLY_TO_L16_AUDIO_SIZE];
private float[] tempUlawBuffer = new float[ULAW_AUDIO_LENGTH];
private byte[] ulawEncodedBuffer = new byte[ULAW_AUDIO_LENGTH];
private long timestamp = 0;
private final static int TS_INCREMENT = 180; // Determined from PCAP traces.
private final PipedOutputStream streamFromFlash;
private PipedInputStream streamToSip;
private final Executor exec = Executors.newSingleThreadExecutor();
private Runnable audioDataProcessor;
private volatile boolean processAudioData = false;
private TranscodedAudioListener transcodedAudioListener;
/**
* The transcode process works by taking a 64-byte-array Nelly audio and converting it into a 256-float-array L16 audio. From the
* 256-float-array L16 audio, we take 160-float-array and convert it to a 160-byte-array Ulaw audio. The remaining 96-float-array
* will be used in the next iteration.
* Therefore, 5 Nelly/L16 packets (5x256 = 1280) will result into 8 Ulaw packets (8x160 = 1280).
*
*/
public NellyFlashToSipTranscoderImp(Codec sipCodec) {
this.sipCodec = sipCodec;
decoder = new Decoder();
decoderMap = null;
Random rgen = new Random();
timestamp = rgen.nextInt(1000);
viewBuffer = l16Audio.asReadOnlyBuffer();
streamFromFlash = new PipedOutputStream();
try {
streamToSip = new PipedInputStream(streamFromFlash);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void start() {
processAudioData = true;
audioDataProcessor = new Runnable() {
public void run() {
processAudioData();
}
};
exec.execute(audioDataProcessor);
}
@Override
public int getOutgoingEncodedFrameSize() {
return sipCodec.getOutgoingEncodedFrameSize();
}
@Override
public int getCodecId() {
return sipCodec.getCodecId();
}
@Override
public void handlePacket(byte[] data, int begin, int end) {
try {
streamFromFlash.write(data, begin, end);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void processAudioData() {
int len = 64;
byte[] nellyAudio = new byte[len];
int remaining = len;
int offset = 0;
while (processAudioData) {
try {
int bytesRead = streamToSip.read(nellyAudio, offset, remaining);
remaining -= bytesRead;
if (remaining == 0) {
remaining = len;
offset = 0;
transcode(nellyAudio, 0, nellyAudio.length);
} else {
offset += bytesRead;
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Override
public void transcode(byte[] audioData, int startOffset, int length) {
if (audioData.length != NELLY_AUDIO_LENGTH) {
if (log.isWarnEnabled()) log.warn("Receiving bad nelly audio. Expecting {}, got {}.", NELLY_AUDIO_LENGTH, audioData.length);
return;
}
// Convert the Nelly audio to L16.
decoderMap = decoder.decode(decoderMap, audioData, 0, tempL16Buffer, 0);
// Store the L16 audio into the buffer
l16Audio.put(tempL16Buffer);
// Read 160-float worth of audio
viewBuffer.get(tempUlawBuffer);
// Convert the L16 audio to Ulaw
int encodedBytes = sipCodec.pcmToCodec(tempUlawBuffer, ulawEncodedBuffer);
// Send it to the server
transcodedAudioListener.handleTranscodedAudioData(ulawEncodedBuffer, timestamp += TS_INCREMENT);
if (l16Audio.position() == l16Audio.capacity()) {
/**
* This means we already processed 5 Nelly packets and sent 5 Ulaw packets.
* However, we have 3 extra Ulaw packets.
* Fire them off to the server. We don't want to discard them as it will
* result in choppy audio.
*/
// Get the 6th packet and send
viewBuffer.get(tempUlawBuffer);
encodedBytes = sipCodec.pcmToCodec(tempUlawBuffer, ulawEncodedBuffer);
if (encodedBytes == sipCodec.getOutgoingEncodedFrameSize()) {
transcodedAudioListener.handleTranscodedAudioData(ulawEncodedBuffer, timestamp += TS_INCREMENT);
} else {
log.error("Failure encoding buffer." );
}
// Get the 7th packet and send
viewBuffer.get(tempUlawBuffer);
encodedBytes = sipCodec.pcmToCodec(tempUlawBuffer, ulawEncodedBuffer);
if (encodedBytes == sipCodec.getOutgoingEncodedFrameSize()) {
transcodedAudioListener.handleTranscodedAudioData(ulawEncodedBuffer, timestamp += TS_INCREMENT);
} else {
log.error("Failure encoding buffer." );
}
// Get the 8th packet and send
viewBuffer.get(tempUlawBuffer);
encodedBytes = sipCodec.pcmToCodec(tempUlawBuffer, ulawEncodedBuffer);
if (encodedBytes == sipCodec.getOutgoingEncodedFrameSize()) {
transcodedAudioListener.handleTranscodedAudioData(ulawEncodedBuffer, timestamp += TS_INCREMENT);
} else {
log.error("Failure encoding buffer." );
}
// Reset the buffer's position back to zero and start over.
l16Audio.clear();
viewBuffer.clear();
}
}
public void setTranscodedAudioListener(TranscodedAudioListener transcodedAudioListener) {
this.transcodedAudioListener = transcodedAudioListener;
}
@Override
public void stop() {
processAudioData = false;
}
}