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