Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Morlunk/Jumble.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Comminos <andrew@morlunk.com>2014-12-09 07:09:05 +0300
committerAndrew Comminos <andrew@morlunk.com>2014-12-09 07:09:05 +0300
commit88a6c52615ac0686652c14c9049935061a822fb6 (patch)
tree4292c02b48abfb71e978a36cc1ec31ea3c2c4928
parent77953bcb550d86a490188eda05737bbe14fd2adc (diff)
Initial transition to more modular encoding process.
-rw-r--r--src/androidTest/java/com/morlunk/jumble/test/EncoderTest.java11
-rw-r--r--src/main/java/com/morlunk/jumble/JumbleService.java9
-rw-r--r--src/main/java/com/morlunk/jumble/audio/AudioInput.java293
-rw-r--r--src/main/java/com/morlunk/jumble/audio/IEncoder.java38
-rw-r--r--src/main/java/com/morlunk/jumble/audio/encoder/CELT11Encoder.java114
-rw-r--r--src/main/java/com/morlunk/jumble/audio/encoder/CELT7Encoder.java114
-rw-r--r--src/main/java/com/morlunk/jumble/audio/encoder/IEncoder.java73
-rw-r--r--src/main/java/com/morlunk/jumble/audio/encoder/OpusEncoder.java147
-rw-r--r--src/main/java/com/morlunk/jumble/audio/encoder/PreprocessingEncoder.java (renamed from src/main/java/com/morlunk/jumble/audio/PreprocessingEncoder.java)36
-rw-r--r--src/main/java/com/morlunk/jumble/audio/encoder/ResamplingEncoder.java (renamed from src/main/java/com/morlunk/jumble/audio/ResamplingEncoder.java)34
-rw-r--r--src/main/java/com/morlunk/jumble/audio/javacpp/CELT11.java33
-rw-r--r--src/main/java/com/morlunk/jumble/audio/javacpp/CELT7.java34
-rw-r--r--src/main/java/com/morlunk/jumble/audio/javacpp/Opus.java37
-rw-r--r--src/main/java/com/morlunk/jumble/protocol/AudioHandler.java142
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);
+ }
}