diff --git a/README.md b/README.md index d318423..da5d155 100644 --- a/README.md +++ b/README.md @@ -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 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 Audio file originals from [freesound](https://freesound.org). This whole project originally created by [brarcher](https://github.com/brarcher/protect-baby-monitor). diff --git a/app/src/main/java/de/rochefort/childmonitor/AudioCodecDefines.java b/app/src/main/java/de/rochefort/childmonitor/AudioCodecDefines.java index a48637e..6ca4ed6 100644 --- a/app/src/main/java/de/rochefort/childmonitor/AudioCodecDefines.java +++ b/app/src/main/java/de/rochefort/childmonitor/AudioCodecDefines.java @@ -18,13 +18,15 @@ package de.rochefort.childmonitor; import android.media.AudioFormat; +import de.rochefort.childmonitor.audio.G711UCodec; + public class AudioCodecDefines { public static final int FREQUENCY = 11025; 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_OUT = AudioFormat.CHANNEL_OUT_MONO; - private AudioCodecDefines() { throw new IllegalStateException("Do not instantiate!"); } diff --git a/app/src/main/java/de/rochefort/childmonitor/AudioListener.java b/app/src/main/java/de/rochefort/childmonitor/AudioListener.java index 053b825..276d32a 100644 --- a/app/src/main/java/de/rochefort/childmonitor/AudioListener.java +++ b/app/src/main/java/de/rochefort/childmonitor/AudioListener.java @@ -17,5 +17,5 @@ package de.rochefort.childmonitor; public interface AudioListener { - void onAudio(byte[] audioBytes); + void onAudio(short[] audioBytes); } diff --git a/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java b/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java index fdf7eeb..6e4b77d 100644 --- a/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java +++ b/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java @@ -16,6 +16,8 @@ */ package de.rochefort.childmonitor; +import static de.rochefort.childmonitor.AudioCodecDefines.CODEC; + import java.io.IOException; import java.io.InputStream; import java.net.Socket; @@ -31,6 +33,8 @@ import android.widget.TextView; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; +import de.rochefort.childmonitor.audio.G711UCodec; + public class ListenActivity extends Activity { final String TAG = "ChildMonitor"; @@ -69,21 +73,26 @@ public class ListenActivity extends Activity 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) { - 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); - byte[] readBytes = new byte[read]; - System.arraycopy(buffer, 0, readBytes, 0, read); - listener.onAudio(readBytes); + audioTrack.write(decodedBuffer, 0, decoded); + short[] decodedBytes = new short[decoded]; + System.arraycopy(decodedBuffer, 0, decodedBytes, 0, decoded); + listener.onAudio(decodedBytes); } } } + catch (Exception e) { + Log.e(TAG, "Connection failed", e); + } finally { audioTrack.stop(); @@ -125,7 +134,7 @@ public class ListenActivity extends Activity final AudioListener listener = new AudioListener() { @Override - public void onAudio(final byte[] audioData) { + public void onAudio(final short[] audioData) { runOnUiThread(new Runnable() { @Override public void run() { diff --git a/app/src/main/java/de/rochefort/childmonitor/MonitorActivity.java b/app/src/main/java/de/rochefort/childmonitor/MonitorActivity.java index 74acab0..b32147c 100644 --- a/app/src/main/java/de/rochefort/childmonitor/MonitorActivity.java +++ b/app/src/main/java/de/rochefort/childmonitor/MonitorActivity.java @@ -16,6 +16,8 @@ */ package de.rochefort.childmonitor; +import static de.rochefort.childmonitor.AudioCodecDefines.CODEC; + import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; @@ -53,13 +55,9 @@ public class MonitorActivity extends Activity { private int currentPort; private void serviceConnection(Socket socket) { - runOnUiThread(new Runnable() { - @Override - public void run() - { - final TextView statusText = (TextView) findViewById(R.id.textStatus); - statusText.setText(R.string.streaming); - } + runOnUiThread(() -> { + final TextView statusText = (TextView) findViewById(R.id.textStatus); + statusText.setText(R.string.streaming); }); final int frequency = AudioCodecDefines.FREQUENCY; @@ -75,21 +73,24 @@ public class MonitorActivity extends Activity { bufferSize ); - final int byteBufferSize = bufferSize*2; - final byte[] buffer = new byte[byteBufferSize]; + final int pcmBufferSize = bufferSize*2; + final short[] pcmBuffer = new short[pcmBufferSize]; + final byte[] ulawBuffer = new byte[pcmBufferSize]; try { audioRecord.startRecording(); final OutputStream out = socket.getOutputStream(); - socket.setSendBufferSize(byteBufferSize); + socket.setSendBufferSize(pcmBufferSize); Log.d(TAG, "Socket send buffer size: " + socket.getSendBufferSize()); while (socket.isConnected() && !Thread.currentThread().isInterrupted()) { - final int read = audioRecord.read(buffer, 0, bufferSize); - out.write(buffer, 0, read); + final int read = audioRecord.read(pcmBuffer, 0, bufferSize); + 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); } finally { audioRecord.stop(); @@ -109,35 +110,31 @@ public class MonitorActivity extends Activity { final Object currentToken = new Object(); connectionToken = currentToken; - new Thread(new Runnable() { - @Override - public void run() - { - while(Objects.equals(connectionToken, currentToken)) { - try (ServerSocket serverSocket = new ServerSocket(currentPort)) { - currentSocket = serverSocket; - // Store the chosen port. - final int localPort = serverSocket.getLocalPort(); + new Thread(() -> { + while(Objects.equals(connectionToken, currentToken)) { + try (ServerSocket serverSocket = new ServerSocket(currentPort)) { + currentSocket = serverSocket; + // Store the chosen port. + final int localPort = serverSocket.getLocalPort(); - // Register the service so that parent devices can - // locate the child device - registerService(localPort); + // Register the service so that parent devices can + // locate the child device + registerService(localPort); - // Wait for a parent to find us and connect - try (Socket socket = serverSocket.accept()) { - Log.i(TAG, "Connection from parent device received"); + // Wait for a parent to find us and connect + try (Socket socket = serverSocket.accept()) { + Log.i(TAG, "Connection from parent device received"); - // We now have a client connection. - // Unregister so no other clients will - // attempt to connect - unregisterService(); - serviceConnection(socket); - } - } catch(IOException e) { - // Just in case - currentPort++; - Log.e(TAG, "Failed to open server socket. Port increased to " + currentPort, e); + // We now have a client connection. + // Unregister so no other clients will + // attempt to connect + unregisterService(); + serviceConnection(socket); } + } catch(Exception e) { + // Just in case + currentPort++; + Log.e(TAG, "Failed to open server socket. Port increased to " + currentPort, e); } } }).start(); diff --git a/app/src/main/java/de/rochefort/childmonitor/VolumeView.java b/app/src/main/java/de/rochefort/childmonitor/VolumeView.java index f091c47..2f0e2e2 100644 --- a/app/src/main/java/de/rochefort/childmonitor/VolumeView.java +++ b/app/src/main/java/de/rochefort/childmonitor/VolumeView.java @@ -56,7 +56,7 @@ public class VolumeView extends View { paint.setColor(Color.rgb(255, 127, 0)); } - public void onAudioData(byte[] data) { + public void onAudioData(short[] data) { double sum = 0; for (int i = 0; i < data.length; i++) { double rel = data[i] / ((double)128); @@ -94,7 +94,7 @@ public class VolumeView extends View { double margins = height * 0.1; double graphHeight = height - 2*margins; 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++) { int xNext = i - leftMost; int yNext = (int) (margins + graphHeight - volumeHistory.get(i) / maxVolume * (graphHeight)); diff --git a/app/src/main/java/de/rochefort/childmonitor/audio/G711UCodec.java b/app/src/main/java/de/rochefort/childmonitor/audio/G711UCodec.java new file mode 100644 index 0000000..c93763a --- /dev/null +++ b/app/src/main/java/de/rochefort/childmonitor/audio/G711UCodec.java @@ -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; + } +} \ No newline at end of file