• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 package org.appspot.apprtc;
12 
13 import android.media.AudioFormat;
14 import android.os.Environment;
15 import android.util.Log;
16 import androidx.annotation.Nullable;
17 import java.io.File;
18 import java.io.FileNotFoundException;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.util.concurrent.ExecutorService;
23 import org.webrtc.audio.JavaAudioDeviceModule;
24 import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback;
25 
26 /**
27  * Implements the AudioRecordSamplesReadyCallback interface and writes
28  * recorded raw audio samples to an output file.
29  */
30 public class RecordedAudioToFileController implements SamplesReadyCallback {
31   private static final String TAG = "RecordedAudioToFile";
32   private static final long MAX_FILE_SIZE_IN_BYTES = 58348800L;
33 
34   private final Object lock = new Object();
35   private final ExecutorService executor;
36   @Nullable private OutputStream rawAudioFileOutputStream;
37   private boolean isRunning;
38   private long fileSizeInBytes;
39 
RecordedAudioToFileController(ExecutorService executor)40   public RecordedAudioToFileController(ExecutorService executor) {
41     Log.d(TAG, "ctor");
42     this.executor = executor;
43   }
44 
45   /**
46    * Should be called on the same executor thread as the one provided at
47    * construction.
48    */
start()49   public boolean start() {
50     Log.d(TAG, "start");
51     if (!isExternalStorageWritable()) {
52       Log.e(TAG, "Writing to external media is not possible");
53       return false;
54     }
55     synchronized (lock) {
56       isRunning = true;
57     }
58     return true;
59   }
60 
61   /**
62    * Should be called on the same executor thread as the one provided at
63    * construction.
64    */
stop()65   public void stop() {
66     Log.d(TAG, "stop");
67     synchronized (lock) {
68       isRunning = false;
69       if (rawAudioFileOutputStream != null) {
70         try {
71           rawAudioFileOutputStream.close();
72         } catch (IOException e) {
73           Log.e(TAG, "Failed to close file with saved input audio: " + e);
74         }
75         rawAudioFileOutputStream = null;
76       }
77       fileSizeInBytes = 0;
78     }
79   }
80 
81   // Checks if external storage is available for read and write.
isExternalStorageWritable()82   private boolean isExternalStorageWritable() {
83     String state = Environment.getExternalStorageState();
84     if (Environment.MEDIA_MOUNTED.equals(state)) {
85       return true;
86     }
87     return false;
88   }
89 
90   // Utilizes audio parameters to create a file name which contains sufficient
91   // information so that the file can be played using an external file player.
92   // Example: /sdcard/recorded_audio_16bits_48000Hz_mono.pcm.
openRawAudioOutputFile(int sampleRate, int channelCount)93   private void openRawAudioOutputFile(int sampleRate, int channelCount) {
94     final String fileName = Environment.getExternalStorageDirectory().getPath() + File.separator
95         + "recorded_audio_16bits_" + String.valueOf(sampleRate) + "Hz"
96         + ((channelCount == 1) ? "_mono" : "_stereo") + ".pcm";
97     final File outputFile = new File(fileName);
98     try {
99       rawAudioFileOutputStream = new FileOutputStream(outputFile);
100     } catch (FileNotFoundException e) {
101       Log.e(TAG, "Failed to open audio output file: " + e.getMessage());
102     }
103     Log.d(TAG, "Opened file for recording: " + fileName);
104   }
105 
106   // Called when new audio samples are ready.
107   @Override
onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples samples)108   public void onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples samples) {
109     // The native audio layer on Android should use 16-bit PCM format.
110     if (samples.getAudioFormat() != AudioFormat.ENCODING_PCM_16BIT) {
111       Log.e(TAG, "Invalid audio format");
112       return;
113     }
114     synchronized (lock) {
115       // Abort early if stop() has been called.
116       if (!isRunning) {
117         return;
118       }
119       // Open a new file for the first callback only since it allows us to add audio parameters to
120       // the file name.
121       if (rawAudioFileOutputStream == null) {
122         openRawAudioOutputFile(samples.getSampleRate(), samples.getChannelCount());
123         fileSizeInBytes = 0;
124       }
125     }
126     // Append the recorded 16-bit audio samples to the open output file.
127     executor.execute(() -> {
128       if (rawAudioFileOutputStream != null) {
129         try {
130           // Set a limit on max file size. 58348800 bytes corresponds to
131           // approximately 10 minutes of recording in mono at 48kHz.
132           if (fileSizeInBytes < MAX_FILE_SIZE_IN_BYTES) {
133             // Writes samples.getData().length bytes to output stream.
134             rawAudioFileOutputStream.write(samples.getData());
135             fileSizeInBytes += samples.getData().length;
136           }
137         } catch (IOException e) {
138           Log.e(TAG, "Failed to write audio to file: " + e.getMessage());
139         }
140       }
141     });
142   }
143 }
144