Add AudioCompression (ulaw)

This commit is contained in:
edr
2023-09-30 14:00:28 +02:00
parent 0fc379c25b
commit 9ec30c33da
7 changed files with 132 additions and 50 deletions

View File

@@ -21,6 +21,9 @@ and streams audio. Room for improvement includes:
At the time this project was forked from _Protect Baby Monitor_ there was no obvious open source solution for a At the time this project was forked from _Protect Baby Monitor_ there was no obvious open source solution for a
baby monitor for Android in F-Droid. baby monitor for Android in F-Droid.
# License information
_Child Monitor_ is licensed under the GPLv3. The Ulaw encoding / decoding code is licensed under the Apache License, Version 2.0 and taken from the Android Open Source Project.
# Thanks # Thanks
Audio file originals from [freesound](https://freesound.org). Audio file originals from [freesound](https://freesound.org).
This whole project originally created by [brarcher](https://github.com/brarcher/protect-baby-monitor). This whole project originally created by [brarcher](https://github.com/brarcher/protect-baby-monitor).

View File

@@ -18,13 +18,15 @@ package de.rochefort.childmonitor;
import android.media.AudioFormat; import android.media.AudioFormat;
import de.rochefort.childmonitor.audio.G711UCodec;
public class AudioCodecDefines { public class AudioCodecDefines {
public static final int FREQUENCY = 11025; public static final int FREQUENCY = 11025;
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final G711UCodec CODEC = new G711UCodec();
public static final int CHANNEL_CONFIGURATION_IN = AudioFormat.CHANNEL_IN_MONO; public static final int CHANNEL_CONFIGURATION_IN = AudioFormat.CHANNEL_IN_MONO;
public static final int CHANNEL_CONFIGURATION_OUT = AudioFormat.CHANNEL_OUT_MONO; public static final int CHANNEL_CONFIGURATION_OUT = AudioFormat.CHANNEL_OUT_MONO;
private AudioCodecDefines() { private AudioCodecDefines() {
throw new IllegalStateException("Do not instantiate!"); throw new IllegalStateException("Do not instantiate!");
} }

View File

@@ -17,5 +17,5 @@
package de.rochefort.childmonitor; package de.rochefort.childmonitor;
public interface AudioListener { public interface AudioListener {
void onAudio(byte[] audioBytes); void onAudio(short[] audioBytes);
} }

View File

@@ -16,6 +16,8 @@
*/ */
package de.rochefort.childmonitor; package de.rochefort.childmonitor;
import static de.rochefort.childmonitor.AudioCodecDefines.CODEC;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.Socket; import java.net.Socket;
@@ -31,6 +33,8 @@ import android.widget.TextView;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat; import android.support.v4.app.NotificationManagerCompat;
import de.rochefort.childmonitor.audio.G711UCodec;
public class ListenActivity extends Activity public class ListenActivity extends Activity
{ {
final String TAG = "ChildMonitor"; final String TAG = "ChildMonitor";
@@ -69,21 +73,26 @@ public class ListenActivity extends Activity
try try
{ {
final byte [] buffer = new byte[byteBufferSize]; final byte [] readBuffer = new byte[byteBufferSize];
final short [] decodedBuffer = new short[byteBufferSize*2];
while(socket.isConnected() && read != -1 && Thread.currentThread().isInterrupted() == false) while(socket.isConnected() && read != -1 && Thread.currentThread().isInterrupted() == false)
{ {
read = is.read(buffer); read = is.read(readBuffer);
int decoded = CODEC.decode(decodedBuffer, readBuffer, read, 0);
if(read > 0) if(decoded > 0)
{ {
audioTrack.write(buffer, 0, read); audioTrack.write(decodedBuffer, 0, decoded);
byte[] readBytes = new byte[read]; short[] decodedBytes = new short[decoded];
System.arraycopy(buffer, 0, readBytes, 0, read); System.arraycopy(decodedBuffer, 0, decodedBytes, 0, decoded);
listener.onAudio(readBytes); listener.onAudio(decodedBytes);
} }
} }
} }
catch (Exception e) {
Log.e(TAG, "Connection failed", e);
}
finally finally
{ {
audioTrack.stop(); audioTrack.stop();
@@ -125,7 +134,7 @@ public class ListenActivity extends Activity
final AudioListener listener = new AudioListener() { final AudioListener listener = new AudioListener() {
@Override @Override
public void onAudio(final byte[] audioData) { public void onAudio(final short[] audioData) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {

View File

@@ -16,6 +16,8 @@
*/ */
package de.rochefort.childmonitor; package de.rochefort.childmonitor;
import static de.rochefort.childmonitor.AudioCodecDefines.CODEC;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetAddress; import java.net.InetAddress;
@@ -53,13 +55,9 @@ public class MonitorActivity extends Activity {
private int currentPort; private int currentPort;
private void serviceConnection(Socket socket) { private void serviceConnection(Socket socket) {
runOnUiThread(new Runnable() { runOnUiThread(() -> {
@Override
public void run()
{
final TextView statusText = (TextView) findViewById(R.id.textStatus); final TextView statusText = (TextView) findViewById(R.id.textStatus);
statusText.setText(R.string.streaming); statusText.setText(R.string.streaming);
}
}); });
final int frequency = AudioCodecDefines.FREQUENCY; final int frequency = AudioCodecDefines.FREQUENCY;
@@ -75,21 +73,24 @@ public class MonitorActivity extends Activity {
bufferSize bufferSize
); );
final int byteBufferSize = bufferSize*2; final int pcmBufferSize = bufferSize*2;
final byte[] buffer = new byte[byteBufferSize]; final short[] pcmBuffer = new short[pcmBufferSize];
final byte[] ulawBuffer = new byte[pcmBufferSize];
try { try {
audioRecord.startRecording(); audioRecord.startRecording();
final OutputStream out = socket.getOutputStream(); final OutputStream out = socket.getOutputStream();
socket.setSendBufferSize(byteBufferSize); socket.setSendBufferSize(pcmBufferSize);
Log.d(TAG, "Socket send buffer size: " + socket.getSendBufferSize()); Log.d(TAG, "Socket send buffer size: " + socket.getSendBufferSize());
while (socket.isConnected() && !Thread.currentThread().isInterrupted()) { while (socket.isConnected() && !Thread.currentThread().isInterrupted()) {
final int read = audioRecord.read(buffer, 0, bufferSize); final int read = audioRecord.read(pcmBuffer, 0, bufferSize);
out.write(buffer, 0, read); int encoded = CODEC.encode(pcmBuffer, read, ulawBuffer, 0);
out.write(ulawBuffer, 0, encoded);
} }
} catch (IOException e) { } catch (Exception e) {
Log.e(TAG, "Connection failed", e); Log.e(TAG, "Connection failed", e);
} finally { } finally {
audioRecord.stop(); audioRecord.stop();
@@ -109,10 +110,7 @@ public class MonitorActivity extends Activity {
final Object currentToken = new Object(); final Object currentToken = new Object();
connectionToken = currentToken; connectionToken = currentToken;
new Thread(new Runnable() { new Thread(() -> {
@Override
public void run()
{
while(Objects.equals(connectionToken, currentToken)) { while(Objects.equals(connectionToken, currentToken)) {
try (ServerSocket serverSocket = new ServerSocket(currentPort)) { try (ServerSocket serverSocket = new ServerSocket(currentPort)) {
currentSocket = serverSocket; currentSocket = serverSocket;
@@ -133,13 +131,12 @@ public class MonitorActivity extends Activity {
unregisterService(); unregisterService();
serviceConnection(socket); serviceConnection(socket);
} }
} catch(IOException e) { } catch(Exception e) {
// Just in case // Just in case
currentPort++; currentPort++;
Log.e(TAG, "Failed to open server socket. Port increased to " + currentPort, e); Log.e(TAG, "Failed to open server socket. Port increased to " + currentPort, e);
} }
} }
}
}).start(); }).start();
final TextView addressText = findViewById(R.id.address); final TextView addressText = findViewById(R.id.address);

View File

@@ -56,7 +56,7 @@ public class VolumeView extends View {
paint.setColor(Color.rgb(255, 127, 0)); paint.setColor(Color.rgb(255, 127, 0));
} }
public void onAudioData(byte[] data) { public void onAudioData(short[] data) {
double sum = 0; double sum = 0;
for (int i = 0; i < data.length; i++) { for (int i = 0; i < data.length; i++) {
double rel = data[i] / ((double)128); double rel = data[i] / ((double)128);
@@ -94,7 +94,7 @@ public class VolumeView extends View {
double margins = height * 0.1; double margins = height * 0.1;
double graphHeight = height - 2*margins; double graphHeight = height - 2*margins;
int leftMost = Math.max(0, volumeHistory.size() - width); int leftMost = Math.max(0, volumeHistory.size() - width);
int yPrev = (int) (graphHeight - margins); int yPrev = (int) (height - margins);
for (int i = leftMost; i < volumeHistory.size() && i - leftMost < width; i++) { for (int i = leftMost; i < volumeHistory.size() && i - leftMost < width; i++) {
int xNext = i - leftMost; int xNext = i - leftMost;
int yNext = (int) (margins + graphHeight - volumeHistory.get(i) / maxVolume * (graphHeight)); int yNext = (int) (margins + graphHeight - volumeHistory.get(i) / maxVolume * (graphHeight));

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Taken from https://android.googlesource.com/platform/external/nist-sip/+/6f95fdeab4481188b6260041b41d1db12b101266/src/com/android/sip/media/G711UCodec.java
*
*/
package de.rochefort.childmonitor.audio;
/**
* G.711 codec. This class provides u-law conversion.
*/
public class G711UCodec {
// s00000001wxyz...s000wxyz
// s0000001wxyza...s001wxyz
// s000001wxyzab...s010wxyz
// s00001wxyzabc...s011wxyz
// s0001wxyzabcd...s100wxyz
// s001wxyzabcde...s101wxyz
// s01wxyzabcdef...s110wxyz
// s1wxyzabcdefg...s111wxyz
private static byte[] table13to8 = new byte[8192];
private static short[] table8to16 = new short[256];
static {
// b13 --> b8
for (int p = 1, q = 0; p <= 0x80; p <<= 1, q+=0x10) {
for (int i = 0, j = (p << 4) - 0x10; i < 16; i++, j += p) {
int v = (i + q) ^ 0x7F;
byte value1 = (byte) v;
byte value2 = (byte) (v + 128);
for (int m = j, e = j + p; m < e; m++) {
table13to8[m] = value1;
table13to8[8191 - m] = value2;
}
}
}
// b8 --> b16
for (int q = 0; q <= 7; q++) {
for (int i = 0, m = (q << 4); i < 16; i++, m++) {
int v = (((i + 0x10) << q) - 0x10) << 3;
table8to16[m ^ 0x7F] = (short) v;
table8to16[(m ^ 0x7F) + 128] = (short) (65536 - v);
}
}
}
public int decode(short[] b16, byte[] ulaw, int count, int offset) {
for (int i = 0, j = offset; i < count; i++, j++) {
b16[i] = table8to16[ulaw[j] & 0xFF];
}
return count;
}
public int encode(short[] b16, int count, byte[] b8, int offset) {
for (int i = 0, j = offset; i < count; i++, j++) {
b8[j] = table13to8[(b16[i] >> 4) & 0x1FFF];
}
return count;
}
public int getSampleCount(int frameSize) {
return frameSize;
}
}