diff options
author | Andrew Comminos <andrew@morlunk.com> | 2014-12-09 07:09:05 +0300 |
---|---|---|
committer | Andrew Comminos <andrew@morlunk.com> | 2014-12-09 07:09:05 +0300 |
commit | 88a6c52615ac0686652c14c9049935061a822fb6 (patch) | |
tree | 4292c02b48abfb71e978a36cc1ec31ea3c2c4928 | |
parent | 77953bcb550d86a490188eda05737bbe14fd2adc (diff) |
Initial transition to more modular encoding process.
14 files changed, 681 insertions, 434 deletions
diff --git a/src/androidTest/java/com/morlunk/jumble/test/EncoderTest.java b/src/androidTest/java/com/morlunk/jumble/test/EncoderTest.java index 70290ff..8baec6f 100644 --- a/src/androidTest/java/com/morlunk/jumble/test/EncoderTest.java +++ b/src/androidTest/java/com/morlunk/jumble/test/EncoderTest.java @@ -20,8 +20,9 @@ package com.morlunk.jumble.test; import android.test.AndroidTestCase; import com.googlecode.javacpp.Loader; -import com.morlunk.jumble.audio.javacpp.CELT11; -import com.morlunk.jumble.audio.javacpp.CELT7; +import com.morlunk.jumble.audio.encoder.CELT11Encoder; +import com.morlunk.jumble.audio.encoder.CELT7Encoder; +import com.morlunk.jumble.audio.encoder.OpusEncoder; import com.morlunk.jumble.audio.javacpp.Opus; import com.morlunk.jumble.exception.NativeAudioException; @@ -42,7 +43,7 @@ public class EncoderTest extends AndroidTestCase { } public void testOpusEncode() throws NativeAudioException { - Opus.OpusEncoder encoder = new Opus.OpusEncoder(SAMPLE_RATE, 1); + OpusEncoder encoder = new OpusEncoder(SAMPLE_RATE, 1); encoder.setBitrate(BITRATE); assertEquals(encoder.getBitrate(), BITRATE); @@ -53,7 +54,7 @@ public class EncoderTest extends AndroidTestCase { } public void testCELT11Encode() throws NativeAudioException { - CELT11.CELT11Encoder encoder = new CELT11.CELT11Encoder(SAMPLE_RATE, 1); + CELT11Encoder encoder = new CELT11Encoder(SAMPLE_RATE, 1); // encoder.setBitrate(BITRATE); // assertEquals(encoder.getBitrate(), BITRATE); @@ -64,7 +65,7 @@ public class EncoderTest extends AndroidTestCase { } public void testCELT7Encode() throws NativeAudioException { - CELT7.CELT7Encoder encoder = new CELT7.CELT7Encoder(SAMPLE_RATE, FRAME_SIZE, 1); + CELT7Encoder encoder = new CELT7Encoder(SAMPLE_RATE, FRAME_SIZE, 1); // encoder.setBitrate(BITRATE); // assertEquals(encoder.getBitrate(), BITRATE); diff --git a/src/main/java/com/morlunk/jumble/JumbleService.java b/src/main/java/com/morlunk/jumble/JumbleService.java index e2ebc13..7a6c413 100644 --- a/src/main/java/com/morlunk/jumble/JumbleService.java +++ b/src/main/java/com/morlunk/jumble/JumbleService.java @@ -134,16 +134,17 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon private List<Message> mMessageLog; private boolean mReconnecting; - private AudioInput.AudioInputListener mAudioInputListener = new AudioInput.AudioInputListener() { + private AudioHandler.AudioEncodeListener mAudioInputListener = + new AudioHandler.AudioEncodeListener() { @Override - public void onFrameEncoded(byte[] data, int length, JumbleUDPMessageType messageType) { + public void onAudioEncoded(byte[] data, int length) { if(mConnection.isSynchronized()) { mConnection.sendUDPMessage(data, length, false); } } @Override - public void onTalkStateChanged(final boolean talking) { + public void onTalkStateChange(final User.TalkState state) { mHandler.post(new Runnable() { @Override public void run() { @@ -151,7 +152,7 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon final User currentUser = mModelHandler.getUser(mConnection.getSession()); if(currentUser == null) return; - currentUser.setTalkState(talking ? User.TalkState.TALKING : User.TalkState.PASSIVE); + currentUser.setTalkState(state); try { mCallbacks.onUserTalkStateUpdated(currentUser); } catch (RemoteException e) { diff --git a/src/main/java/com/morlunk/jumble/audio/AudioInput.java b/src/main/java/com/morlunk/jumble/audio/AudioInput.java index ef31366..d66c2e5 100644 --- a/src/main/java/com/morlunk/jumble/audio/AudioInput.java +++ b/src/main/java/com/morlunk/jumble/audio/AudioInput.java @@ -19,18 +19,20 @@ package com.morlunk.jumble.audio; import android.media.AudioFormat; import android.media.AudioRecord; -import android.media.MediaRecorder; import android.util.Log; import com.googlecode.javacpp.IntPointer; import com.googlecode.javacpp.Loader; import com.morlunk.jumble.Constants; -import com.morlunk.jumble.audio.javacpp.CELT11; -import com.morlunk.jumble.audio.javacpp.CELT7; +import com.morlunk.jumble.audio.encoder.IEncoder; +import com.morlunk.jumble.audio.encoder.CELT11Encoder; +import com.morlunk.jumble.audio.encoder.CELT7Encoder; +import com.morlunk.jumble.audio.encoder.OpusEncoder; import com.morlunk.jumble.audio.javacpp.Opus; import com.morlunk.jumble.audio.javacpp.Speex; import com.morlunk.jumble.exception.AudioInitializationException; import com.morlunk.jumble.exception.NativeAudioException; +import com.morlunk.jumble.model.User; import com.morlunk.jumble.net.JumbleUDPMessageType; import com.morlunk.jumble.net.PacketBuffer; import com.morlunk.jumble.protocol.AudioHandler; @@ -39,79 +41,29 @@ import com.morlunk.jumble.protocol.AudioHandler; * Created by andrew on 23/08/13. */ public class AudioInput implements Runnable { - - static { - Loader.load(Opus.class); // Do this so we can reference IntPointer and the like earlier. - } - - public interface AudioInputListener { - /** - * Called when a frame has finished processing, and is ready to go to the server. - * @param data The encoded audio data. - * @param length The length of the encoded audio data. - * @param messageType The codec of the encoded data. - */ - public void onFrameEncoded(byte[] data, int length, JumbleUDPMessageType messageType); - - public void onTalkStateChanged(boolean talking); - } - public static final int[] SAMPLE_RATES = { 48000, 44100, 22050, 16000, 11025, 8000 }; - private static final int SPEEX_RESAMPLE_QUALITY = 3; - private static final int OPUS_MAX_BYTES = 960; // Opus specifies 4000 bytes as a recommended value for encoding, but the official mumble project uses 512. private static final int SPEECH_DETECT_THRESHOLD = (int) (0.25 * Math.pow(10, 9)); // Continue speech for 250ms to prevent dropping - private IEncoder mEncoder; - private Speex.SpeexPreprocessState mPreprocessState; - private Speex.SpeexResampler mResampler; - // AudioRecord state private AudioInputListener mListener; private AudioRecord mAudioRecord; private final int mFrameSize; - private final int mMicFrameSize; // Preferences - private int mBitrate; - private final int mFramesPerPacket; private int mTransmitMode; private float mVADThreshold; private float mAmplitudeBoost = 1.0f; - private boolean mUsePreprocessor = true; - - // Encoder state - final short[] mAudioBuffer; - final short[] mOpusBuffer; - final byte[][] mCELTBuffer; - final short[] mResampleBuffer; - - private final byte[] mEncodedBuffer = new byte[OPUS_MAX_BYTES]; - private int mBufferedFrames = 0; - private int mFrameCounter; - - private JumbleUDPMessageType mCodec = null; private Thread mRecordThread; private boolean mRecording; - public AudioInput(AudioInputListener listener, JumbleUDPMessageType codec, int audioSource, - int targetSampleRate, int bitrate, int framesPerPacket, int transmitMode, - float vadThreshold, float amplitudeBoost, boolean preprocessorEnabled) throws + public AudioInput(AudioInputListener listener, int audioSource, int targetSampleRate, + int transmitMode, float vadThreshold, float amplitudeBoost) throws NativeAudioException, AudioInitializationException { mListener = listener; - mCodec = codec; - mBitrate = bitrate; - mFramesPerPacket = framesPerPacket; mTransmitMode = transmitMode; mVADThreshold = vadThreshold; mAmplitudeBoost = amplitudeBoost; - mUsePreprocessor = preprocessorEnabled; - mEncoder = createEncoder(mCodec); - mFrameSize = AudioHandler.FRAME_SIZE; - - mAudioBuffer = new short[mFrameSize]; - mOpusBuffer = new short[mFrameSize * mFramesPerPacket]; - mCELTBuffer = new byte[mFramesPerPacket][AudioHandler.SAMPLE_RATE / 800]; // Attempt to construct an AudioRecord with the target sample rate first. // If it fails, keep producing AudioRecord instances until we find one that initializes @@ -131,49 +83,8 @@ public class AudioInput implements Runnable { throw new AudioInitializationException("Unable to initialize AudioInput."); } - int sampleRate = mAudioRecord.getSampleRate(); - if(sampleRate != AudioHandler.SAMPLE_RATE) { - mResampler = new Speex.SpeexResampler(1, sampleRate, AudioHandler.SAMPLE_RATE, - SPEEX_RESAMPLE_QUALITY); - mMicFrameSize = (sampleRate * mFrameSize) / AudioHandler.SAMPLE_RATE; - mResampleBuffer = new short[mMicFrameSize]; - } else { - mMicFrameSize = mFrameSize; - mResampleBuffer = null; - } - - configurePreprocessState(); - - Log.i(Constants.TAG, "AudioInput: " + mBitrate + "bps, " + mFramesPerPacket + - " frames/packet, " + mAudioRecord.getSampleRate() + "hz"); - } - - /** - * Initializes and configures the Speex preprocessor. - * Based off of Mumble project's AudioInput method resetAudioProcessor(). - */ - private void configurePreprocessState() { - if(mPreprocessState != null) mPreprocessState.destroy(); - - mPreprocessState = new Speex.SpeexPreprocessState(mFrameSize, AudioHandler.SAMPLE_RATE); - - IntPointer arg = new IntPointer(1); - - arg.put(0); - mPreprocessState.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_VAD, arg); - arg.put(1); - mPreprocessState.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_AGC, arg); - mPreprocessState.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_DENOISE, arg); - mPreprocessState.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_DEREVERB, arg); - - arg.put(30000); - mPreprocessState.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_AGC_TARGET, arg); - - // TODO AGC max gain, decrement, noise suppress, echo - - // Increase VAD difficulty - arg.put(99); - mPreprocessState.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_GET_PROB_START, arg); + int sampleRate = getSampleRate(); + mFrameSize = (sampleRate * AudioHandler.FRAME_SIZE) / AudioHandler.SAMPLE_RATE; } private static AudioRecord setupAudioRecord(int sampleRate, int audioSource) throws AudioInitializationException { @@ -195,30 +106,6 @@ public class AudioInput implements Runnable { return audioRecord; } - private IEncoder createEncoder(JumbleUDPMessageType codec) throws NativeAudioException { - Log.v(Constants.TAG, "Using codec "+codec.toString()+" for input"); - - IEncoder encoder; - switch (codec) { - case UDPVoiceOpus: - encoder = new Opus.OpusEncoder(AudioHandler.SAMPLE_RATE, 1); - break; - case UDPVoiceCELTBeta: - encoder = new CELT11.CELT11Encoder(AudioHandler.SAMPLE_RATE, 1); - break; - case UDPVoiceCELTAlpha: - encoder = new CELT7.CELT7Encoder(AudioHandler.SAMPLE_RATE, mFrameSize, 1); - break; -// case UDPVoiceSpeex: - // TODO -// break; - default: - throw new NativeAudioException("Codec " + codec + " not supported."); - } - encoder.setBitrate(mBitrate); - return encoder; - } - /** * Starts the recording thread. * Not thread-safe. @@ -248,24 +135,10 @@ public class AudioInput implements Runnable { mAmplitudeBoost = boost; } - public void setBitrate(int bitrate) { - mBitrate = bitrate; - if(mEncoder != null) mEncoder.setBitrate(bitrate); - } - - public void setPreprocessorEnabled(boolean preprocessorEnabled) { - mUsePreprocessor = preprocessorEnabled; - } - - public boolean isResampling() { - return mResampler != null; - } - /** * Stops the record loop and waits on it to finish. * Releases native audio resources. - * NOTE: It is safe to call startRecording after. - * @throws InterruptedException + * NOTE: It is not safe to call startRecording after. */ public void shutdown() { if(mRecording) { @@ -278,47 +151,37 @@ public class AudioInput implements Runnable { } mRecordThread = null; - mCodec = null; if(mAudioRecord != null) { mAudioRecord.release(); mAudioRecord = null; } - if(mEncoder != null) { - mEncoder.destroy(); - mEncoder = null; - } - if(mPreprocessState != null) { - mPreprocessState.destroy(); - mPreprocessState = null; - } - if(mResampler != null) { - mResampler.destroy(); - mResampler = null; - } - if(mAudioRecord != null) { - mAudioRecord.release(); - mAudioRecord = null; - } } public boolean isRecording() { return mRecording; } + /** + * @return the sample rate used by the AudioRecord instance. + */ + public int getSampleRate() { + return mAudioRecord.getSampleRate(); + } + + /** + * @return the frame size used, varying depending on the sample rate selected. + */ + public int getFrameSize() { + return mFrameSize; + } + @Override public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); - if(mCodec == null) { - Log.v(Constants.TAG, "Tried to start recording without a codec version!"); - return; - } - boolean vadLastDetected = false; long vadLastDetectedTime = 0; - mBufferedFrames = 0; - mFrameCounter = 0; mAudioRecord.startRecording(); @@ -326,27 +189,13 @@ public class AudioInput implements Runnable { return; if(mTransmitMode == Constants.TRANSMIT_CONTINUOUS || mTransmitMode == Constants.TRANSMIT_PUSH_TO_TALK) - mListener.onTalkStateChanged(true); + mListener.onTalkStateChange(User.TalkState.TALKING); + final short[] mAudioBuffer = new short[mFrameSize]; // We loop when the 'recording' instance var is true instead of checking audio record state because we want to always cleanly shutdown. while(mRecording) { - short[] targetBuffer = isResampling() ? mResampleBuffer : mAudioBuffer; - int shortsRead = mAudioRecord.read(targetBuffer, 0, mMicFrameSize); + int shortsRead = mAudioRecord.read(mAudioBuffer, 0, mFrameSize); if(shortsRead > 0) { - int len = 0; - boolean encoded = false; - mFrameCounter++; - - // Resample if necessary - if(isResampling()) { - mResampler.resample(mResampleBuffer, mAudioBuffer); - } - - // Run preprocessor on audio data. TODO echo! - if (mUsePreprocessor) { - mPreprocessState.preprocess(mAudioBuffer); - } - // Boost/reduce amplitude based on user preference if(mAmplitudeBoost != 1.0f) { for(int i = 0; i < mFrameSize; i++) { @@ -375,96 +224,26 @@ public class AudioInput implements Runnable { talking |= (System.nanoTime() - vadLastDetectedTime) < SPEECH_DETECT_THRESHOLD; if(talking ^ vadLastDetected) // Update the service with the new talking state if we detected voice. - mListener.onTalkStateChanged(talking); + mListener.onTalkStateChange(talking ? User.TalkState.TALKING : + User.TalkState.PASSIVE); vadLastDetected = talking; } - if(!talking) { - continue; + if(talking) { + mListener.onAudioInputReceived(mAudioBuffer, mFrameSize); } - - // TODO integrate this switch's behaviour into IEncoder implementations - switch (mCodec) { - case UDPVoiceOpus: - System.arraycopy(mAudioBuffer, 0, mOpusBuffer, mFrameSize * mBufferedFrames, mFrameSize); - mBufferedFrames++; - - if((!mRecording && mBufferedFrames > 0) || mBufferedFrames >= mFramesPerPacket) { - if(mBufferedFrames < mFramesPerPacket) - mBufferedFrames = mFramesPerPacket; // If recording was stopped early, encode remaining empty frames too. - - try { - len = mEncoder.encode(mOpusBuffer, mFrameSize * mBufferedFrames, mEncodedBuffer, OPUS_MAX_BYTES); - encoded = true; - } catch (NativeAudioException e) { - mBufferedFrames = 0; - continue; - } - } - break; - case UDPVoiceCELTBeta: - case UDPVoiceCELTAlpha: - try { - len = mEncoder.encode(mAudioBuffer, mFrameSize, mCELTBuffer[mBufferedFrames], AudioHandler.SAMPLE_RATE/800); - mBufferedFrames++; - encoded = mBufferedFrames >= mFramesPerPacket || (!mRecording && mBufferedFrames > 0); - } catch (NativeAudioException e) { - mBufferedFrames = 0; - continue; - } - break; - case UDPVoiceSpeex: - break; - } - - if(encoded) sendFrame(!mRecording, len); } else { - Log.e(Constants.TAG, "Error fetching audio! AudioRecord error "+shortsRead); - mBufferedFrames = 0; + Log.e(Constants.TAG, "Error fetching audio! AudioRecord error " + shortsRead); } } mAudioRecord.stop(); - mListener.onTalkStateChanged(false); + mListener.onTalkStateChange(User.TalkState.PASSIVE); } - /** - * Sends the encoded frame to the server. - * Volatile; depends on class state and must not be called concurrently. - */ - private void sendFrame(boolean terminator, int length) { - int frames = mBufferedFrames; - mBufferedFrames = 0; - - int flags = 0; - flags |= mCodec.ordinal() << 5; - - final byte[] packetBuffer = new byte[1024]; - packetBuffer[0] = (byte) (flags & 0xFF); - - PacketBuffer ds = new PacketBuffer(packetBuffer, 1024); - ds.skip(1); - ds.writeLong(mFrameCounter - frames); - - if(mCodec == JumbleUDPMessageType.UDPVoiceOpus) { - byte[] frame = mEncodedBuffer; - long size = length; - if(terminator) - size |= 1 << 13; - ds.writeLong(size); - ds.append(frame, length); - } else { - for (int x=0;x<frames;x++) { - byte[] frame = mCELTBuffer[x]; - int head = frame.length; - if(x < frames-1) - head |= 0x80; - ds.append(head); - ds.append(frame, frame.length); - } - } - - mListener.onFrameEncoded(packetBuffer, ds.size(), mCodec); + public interface AudioInputListener { + public void onTalkStateChange(User.TalkState state); + public void onAudioInputReceived(short[] frame, int frameSize); } } diff --git a/src/main/java/com/morlunk/jumble/audio/IEncoder.java b/src/main/java/com/morlunk/jumble/audio/IEncoder.java deleted file mode 100644 index 7cbeb3f..0000000 --- a/src/main/java/com/morlunk/jumble/audio/IEncoder.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2014 Andrew Comminos - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package com.morlunk.jumble.audio; - -import com.morlunk.jumble.exception.NativeAudioException; - -/** - * Created by andrew on 07/03/14. - */ -public interface IEncoder { - /** - * Encodes the provided input and returns the number of bytes encoded. - * @param input The short PCM data to encode. - * @param inputSize The number of samples to encode. - * @param output The output buffer. - * @param outputSize The size of the output buffer. - * @return The number of bytes encoded. - * @throws NativeAudioException if there was an error encoding. - */ - public int encode(short[] input, int inputSize, byte[] output, int outputSize) throws NativeAudioException; - public void setBitrate(int bitrate); - public void destroy(); -} diff --git a/src/main/java/com/morlunk/jumble/audio/encoder/CELT11Encoder.java b/src/main/java/com/morlunk/jumble/audio/encoder/CELT11Encoder.java new file mode 100644 index 0000000..36ec9c0 --- /dev/null +++ b/src/main/java/com/morlunk/jumble/audio/encoder/CELT11Encoder.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2014 Andrew Comminos + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble.audio.encoder; + +import com.googlecode.javacpp.IntPointer; +import com.googlecode.javacpp.Pointer; +import com.morlunk.jumble.audio.javacpp.CELT11; +import com.morlunk.jumble.exception.NativeAudioException; +import com.morlunk.jumble.net.PacketBuffer; +import com.morlunk.jumble.protocol.AudioHandler; + +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; + +/** +* Created by andrew on 08/12/14. +*/ +public class CELT11Encoder implements IEncoder { + public static final int CELT_BUFFER_SIZE = AudioHandler.SAMPLE_RATE / 800; + + private final byte[][] mBuffer; + private final int mFramesPerPacket; + private int mBufferedFrames; + + private Pointer mState; + + public CELT11Encoder(int sampleRate, int channels, int framesPerPacket) throws + NativeAudioException { + mFramesPerPacket = framesPerPacket; + mBuffer = new byte[framesPerPacket][CELT_BUFFER_SIZE]; + mBufferedFrames = 0; + + IntPointer error = new IntPointer(1); + error.put(0); + mState = CELT11.celt_encoder_create(sampleRate, channels, error); + if(error.get() < 0) throw new NativeAudioException("CELT 0.11.0 encoder initialization " + + "failed with error: "+error.get()); + } + + @Override + public int encode(short[] input, int frameSize) throws NativeAudioException { + if (mBufferedFrames >= mFramesPerPacket) { + throw new BufferOverflowException(); + } + + int result = CELT11.celt_encode(mState, input, frameSize, mBuffer[mBufferedFrames], + CELT_BUFFER_SIZE); + if(result < 0) throw new NativeAudioException("CELT 0.11.0 encoding failed with error: " + + result); + mBufferedFrames++; + return result; + } + + @Override + public int getBufferedFrames() { + return mBufferedFrames; + } + + @Override + public boolean isReady() { + return mBufferedFrames == mFramesPerPacket; + } + + @Override + public void getEncodedData(PacketBuffer packetBuffer) throws BufferUnderflowException { + if (mBufferedFrames < mFramesPerPacket) { + throw new BufferUnderflowException(); + } + + for (int x = 0; x < mBufferedFrames; x++) { + byte[] frame = mBuffer[x]; + int head = frame.length; + if(x < mBufferedFrames - 1) + head |= 0x80; + packetBuffer.append(head); + packetBuffer.append(frame, frame.length); + } + + mBufferedFrames = 0; + } + + @Override + public void setBitrate(int bitrate) { + // FIXME +// IntPointer ptr = new IntPointer(1); +// ptr.put(bitrate); +// celt_encoder_ctl(mState, CELT_SET_BITRATE_REQUEST, ptr); + } + + @Override + public void terminate() throws NativeAudioException { + // TODO + } + + @Override + public void destroy() { + CELT11.celt_encoder_destroy(mState); + } +} diff --git a/src/main/java/com/morlunk/jumble/audio/encoder/CELT7Encoder.java b/src/main/java/com/morlunk/jumble/audio/encoder/CELT7Encoder.java new file mode 100644 index 0000000..79954cb --- /dev/null +++ b/src/main/java/com/morlunk/jumble/audio/encoder/CELT7Encoder.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2014 Andrew Comminos + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble.audio.encoder; + +import com.googlecode.javacpp.IntPointer; +import com.googlecode.javacpp.Pointer; +import com.morlunk.jumble.audio.javacpp.CELT7; +import com.morlunk.jumble.exception.NativeAudioException; +import com.morlunk.jumble.net.PacketBuffer; +import com.morlunk.jumble.protocol.AudioHandler; + +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; + +/** +* Created by andrew on 08/12/14. +*/ +public class CELT7Encoder implements IEncoder { + public static final int CELT_BUFFER_SIZE = AudioHandler.SAMPLE_RATE / 800; + + private final byte[][] mBuffer; + private final int mFramesPerPacket; + private int mBufferedFrames; + + private Pointer mMode; + private Pointer mState; + + public CELT7Encoder(int sampleRate, int frameSize, int channels, + int framesPerPacket) throws NativeAudioException { + mFramesPerPacket = framesPerPacket; + mBuffer = new byte[framesPerPacket][CELT_BUFFER_SIZE]; + mBufferedFrames = 0; + + IntPointer error = new IntPointer(1); + error.put(0); + mMode = CELT7.celt_mode_create(sampleRate, frameSize, error); + if(error.get() < 0) throw new NativeAudioException("CELT 0.7.0 encoder initialization failed with error: "+error.get()); + mState = CELT7.celt_encoder_create(mMode, channels, error); + if(error.get() < 0) throw new NativeAudioException("CELT 0.7.0 encoder initialization failed with error: "+error.get()); + } + + @Override + public int encode(short[] input, int inputSize) throws NativeAudioException { + if (mBufferedFrames >= mFramesPerPacket) { + throw new BufferOverflowException(); + } + + int result = CELT7.celt_encode(mState, input, null, mBuffer[mBufferedFrames], + CELT_BUFFER_SIZE); + if(result < 0) throw new NativeAudioException("CELT 0.7.0 encoding failed with error: " + + result); + mBufferedFrames++; + return result; + } + + @Override + public int getBufferedFrames() { + return mBufferedFrames; + } + + @Override + public boolean isReady() { + return mBufferedFrames == mFramesPerPacket; + } + + @Override + public void getEncodedData(PacketBuffer packetBuffer) throws BufferUnderflowException { + if (mBufferedFrames < mFramesPerPacket) { + throw new BufferUnderflowException(); + } + + for (int x = 0; x < mBufferedFrames; x++) { + byte[] frame = mBuffer[x]; + int head = frame.length; + if(x < mBufferedFrames - 1) + head |= 0x80; + packetBuffer.append(head); + packetBuffer.append(frame, frame.length); + } + + mBufferedFrames = 0; + } + + @Override + public void setBitrate(int bitrate) { + // FIXME + } + + @Override + public void terminate() throws NativeAudioException { + // TODO + } + + @Override + public void destroy() { + CELT7.celt_encoder_destroy(mState); + CELT7.celt_mode_destroy(mMode); + } +} diff --git a/src/main/java/com/morlunk/jumble/audio/encoder/IEncoder.java b/src/main/java/com/morlunk/jumble/audio/encoder/IEncoder.java new file mode 100644 index 0000000..3457fda --- /dev/null +++ b/src/main/java/com/morlunk/jumble/audio/encoder/IEncoder.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2014 Andrew Comminos + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble.audio.encoder; + +import com.morlunk.jumble.exception.NativeAudioException; +import com.morlunk.jumble.net.PacketBuffer; + +import java.nio.BufferUnderflowException; + +/** + * IEncoder provides an interface for native audio encoders to buffer and serve encoded audio + * data. + * Created by andrew on 07/03/14. + */ +public interface IEncoder { + /** + * Encodes the provided input and returns the number of bytes encoded. + * @param input The short PCM data to encode. + * @param inputSize The number of samples to encode. + * @return The number of bytes encoded. + * @throws NativeAudioException if there was an error encoding. + */ + public int encode(short[] input, int inputSize) throws NativeAudioException; + + /** + * @return the number of audio frames buffered. + */ + public int getBufferedFrames(); + + /** + * @return true if enough buffered audio has been encoded to send to the server. + */ + public boolean isReady(); + + /** + * Writes the currently encoded audio data into the provided {@link PacketBuffer}. + * Use {@link #isReady()} to determine whether or not this should be called. + * @throws BufferUnderflowException if insufficient audio data has been buffered. + */ + public void getEncodedData(PacketBuffer packetBuffer) throws BufferUnderflowException; + + /** + * Sets the bitrate of the encoder. + * @param bitrate The bitrate (in bps) to encode audio with. + */ + public void setBitrate(int bitrate); + + /** + * Informs the encoder that there are no more audio packets to be queued. Often, this will + * trigger an encode operation, changing the result of {@link #isReady()}. + */ + public void terminate() throws NativeAudioException; + + /** + * Destroys the encoder, cleaning up natively allocated resources. + */ + public void destroy(); +} diff --git a/src/main/java/com/morlunk/jumble/audio/encoder/OpusEncoder.java b/src/main/java/com/morlunk/jumble/audio/encoder/OpusEncoder.java new file mode 100644 index 0000000..38c864f --- /dev/null +++ b/src/main/java/com/morlunk/jumble/audio/encoder/OpusEncoder.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2014 Andrew Comminos + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble.audio.encoder; + +import com.googlecode.javacpp.IntPointer; +import com.googlecode.javacpp.Pointer; +import com.morlunk.jumble.audio.javacpp.Opus; +import com.morlunk.jumble.exception.NativeAudioException; +import com.morlunk.jumble.net.PacketBuffer; + +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; + +/** +* Created by andrew on 08/12/14. +*/ +public class OpusEncoder implements IEncoder { + /** + * Opus specifies 4000 bytes as a recommended value for encoding, but the official + * Mumble project uses 960. + */ + private static final int OPUS_MAX_BYTES = 960; + + private final byte[] mBuffer; + private final short[] mAudioBuffer; + private final int mFramesPerPacket; + private final int mFrameSize; + + // Stateful + private int mBufferedFrames; + private int mEncodedLength; + private boolean mTerminated; + + private Pointer mState; + + public OpusEncoder(int sampleRate, int channels, int frameSize, int framesPerPacket) throws + NativeAudioException { + mBuffer = new byte[OPUS_MAX_BYTES]; + mAudioBuffer = new short[framesPerPacket * frameSize]; + mFramesPerPacket = framesPerPacket; + mFrameSize = frameSize; + mBufferedFrames = 0; + mEncodedLength = 0; + + IntPointer error = new IntPointer(1); + error.put(0); + mState = Opus.opus_encoder_create(sampleRate, channels, Opus.OPUS_APPLICATION_VOIP, error); + if(error.get() < 0) throw new NativeAudioException("Opus encoder initialization failed with error: "+error.get()); + Opus.opus_encoder_ctl(mState, Opus.OPUS_SET_VBR_REQUEST, 0); // enable CBR + } + + @Override + public int encode(short[] input, int inputSize) throws NativeAudioException { + if (mBufferedFrames >= mFramesPerPacket) { + throw new BufferOverflowException(); + } + + if (inputSize != mFrameSize) { + throw new IllegalArgumentException("This Opus encoder implementation requires a " + + "constant frame size."); + } + + mTerminated = false; + System.arraycopy(input, 0, mAudioBuffer, mFrameSize * mBufferedFrames, mFrameSize); + mBufferedFrames++; + + if (mBufferedFrames == mFramesPerPacket) { + return encode(); + } + return 0; + } + + private int encode() throws NativeAudioException { + int result = Opus.opus_encode(mState, mAudioBuffer, mFrameSize * mBufferedFrames, + mBuffer, OPUS_MAX_BYTES); + if(result < 0) throw new NativeAudioException("Opus encoding failed with error: " + + result); + mEncodedLength = result; + return result; + } + + @Override + public int getBufferedFrames() { + return mBufferedFrames; + } + + @Override + public boolean isReady() { + return mEncodedLength > 0; + } + + @Override + public void getEncodedData(PacketBuffer packetBuffer) throws BufferUnderflowException { + if (mEncodedLength <= 0) { + throw new BufferUnderflowException(); + } + + int size = mEncodedLength; + if(mTerminated) + size |= 1 << 13; + packetBuffer.writeLong(size); + packetBuffer.append(mBuffer, size); + + mBufferedFrames = 0; + mEncodedLength = 0; + mTerminated = false; + } + + @Override + public void setBitrate(int bitrate) { + Opus.opus_encoder_ctl(mState, Opus.OPUS_SET_BITRATE_REQUEST, bitrate); + } + + @Override + public void terminate() throws NativeAudioException { + mTerminated = true; + if (mBufferedFrames > 0) { + encode(); + } + } + + public int getBitrate() { + IntPointer ptr = new IntPointer(1); + Opus.opus_encoder_ctl(mState, Opus.OPUS_GET_BITRATE_REQUEST, ptr); + return ptr.get(); + } + + @Override + public void destroy() { + Opus.opus_encoder_destroy(mState); + } +} diff --git a/src/main/java/com/morlunk/jumble/audio/PreprocessingEncoder.java b/src/main/java/com/morlunk/jumble/audio/encoder/PreprocessingEncoder.java index f8cf9be..915b2d4 100644 --- a/src/main/java/com/morlunk/jumble/audio/PreprocessingEncoder.java +++ b/src/main/java/com/morlunk/jumble/audio/encoder/PreprocessingEncoder.java @@ -15,11 +15,14 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package com.morlunk.jumble.audio; +package com.morlunk.jumble.audio.encoder; import com.googlecode.javacpp.IntPointer; import com.morlunk.jumble.audio.javacpp.Speex; import com.morlunk.jumble.exception.NativeAudioException; +import com.morlunk.jumble.net.PacketBuffer; + +import java.nio.BufferUnderflowException; /** * Wrapper performing preprocessing options on the nested encoder. @@ -35,21 +38,43 @@ public class PreprocessingEncoder implements IEncoder { mPreprocessor = new Speex.SpeexPreprocessState(frameSize, sampleRate); IntPointer arg = new IntPointer(1); + arg.put(0); mPreprocessor.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_VAD, arg); arg.put(1); mPreprocessor.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_AGC, arg); mPreprocessor.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_DENOISE, arg); mPreprocessor.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_DEREVERB, arg); + arg.put(30000); mPreprocessor.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_SET_AGC_TARGET, arg); + // TODO AGC max gain, decrement, noise suppress, echo + + // Increase VAD difficulty + arg.put(99); + mPreprocessor.control(Speex.SpeexPreprocessState.SPEEX_PREPROCESS_GET_PROB_START, arg); } @Override - public int encode(short[] input, int inputSize, byte[] output, int outputSize) throws NativeAudioException { + public int encode(short[] input, int inputSize) throws NativeAudioException { mPreprocessor.preprocess(input); - return mEncoder.encode(input, inputSize, output, outputSize); + return mEncoder.encode(input, inputSize); + } + + @Override + public int getBufferedFrames() { + return mEncoder.getBufferedFrames(); + } + + @Override + public boolean isReady() { + return mEncoder.isReady(); + } + + @Override + public void getEncodedData(PacketBuffer packetBuffer) throws BufferUnderflowException { + mEncoder.getEncodedData(packetBuffer); } @Override @@ -57,6 +82,11 @@ public class PreprocessingEncoder implements IEncoder { mEncoder.setBitrate(bitrate); } + @Override + public void terminate() throws NativeAudioException { + mEncoder.terminate(); + } + public void setEncoder(IEncoder encoder) { if(mEncoder != null) mEncoder.destroy(); mEncoder = encoder; diff --git a/src/main/java/com/morlunk/jumble/audio/ResamplingEncoder.java b/src/main/java/com/morlunk/jumble/audio/encoder/ResamplingEncoder.java index 4e87c07..6615892 100644 --- a/src/main/java/com/morlunk/jumble/audio/ResamplingEncoder.java +++ b/src/main/java/com/morlunk/jumble/audio/encoder/ResamplingEncoder.java @@ -15,31 +15,52 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package com.morlunk.jumble.audio; +package com.morlunk.jumble.audio.encoder; import com.morlunk.jumble.audio.javacpp.Speex; import com.morlunk.jumble.exception.NativeAudioException; +import com.morlunk.jumble.net.PacketBuffer; + +import java.nio.BufferUnderflowException; /** * Wraps around another encoder, resampling up/down all input using the Speex resampler. * Created by andrew on 16/04/14. */ public class ResamplingEncoder implements IEncoder { - private static final int SPEEX_RESAMPLE_QUALITY = 3; private IEncoder mEncoder; private Speex.SpeexResampler mResampler; + private final int mInputSampleRate; + private final int mTargetSampleRate; public ResamplingEncoder(IEncoder encoder, int channels, int inputSampleRate, int targetSampleRate) { mEncoder = encoder; + mInputSampleRate = inputSampleRate; + mTargetSampleRate = targetSampleRate; mResampler = new Speex.SpeexResampler(channels, inputSampleRate, targetSampleRate, SPEEX_RESAMPLE_QUALITY); } @Override - public int encode(short[] input, int inputSize, byte[] output, int outputSize) throws NativeAudioException { + public int encode(short[] input, int inputSize) throws NativeAudioException { mResampler.resample(input, input); - return mEncoder.encode(input, inputSize, output, outputSize); + return mEncoder.encode(input, inputSize * (mTargetSampleRate / mInputSampleRate)); + } + + @Override + public int getBufferedFrames() { + return mEncoder.getBufferedFrames(); + } + + @Override + public boolean isReady() { + return mEncoder.isReady(); + } + + @Override + public void getEncodedData(PacketBuffer packetBuffer) throws BufferUnderflowException { + mEncoder.getEncodedData(packetBuffer); } @Override @@ -47,6 +68,11 @@ public class ResamplingEncoder implements IEncoder { mEncoder.setBitrate(bitrate); } + @Override + public void terminate() throws NativeAudioException { + mEncoder.terminate(); + } + public void setEncoder(IEncoder encoder) { if(mEncoder != null) mEncoder.destroy(); mEncoder = encoder; diff --git a/src/main/java/com/morlunk/jumble/audio/javacpp/CELT11.java b/src/main/java/com/morlunk/jumble/audio/javacpp/CELT11.java index 2139eb4..47c2cf8 100644 --- a/src/main/java/com/morlunk/jumble/audio/javacpp/CELT11.java +++ b/src/main/java/com/morlunk/jumble/audio/javacpp/CELT11.java @@ -23,7 +23,6 @@ import com.googlecode.javacpp.Pointer; import com.googlecode.javacpp.annotation.Cast; import com.googlecode.javacpp.annotation.Platform; import com.morlunk.jumble.audio.IDecoder; -import com.morlunk.jumble.audio.IEncoder; import com.morlunk.jumble.exception.NativeAudioException; import java.nio.ByteBuffer; @@ -57,38 +56,6 @@ public class CELT11 { public static native int celt_encode(@Cast("CELTEncoder*") Pointer state, @Cast("const short*") short[] pcm, int frameSize, @Cast("unsigned char*") byte[] compressed, int maxCompressedBytes); public static native void celt_encoder_destroy(@Cast("CELTEncoder*") Pointer state); - public static class CELT11Encoder implements IEncoder { - - private Pointer mState; - - public CELT11Encoder(int sampleRate, int channels) throws NativeAudioException { - IntPointer error = new IntPointer(1); - error.put(0); - mState = celt_encoder_create(sampleRate, channels, error); - if(error.get() < 0) throw new NativeAudioException("CELT 0.11.0 encoder initialization failed with error: "+error.get()); - } - - @Override - public int encode(short[] input, int frameSize, byte[] output, int outputSize) throws NativeAudioException { - int result = celt_encode(mState, input, frameSize, output, outputSize); - if(result < 0) throw new NativeAudioException("CELT 0.11.0 encoding failed with error: "+result); - return result; - } - - @Override - public void setBitrate(int bitrate) { - // FIXME -// IntPointer ptr = new IntPointer(1); -// ptr.put(bitrate); -// celt_encoder_ctl(mState, CELT_SET_BITRATE_REQUEST, ptr); - } - - @Override - public void destroy() { - celt_encoder_destroy(mState); - } - } - public static class CELT11Decoder implements IDecoder { private Pointer mState; diff --git a/src/main/java/com/morlunk/jumble/audio/javacpp/CELT7.java b/src/main/java/com/morlunk/jumble/audio/javacpp/CELT7.java index 7c828eb..a8ff4a2 100644 --- a/src/main/java/com/morlunk/jumble/audio/javacpp/CELT7.java +++ b/src/main/java/com/morlunk/jumble/audio/javacpp/CELT7.java @@ -23,7 +23,6 @@ import com.googlecode.javacpp.Pointer; import com.googlecode.javacpp.annotation.Cast; import com.googlecode.javacpp.annotation.Platform; import com.morlunk.jumble.audio.IDecoder; -import com.morlunk.jumble.audio.IEncoder; import com.morlunk.jumble.exception.NativeAudioException; import java.nio.ByteBuffer; @@ -55,39 +54,6 @@ public class CELT7 { public static native int celt_encode(@Cast("CELTEncoder *") Pointer state, @Cast("const short *") short[] pcm, @Cast("short *") short[] optionalSynthesis, @Cast("unsigned char *") byte[] compressed, int nbCompressedBytes); public static native void celt_encoder_destroy(@Cast("CELTEncoder *") Pointer state); - public static class CELT7Encoder implements IEncoder { - - private Pointer mMode; - private Pointer mState; - - public CELT7Encoder(int sampleRate, int frameSize, int channels) throws NativeAudioException { - IntPointer error = new IntPointer(1); - error.put(0); - mMode = celt_mode_create(sampleRate, frameSize, error); - if(error.get() < 0) throw new NativeAudioException("CELT 0.7.0 encoder initialization failed with error: "+error.get()); - mState = celt_encoder_create(mMode, channels, error); - if(error.get() < 0) throw new NativeAudioException("CELT 0.7.0 encoder initialization failed with error: "+error.get()); - } - - @Override - public int encode(short[] input, int inputSize, byte[] output, int outputSize) throws NativeAudioException { - int result = celt_encode(mState, input, null, output, outputSize); - if(result < 0) throw new NativeAudioException("CELT 0.7.0 encoding failed with error: "+result); - return result; - } - - @Override - public void setBitrate(int bitrate) { - // FIXME - } - - @Override - public void destroy() { - celt_encoder_destroy(mState); - celt_mode_destroy(mMode); - } - } - public static class CELT7Decoder implements IDecoder { private Pointer mMode; diff --git a/src/main/java/com/morlunk/jumble/audio/javacpp/Opus.java b/src/main/java/com/morlunk/jumble/audio/javacpp/Opus.java index 2bbdc97..7dc79fc 100644 --- a/src/main/java/com/morlunk/jumble/audio/javacpp/Opus.java +++ b/src/main/java/com/morlunk/jumble/audio/javacpp/Opus.java @@ -23,7 +23,6 @@ import com.googlecode.javacpp.Pointer; import com.googlecode.javacpp.annotation.Cast; import com.googlecode.javacpp.annotation.Platform; import com.morlunk.jumble.audio.IDecoder; -import com.morlunk.jumble.audio.IEncoder; import com.morlunk.jumble.exception.NativeAudioException; import java.nio.ByteBuffer; @@ -68,42 +67,6 @@ public class Opus { Loader.load(); } - public static class OpusEncoder implements IEncoder { - - private Pointer mState; - - public OpusEncoder(int sampleRate, int channels) throws NativeAudioException { - IntPointer error = new IntPointer(1); - error.put(0); - mState = opus_encoder_create(sampleRate, channels, OPUS_APPLICATION_VOIP, error); - if(error.get() < 0) throw new NativeAudioException("Opus encoder initialization failed with error: "+error.get()); - Opus.opus_encoder_ctl(mState, Opus.OPUS_SET_VBR_REQUEST, 0); // enable CBR - } - - @Override - public int encode(short[] input, int inputSize, byte[] output, int outputSize) throws NativeAudioException { - int result = Opus.opus_encode(mState, input, inputSize, output, outputSize); - if(result < 0) throw new NativeAudioException("Opus encoding failed with error: "+result); - return result; - } - - @Override - public void setBitrate(int bitrate) { - Opus.opus_encoder_ctl(mState, Opus.OPUS_SET_BITRATE_REQUEST, bitrate); - } - - public int getBitrate() { - IntPointer ptr = new IntPointer(1); - Opus.opus_encoder_ctl(mState, OPUS_GET_BITRATE_REQUEST, ptr); - return ptr.get(); - } - - @Override - public void destroy() { - Opus.opus_encoder_destroy(mState); - } - } - public static class OpusDecoder implements IDecoder { private Pointer mState; diff --git a/src/main/java/com/morlunk/jumble/protocol/AudioHandler.java b/src/main/java/com/morlunk/jumble/protocol/AudioHandler.java index 6d55a56..65be08a 100644 --- a/src/main/java/com/morlunk/jumble/protocol/AudioHandler.java +++ b/src/main/java/com/morlunk/jumble/protocol/AudioHandler.java @@ -22,16 +22,26 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; +import android.util.Log; import android.widget.Toast; import com.morlunk.jumble.Constants; import com.morlunk.jumble.R; import com.morlunk.jumble.audio.AudioInput; import com.morlunk.jumble.audio.AudioOutput; +import com.morlunk.jumble.audio.encoder.CELT11Encoder; +import com.morlunk.jumble.audio.encoder.CELT7Encoder; +import com.morlunk.jumble.audio.encoder.IEncoder; +import com.morlunk.jumble.audio.encoder.OpusEncoder; +import com.morlunk.jumble.audio.encoder.PreprocessingEncoder; +import com.morlunk.jumble.audio.encoder.ResamplingEncoder; import com.morlunk.jumble.exception.AudioException; import com.morlunk.jumble.exception.AudioInitializationException; +import com.morlunk.jumble.exception.NativeAudioException; import com.morlunk.jumble.model.Message; +import com.morlunk.jumble.model.User; import com.morlunk.jumble.net.JumbleUDPMessageType; +import com.morlunk.jumble.net.PacketBuffer; import com.morlunk.jumble.protobuf.Mumble; import com.morlunk.jumble.util.JumbleLogger; import com.morlunk.jumble.util.JumbleNetworkListener; @@ -44,7 +54,7 @@ import com.morlunk.jumble.util.JumbleNetworkListener; * Calling shutdown() will cleanup both input and output threads. It is safe to restart after. * Created by andrew on 23/04/14. */ -public class AudioHandler extends JumbleNetworkListener { +public class AudioHandler extends JumbleNetworkListener implements AudioInput.AudioInputListener { public static final int SAMPLE_RATE = 48000; public static final int FRAME_SIZE = SAMPLE_RATE/100; @@ -53,10 +63,13 @@ public class AudioHandler extends JumbleNetworkListener { private AudioManager mAudioManager; private AudioInput mInput; private AudioOutput mOutput; - private AudioInput.AudioInputListener mInputListener; private AudioOutput.AudioOutputListener mOutputListener; + private AudioEncodeListener mEncodeListener; + + private JumbleUDPMessageType mCodec; + private IEncoder mEncoder; + private int mFrameCounter; - private JumbleUDPMessageType mCodec = JumbleUDPMessageType.UDPVoiceOpus; private int mAudioStream; private int mAudioSource; private int mSampleRate; @@ -104,10 +117,11 @@ public class AudioHandler extends JumbleNetworkListener { } }; - public AudioHandler(Context context, JumbleLogger logger, AudioInput.AudioInputListener inputListener, AudioOutput.AudioOutputListener outputListener) { + public AudioHandler(Context context, JumbleLogger logger, AudioEncodeListener encodeListener, + AudioOutput.AudioOutputListener outputListener) { mContext = context; mLogger = logger; - mInputListener = inputListener; + mEncodeListener = encodeListener; mOutputListener = outputListener; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } @@ -121,8 +135,8 @@ public class AudioHandler extends JumbleNetworkListener { private void createAudioInput() throws AudioException { if(mInput != null) mInput.shutdown(); - mInput = new AudioInput(mInputListener, mCodec, mAudioSource, mSampleRate, mBitrate, - mFramesPerPacket, mTransmitMode, mVADThreshold, mAmplitudeBoost, mPreprocessorEnabled); + mInput = new AudioInput(this, mAudioSource, mSampleRate, mTransmitMode, + mVADThreshold, mAmplitudeBoost); if(mTransmitMode == Constants.TRANSMIT_VOICE_ACTIVITY || mTransmitMode == Constants.TRANSMIT_CONTINUOUS) { mInput.startRecording(); } @@ -145,7 +159,7 @@ public class AudioHandler extends JumbleNetworkListener { public synchronized void initialize() throws AudioException { if(mInitialized) return; if(mOutput == null) createAudioOutput(); - if(mInput == null && mCodec != null) createAudioInput(); + if(mInput == null) createAudioInput(); // This sticky broadcast will initialize the audio output. mContext.registerReceiver(mBluetoothReceiver, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED)); mInitialized = true; @@ -184,6 +198,37 @@ public class AudioHandler extends JumbleNetworkListener { return mCodec; } + public void setCodec(JumbleUDPMessageType codec) throws NativeAudioException { + mCodec = codec; + + IEncoder encoder; + switch (codec) { + case UDPVoiceCELTAlpha: + encoder = new CELT7Encoder(SAMPLE_RATE, AudioHandler.FRAME_SIZE, 1, + mFramesPerPacket); + break; + case UDPVoiceCELTBeta: + encoder = new CELT11Encoder(SAMPLE_RATE, 1, mFramesPerPacket); + break; + case UDPVoiceOpus: + encoder = new OpusEncoder(SAMPLE_RATE, 1, FRAME_SIZE, mFramesPerPacket); + break; + default: + Log.w(Constants.TAG, "Unsupported codec, input disabled."); + return; + } + + if (mPreprocessorEnabled) { + encoder = new PreprocessingEncoder(encoder, FRAME_SIZE, SAMPLE_RATE); + } + + if (mInput.getSampleRate() != SAMPLE_RATE) { + encoder = new ResamplingEncoder(encoder, 1, mInput.getSampleRate(), SAMPLE_RATE); + } + + mEncoder = encoder; + } + public int getAudioStream() { return mAudioStream; } @@ -238,7 +283,7 @@ public class AudioHandler extends JumbleNetworkListener { */ public void setBitrate(int bitrate) { this.mBitrate = bitrate; - if(mInput != null) mInput.setBitrate(bitrate); + if(mEncoder != null) mEncoder.setBitrate(bitrate); } public int getFramesPerPacket() { @@ -247,12 +292,10 @@ public class AudioHandler extends JumbleNetworkListener { /** * Sets the number of frames per packet to be encoded before sending to the server. - * The input thread will be automatically respawned if currently recording. * @param framesPerPacket The number of frames per audio packet. */ public void setFramesPerPacket(int framesPerPacket) throws AudioException { this.mFramesPerPacket = framesPerPacket; - if(mInput != null) createAudioInput(); } public int getTransmitMode() { @@ -323,7 +366,7 @@ public class AudioHandler extends JumbleNetworkListener { */ public void setPreprocessorEnabled(boolean preprocessorEnabled) { mPreprocessorEnabled = preprocessorEnabled; - if (mInitialized) mInput.setPreprocessorEnabled(preprocessorEnabled); + // FIXME } /** @@ -355,19 +398,20 @@ public class AudioHandler extends JumbleNetworkListener { @Override public void messageCodecVersion(Mumble.CodecVersion msg) { + JumbleUDPMessageType codec; if (msg.hasOpus() && msg.getOpus()) { - mCodec = JumbleUDPMessageType.UDPVoiceOpus; + codec = JumbleUDPMessageType.UDPVoiceOpus; } else if (msg.hasBeta() && !msg.getPreferAlpha()) { - mCodec = JumbleUDPMessageType.UDPVoiceCELTBeta; + codec = JumbleUDPMessageType.UDPVoiceCELTBeta; } else { - mCodec = JumbleUDPMessageType.UDPVoiceCELTAlpha; + codec = JumbleUDPMessageType.UDPVoiceCELTAlpha; } - if(mInitialized) { + + if (codec != mCodec) { try { - createAudioInput(); - } catch (AudioException e) { + setCodec(codec); + } catch (NativeAudioException e) { e.printStackTrace(); - // TODO handle gracefully } } } @@ -376,4 +420,64 @@ public class AudioHandler extends JumbleNetworkListener { public void messageVoiceData(byte[] data, JumbleUDPMessageType messageType) { mOutput.queueVoiceData(data, messageType); } + + @Override + public void onTalkStateChange(User.TalkState state) { +// if (mEncoder != null && state == User.TalkState.PASSIVE) { +// try { +// mEncoder.terminate(); +// if (mEncoder.isReady()) { +// sendEncodedAudio(); +// } +// } catch (NativeAudioException e) { +// e.printStackTrace(); +// } +// } + mEncodeListener.onTalkStateChange(state); + } + + @Override + public void onAudioInputReceived(short[] frame, int frameSize) { + if (mEncoder != null) { + try { + mEncoder.encode(frame, frameSize); + mFrameCounter++; + } catch (NativeAudioException e) { + e.printStackTrace(); + return; + } + + if (mEncoder.isReady()) { + sendEncodedAudio(); + } + } + } + + /** + * Fetches the buffered audio from the current encoder and sends it to the server. + */ + private void sendEncodedAudio() { + int frames = mEncoder.getBufferedFrames(); + + int flags = 0; + flags |= mCodec.ordinal() << 5; + + final byte[] packetBuffer = new byte[1024]; + packetBuffer[0] = (byte) (flags & 0xFF); + + PacketBuffer ds = new PacketBuffer(packetBuffer, 1024); + ds.skip(1); + ds.writeLong(mFrameCounter - frames); + mEncoder.getEncodedData(ds); + int length = ds.size(); + ds.rewind(); + + byte[] packet = ds.dataBlock(length); + mEncodeListener.onAudioEncoded(packet, length); + } + + public interface AudioEncodeListener { + public void onAudioEncoded(byte[] data, int length); + public void onTalkStateChange(User.TalkState state); + } } |