Add support for saving local audio input to file in AppRTCMobile
Uses new WebRtcAudioRecordSamplesReadyCallback which was added recently in
https://webrtc-review.googlesource.com/c/src/+/49981.
This CL:
- Serves as a test of new WebRtcAudioRecordSamplesReadyCallback.
- Useful for debugging purposes since it records the most native raw audio.
Bug: None
Change-Id: I57375cbf237c171e045b0bdb05f7ae1401930fbc
Reviewed-on: https://webrtc-review.googlesource.com/53120
Commit-Queue: Henrik Andreassson <henrika@webrtc.org>
Reviewed-by: Sami Kalliomäki <sakal@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#22128}
diff --git a/examples/BUILD.gn b/examples/BUILD.gn
index c37bcce..c6940e8 100644
--- a/examples/BUILD.gn
+++ b/examples/BUILD.gn
@@ -84,6 +84,7 @@
"androidapp/src/org/appspot/apprtc/PeerConnectionClient.java",
"androidapp/src/org/appspot/apprtc/RoomParametersFetcher.java",
"androidapp/src/org/appspot/apprtc/RtcEventLog.java",
+ "androidapp/src/org/appspot/apprtc/RecordedAudioToFileController.java",
"androidapp/src/org/appspot/apprtc/SettingsActivity.java",
"androidapp/src/org/appspot/apprtc/SettingsFragment.java",
"androidapp/src/org/appspot/apprtc/TCPChannelClient.java",
diff --git a/examples/androidapp/res/values/strings.xml b/examples/androidapp/res/values/strings.xml
index 4aec2c1..ea525eb 100644
--- a/examples/androidapp/res/values/strings.xml
+++ b/examples/androidapp/res/values/strings.xml
@@ -125,6 +125,11 @@
<string name="pref_aecdump_dlg">Enable diagnostic audio recordings.</string>
<string name="pref_aecdump_default">false</string>
+ <string name="pref_enable_save_input_audio_to_file_key">enable_key</string>
+ <string name="pref_enable_save_input_audio_to_file_title">Save input audio to file.</string>
+ <string name="pref_enable_save_input_audio_to_file_dlg">Save input audio to file.</string>
+ <string name="pref_enable_save_input_audio_to_file_default">false</string>
+
<string name="pref_opensles_key">opensles_preference</string>
<string name="pref_opensles_title">Use OpenSL ES for audio playback.</string>
<string name="pref_opensles_dlg">Use OpenSL ES for audio playback.</string>
diff --git a/examples/androidapp/res/xml/preferences.xml b/examples/androidapp/res/xml/preferences.xml
index e8b9547..6372a6e 100644
--- a/examples/androidapp/res/xml/preferences.xml
+++ b/examples/androidapp/res/xml/preferences.xml
@@ -124,6 +124,12 @@
android:defaultValue="@string/pref_aecdump_default" />
<CheckBoxPreference
+ android:key="@string/pref_enable_save_input_audio_to_file_key"
+ android:title="@string/pref_enable_save_input_audio_to_file_title"
+ android:dialogTitle="@string/pref_enable_save_input_audio_to_file_dlg"
+ android:defaultValue="@string/pref_enable_save_input_audio_to_file_default" />
+
+ <CheckBoxPreference
android:key="@string/pref_opensles_key"
android:title="@string/pref_opensles_title"
android:dialogTitle="@string/pref_opensles_dlg"
diff --git a/examples/androidapp/src/org/appspot/apprtc/CallActivity.java b/examples/androidapp/src/org/appspot/apprtc/CallActivity.java
index 70d0e40..eb3f263 100644
--- a/examples/androidapp/src/org/appspot/apprtc/CallActivity.java
+++ b/examples/androidapp/src/org/appspot/apprtc/CallActivity.java
@@ -90,6 +90,8 @@
public static final String EXTRA_NOAUDIOPROCESSING_ENABLED =
"org.appspot.apprtc.NOAUDIOPROCESSING";
public static final String EXTRA_AECDUMP_ENABLED = "org.appspot.apprtc.AECDUMP";
+ public static final String EXTRA_SAVE_INPUT_AUDIO_TO_FILE_ENABLED =
+ "org.appspot.apprtc.SAVE_INPUT_AUDIO_TO_FILE";
public static final String EXTRA_OPENSLES_ENABLED = "org.appspot.apprtc.OPENSLES";
public static final String EXTRA_DISABLE_BUILT_IN_AEC = "org.appspot.apprtc.DISABLE_BUILT_IN_AEC";
public static final String EXTRA_DISABLE_BUILT_IN_AGC = "org.appspot.apprtc.DISABLE_BUILT_IN_AGC";
@@ -331,6 +333,7 @@
intent.getIntExtra(EXTRA_AUDIO_BITRATE, 0), intent.getStringExtra(EXTRA_AUDIOCODEC),
intent.getBooleanExtra(EXTRA_NOAUDIOPROCESSING_ENABLED, false),
intent.getBooleanExtra(EXTRA_AECDUMP_ENABLED, false),
+ intent.getBooleanExtra(EXTRA_SAVE_INPUT_AUDIO_TO_FILE_ENABLED, false),
intent.getBooleanExtra(EXTRA_OPENSLES_ENABLED, false),
intent.getBooleanExtra(EXTRA_DISABLE_BUILT_IN_AEC, false),
intent.getBooleanExtra(EXTRA_DISABLE_BUILT_IN_AGC, false),
diff --git a/examples/androidapp/src/org/appspot/apprtc/ConnectActivity.java b/examples/androidapp/src/org/appspot/apprtc/ConnectActivity.java
index 17b2f8da..10e55a5 100644
--- a/examples/androidapp/src/org/appspot/apprtc/ConnectActivity.java
+++ b/examples/androidapp/src/org/appspot/apprtc/ConnectActivity.java
@@ -315,10 +315,14 @@
CallActivity.EXTRA_NOAUDIOPROCESSING_ENABLED, R.string.pref_noaudioprocessing_default,
useValuesFromIntent);
- // Check Disable Audio Processing flag.
boolean aecDump = sharedPrefGetBoolean(R.string.pref_aecdump_key,
CallActivity.EXTRA_AECDUMP_ENABLED, R.string.pref_aecdump_default, useValuesFromIntent);
+ boolean saveInputAudioToFile =
+ sharedPrefGetBoolean(R.string.pref_enable_save_input_audio_to_file_key,
+ CallActivity.EXTRA_SAVE_INPUT_AUDIO_TO_FILE_ENABLED,
+ R.string.pref_enable_save_input_audio_to_file_default, useValuesFromIntent);
+
// Check OpenSL ES enabled flag.
boolean useOpenSLES = sharedPrefGetBoolean(R.string.pref_opensles_key,
CallActivity.EXTRA_OPENSLES_ENABLED, R.string.pref_opensles_default, useValuesFromIntent);
@@ -476,6 +480,7 @@
intent.putExtra(CallActivity.EXTRA_FLEXFEC_ENABLED, flexfecEnabled);
intent.putExtra(CallActivity.EXTRA_NOAUDIOPROCESSING_ENABLED, noAudioProcessing);
intent.putExtra(CallActivity.EXTRA_AECDUMP_ENABLED, aecDump);
+ intent.putExtra(CallActivity.EXTRA_SAVE_INPUT_AUDIO_TO_FILE_ENABLED, saveInputAudioToFile);
intent.putExtra(CallActivity.EXTRA_OPENSLES_ENABLED, useOpenSLES);
intent.putExtra(CallActivity.EXTRA_DISABLE_BUILT_IN_AEC, disableBuiltInAEC);
intent.putExtra(CallActivity.EXTRA_DISABLE_BUILT_IN_AGC, disableBuiltInAGC);
diff --git a/examples/androidapp/src/org/appspot/apprtc/PeerConnectionClient.java b/examples/androidapp/src/org/appspot/apprtc/PeerConnectionClient.java
index 961e2e8..bd95dfd 100644
--- a/examples/androidapp/src/org/appspot/apprtc/PeerConnectionClient.java
+++ b/examples/androidapp/src/org/appspot/apprtc/PeerConnectionClient.java
@@ -35,6 +35,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
+import org.appspot.apprtc.RecordedAudioToFileController;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.CameraVideoCapturer;
@@ -163,6 +164,9 @@
private boolean dataChannelEnabled;
// Enable RtcEventLog.
private RtcEventLog rtcEventLog;
+ // Implements the WebRtcAudioRecordSamplesReadyCallback interface and writes
+ // recorded audio samples to an output file.
+ private RecordedAudioToFileController saveRecordedAudioToFile = null;
/**
* Peer connection parameters.
@@ -204,6 +208,7 @@
public final String audioCodec;
public final boolean noAudioProcessing;
public final boolean aecDump;
+ public final boolean saveInputAudioToFile;
public final boolean useOpenSLES;
public final boolean disableBuiltInAEC;
public final boolean disableBuiltInAGC;
@@ -216,22 +221,10 @@
public PeerConnectionParameters(boolean videoCallEnabled, boolean loopback, boolean tracing,
int videoWidth, int videoHeight, int videoFps, int videoMaxBitrate, String videoCodec,
boolean videoCodecHwAcceleration, boolean videoFlexfecEnabled, int audioStartBitrate,
- String audioCodec, boolean noAudioProcessing, boolean aecDump, boolean useOpenSLES,
- boolean disableBuiltInAEC, boolean disableBuiltInAGC, boolean disableBuiltInNS,
- boolean enableLevelControl, boolean disableWebRtcAGCAndHPF, boolean enableRtcEventLog) {
- this(videoCallEnabled, loopback, tracing, videoWidth, videoHeight, videoFps, videoMaxBitrate,
- videoCodec, videoCodecHwAcceleration, videoFlexfecEnabled, audioStartBitrate, audioCodec,
- noAudioProcessing, aecDump, useOpenSLES, disableBuiltInAEC, disableBuiltInAGC,
- disableBuiltInNS, enableLevelControl, disableWebRtcAGCAndHPF, enableRtcEventLog, null);
- }
-
- public PeerConnectionParameters(boolean videoCallEnabled, boolean loopback, boolean tracing,
- int videoWidth, int videoHeight, int videoFps, int videoMaxBitrate, String videoCodec,
- boolean videoCodecHwAcceleration, boolean videoFlexfecEnabled, int audioStartBitrate,
- String audioCodec, boolean noAudioProcessing, boolean aecDump, boolean useOpenSLES,
- boolean disableBuiltInAEC, boolean disableBuiltInAGC, boolean disableBuiltInNS,
- boolean enableLevelControl, boolean disableWebRtcAGCAndHPF, boolean enableRtcEventLog,
- DataChannelParameters dataChannelParameters) {
+ String audioCodec, boolean noAudioProcessing, boolean aecDump, boolean saveInputAudioToFile,
+ boolean useOpenSLES, boolean disableBuiltInAEC, boolean disableBuiltInAGC,
+ boolean disableBuiltInNS, boolean enableLevelControl, boolean disableWebRtcAGCAndHPF,
+ boolean enableRtcEventLog, DataChannelParameters dataChannelParameters) {
this.videoCallEnabled = videoCallEnabled;
this.loopback = loopback;
this.tracing = tracing;
@@ -246,6 +239,7 @@
this.audioCodec = audioCodec;
this.noAudioProcessing = noAudioProcessing;
this.aecDump = aecDump;
+ this.saveInputAudioToFile = saveInputAudioToFile;
this.useOpenSLES = useOpenSLES;
this.disableBuiltInAEC = disableBuiltInAEC;
this.disableBuiltInAGC = disableBuiltInAGC;
@@ -508,6 +502,22 @@
}
});
+ // It is possible to save a copy in raw PCM format on a file by checking
+ // the "Save input audio to file" checkbox in the Settings UI. A callback
+ // interface is set when this flag is enabled. As a result, a copy of recorded
+ // audio samples are provided to this client directly from the native audio
+ // layer in Java.
+ if (peerConnectionParameters.saveInputAudioToFile) {
+ if (!peerConnectionParameters.useOpenSLES) {
+ Log.d(TAG, "Enable recording of microphone input audio to file");
+ saveRecordedAudioToFile = new RecordedAudioToFileController(executor);
+ } else {
+ // TODO(henrika): ensure that the UI reflects that if OpenSL ES is selected,
+ // then the "Save inut audio to file" option shall be grayed out.
+ Log.e(TAG, "Recording of input audio is not supported for OpenSL ES");
+ }
+ }
+
WebRtcAudioTrack.setErrorCallback(new WebRtcAudioTrack.ErrorCallback() {
@Override
public void onWebRtcAudioTrackInitError(String errorMessage) {
@@ -677,6 +687,11 @@
}
}
+ if (saveRecordedAudioToFile != null) {
+ if (saveRecordedAudioToFile.start()) {
+ Log.d(TAG, "Recording input audio to file is activated");
+ }
+ }
Log.d(TAG, "Peer connection created.");
}
@@ -740,6 +755,11 @@
videoSource.dispose();
videoSource = null;
}
+ if (saveRecordedAudioToFile != null) {
+ Log.d(TAG, "Closing audio file for recorded input audio.");
+ saveRecordedAudioToFile.stop();
+ saveRecordedAudioToFile = null;
+ }
localRender = null;
remoteRenders = null;
Log.d(TAG, "Closing peer connection factory.");
diff --git a/examples/androidapp/src/org/appspot/apprtc/RecordedAudioToFileController.java b/examples/androidapp/src/org/appspot/apprtc/RecordedAudioToFileController.java
new file mode 100644
index 0000000..6f1cc63
--- /dev/null
+++ b/examples/androidapp/src/org/appspot/apprtc/RecordedAudioToFileController.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.appspot.apprtc;
+
+import android.media.AudioFormat;
+import android.os.Environment;
+import android.util.Log;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+import org.webrtc.voiceengine.WebRtcAudioRecord;
+import org.webrtc.voiceengine.WebRtcAudioRecord.AudioSamples;
+import org.webrtc.voiceengine.WebRtcAudioRecord.WebRtcAudioRecordSamplesReadyCallback;
+
+/**
+ * Implements the WebRtcAudioRecordSamplesReadyCallback interface and writes
+ * recorded raw audio samples to an output file.
+ */
+public class RecordedAudioToFileController implements WebRtcAudioRecordSamplesReadyCallback {
+ private static final String TAG = "RecordedAudioToFile";
+ private static final long MAX_FILE_SIZE_IN_BYTES = 58348800L;
+
+ private final Object lock = new Object();
+ private final ExecutorService executor;
+ private OutputStream rawAudioFileOutputStream = null;
+ private long fileSizeInBytes = 0;
+
+ public RecordedAudioToFileController(ExecutorService executor) {
+ Log.d(TAG, "ctor");
+ this.executor = executor;
+ }
+
+ /**
+ * Should be called on the same executor thread as the one provided at
+ * construction.
+ */
+ public boolean start() {
+ Log.d(TAG, "start");
+ if (!isExternalStorageWritable()) {
+ Log.e(TAG, "Writing to external media is not possible");
+ return false;
+ }
+ // Register this class as receiver of recorded audio samples for storage.
+ WebRtcAudioRecord.setOnAudioSamplesReady(this);
+ return true;
+ }
+
+ /**
+ * Should be called on the same executor thread as the one provided at
+ * construction.
+ */
+ public void stop() {
+ Log.d(TAG, "stop");
+ // De-register this class as receiver of recorded audio samples for storage.
+ WebRtcAudioRecord.setOnAudioSamplesReady(null);
+ synchronized (lock) {
+ if (rawAudioFileOutputStream != null) {
+ try {
+ rawAudioFileOutputStream.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to close file with saved input audio: " + e);
+ }
+ rawAudioFileOutputStream = null;
+ }
+ fileSizeInBytes = 0;
+ }
+ }
+
+ // Checks if external storage is available for read and write.
+ private boolean isExternalStorageWritable() {
+ String state = Environment.getExternalStorageState();
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ return true;
+ }
+ return false;
+ }
+
+ // Utilizes audio parameters to create a file name which contains sufficient
+ // information so that the file can be played using an external file player.
+ // Example: /sdcard/recorded_audio_16bits_48000Hz_mono.pcm.
+ private void openRawAudioOutputFile(int sampleRate, int channelCount) {
+ final String fileName = Environment.getExternalStorageDirectory().getPath() + File.separator
+ + "recorded_audio_16bits_" + String.valueOf(sampleRate) + "Hz"
+ + ((channelCount == 1) ? "_mono" : "_stereo") + ".pcm";
+ final File outputFile = new File(fileName);
+ try {
+ rawAudioFileOutputStream = new FileOutputStream(outputFile);
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Failed to open audio output file: " + e.getMessage());
+ }
+ Log.d(TAG, "Opened file for recording: " + fileName);
+ }
+
+ // Called when new audio samples are ready.
+ @Override
+ public void onWebRtcAudioRecordSamplesReady(AudioSamples samples) {
+ // The native audio layer on Android should use 16-bit PCM format.
+ if (samples.getAudioFormat() != AudioFormat.ENCODING_PCM_16BIT) {
+ Log.e(TAG, "Invalid audio format");
+ return;
+ }
+ // Open a new file for the first callback only since it allows us to add
+ // audio parameters to the file name.
+ synchronized (lock) {
+ if (rawAudioFileOutputStream == null) {
+ openRawAudioOutputFile(samples.getSampleRate(), samples.getChannelCount());
+ fileSizeInBytes = 0;
+ }
+ }
+ // Append the recorded 16-bit audio samples to the open output file.
+ executor.execute(() -> {
+ if (rawAudioFileOutputStream != null) {
+ try {
+ // Set a limit on max file size. 58348800 bytes corresponds to
+ // approximately 10 minutes of recording in mono at 48kHz.
+ if (fileSizeInBytes < MAX_FILE_SIZE_IN_BYTES) {
+ // Writes samples.getData().length bytes to output stream.
+ rawAudioFileOutputStream.write(samples.getData());
+ fileSizeInBytes += samples.getData().length;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write audio to file: " + e.getMessage());
+ }
+ }
+ });
+ }
+}
diff --git a/examples/androidapp/src/org/appspot/apprtc/SettingsActivity.java b/examples/androidapp/src/org/appspot/apprtc/SettingsActivity.java
index 152bb7d..eea5961 100644
--- a/examples/androidapp/src/org/appspot/apprtc/SettingsActivity.java
+++ b/examples/androidapp/src/org/appspot/apprtc/SettingsActivity.java
@@ -42,6 +42,7 @@
private String keyPrefAudioCodec;
private String keyprefNoAudioProcessing;
private String keyprefAecDump;
+ private String keyprefEnableSaveInputAudioToFile;
private String keyprefOpenSLES;
private String keyprefDisableBuiltInAEC;
private String keyprefDisableBuiltInAGC;
@@ -84,6 +85,8 @@
keyPrefAudioCodec = getString(R.string.pref_audiocodec_key);
keyprefNoAudioProcessing = getString(R.string.pref_noaudioprocessing_key);
keyprefAecDump = getString(R.string.pref_aecdump_key);
+ keyprefEnableSaveInputAudioToFile =
+ getString(R.string.pref_enable_save_input_audio_to_file_key);
keyprefOpenSLES = getString(R.string.pref_opensles_key);
keyprefDisableBuiltInAEC = getString(R.string.pref_disable_built_in_aec_key);
keyprefDisableBuiltInAGC = getString(R.string.pref_disable_built_in_agc_key);
@@ -140,6 +143,7 @@
updateSummary(sharedPreferences, keyPrefAudioCodec);
updateSummaryB(sharedPreferences, keyprefNoAudioProcessing);
updateSummaryB(sharedPreferences, keyprefAecDump);
+ updateSummaryB(sharedPreferences, keyprefEnableSaveInputAudioToFile);
updateSummaryB(sharedPreferences, keyprefOpenSLES);
updateSummaryB(sharedPreferences, keyprefDisableBuiltInAEC);
updateSummaryB(sharedPreferences, keyprefDisableBuiltInAGC);
@@ -235,6 +239,7 @@
|| key.equals(keyprefFlexfec)
|| key.equals(keyprefNoAudioProcessing)
|| key.equals(keyprefAecDump)
+ || key.equals(keyprefEnableSaveInputAudioToFile)
|| key.equals(keyprefOpenSLES)
|| key.equals(keyprefDisableBuiltInAEC)
|| key.equals(keyprefDisableBuiltInAGC)
diff --git a/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java b/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
index c9c74b7..b1a3907 100644
--- a/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
+++ b/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
@@ -344,9 +344,10 @@
"OPUS", /* audioCodec */
false, /* noAudioProcessing */
false, /* aecDump */
+ false, /* saveInputAudioToFile */
false /* useOpenSLES */, false /* disableBuiltInAEC */, false /* disableBuiltInAGC */,
false /* disableBuiltInNS */, false /* enableLevelControl */, false /* disableWebRtcAGC */,
- false /* enableRtcEventLog */);
+ false /* enableRtcEventLog */, null /*dataChannelParameters */);
}
private VideoCapturer createCameraCapturer(boolean captureToTexture) {
@@ -380,9 +381,10 @@
"OPUS", /* audioCodec */
false, /* noAudioProcessing */
false, /* aecDump */
+ false, /* saveInputAudioToFile */
false /* useOpenSLES */, false /* disableBuiltInAEC */, false /* disableBuiltInAGC */,
false /* disableBuiltInNS */, false /* enableLevelControl */, false /* disableWebRtcAGC */,
- false /* enableRtcEventLog */);
+ false /* enableRtcEventLog */, null /*dataChannelParameters */);
}
@Before