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