1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.settings.system.development.audio; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.media.AudioFormat; 22 import android.media.AudioTrack; 23 import android.os.Handler; 24 import android.util.Log; 25 26 import java.nio.ShortBuffer; 27 28 /** Manages audio recording, audio metrics, and audio playback for debugging purposes. */ 29 public class AudioDebug { 30 31 private static final String TAG = "AudioDebug"; 32 33 public static final int CHANNELS = 1; 34 public static final int SAMPLE_RATE = 16000; 35 public static final int BITRATE = 16; 36 public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; 37 38 private final Context mContext; 39 40 private final AudioRecordedCallback mAudioRecordedCallback; 41 42 private final AudioMetrics mMetrics; 43 44 @Nullable 45 private AudioReader mAudioReader; 46 47 @Nullable 48 private ShortBuffer mAudioBuffer; 49 50 @Nullable 51 private AudioTrack mAudioTrack; 52 53 /** Interface for receiving a notification when audio recording finishes. */ 54 public interface AudioRecordedCallback { 55 /** Callback for receiving a notification when audio recording finishes. */ onAudioRecorded(boolean successful)56 void onAudioRecorded(boolean successful); 57 } 58 59 /** 60 * @param context The parent context 61 * @param audioRecordedCallback Callback for notification on audio recording completion 62 * @param metricsCallback Callback for metrics updates 63 */ AudioDebug(Context context, AudioRecordedCallback audioRecordedCallback, AudioMetrics.UpdateMetricsCallback metricsCallback)64 public AudioDebug(Context context, AudioRecordedCallback audioRecordedCallback, 65 AudioMetrics.UpdateMetricsCallback metricsCallback) { 66 this.mContext = context; 67 this.mAudioRecordedCallback = audioRecordedCallback; 68 69 mMetrics = new AudioMetrics(metricsCallback); 70 } 71 72 /** Starts recording audio. */ startRecording()73 public void startRecording() throws AudioReaderException { 74 if (mAudioReader != null) { 75 mAudioReader.stop(); 76 } 77 78 mMetrics.start(); 79 80 mAudioReader = new AudioReader(mMetrics); 81 mAudioReader.addListener((ShortBuffer buffer) -> onAudioRecorded(buffer)); 82 83 Thread audioReaderThread = new Thread(mAudioReader); 84 audioReaderThread.setPriority(10); 85 audioReaderThread.start(); 86 } 87 88 /** 89 * Stores a buffer containing recorded audio in an AudioTrack. Overwrites any previously 90 * recorded audio. 91 * 92 * @param audioBuffer The buffer containing the recorded audio. 93 */ onAudioRecorded(ShortBuffer audioBuffer)94 private void onAudioRecorded(ShortBuffer audioBuffer) { 95 if (audioBuffer.position() == 0) { 96 Log.e(TAG, "Empty buffer recorded"); 97 return; 98 } 99 100 this.mAudioBuffer = audioBuffer; 101 102 int numShorts = audioBuffer.position(); 103 int numBytes = numShorts * 2; 104 105 Handler mainHandler = new Handler(mContext.getMainLooper()); 106 107 try { 108 mAudioTrack = 109 new AudioTrack.Builder() 110 .setAudioFormat( 111 new AudioFormat.Builder() 112 .setSampleRate(SAMPLE_RATE) 113 .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) 114 .setEncoding(ENCODING) 115 .build() 116 ) 117 .setTransferMode(AudioTrack.MODE_STATIC) 118 .setBufferSizeInBytes(numBytes) 119 .build(); 120 } catch (UnsupportedOperationException | IllegalArgumentException e) { 121 Log.e(TAG, "Failed to create AudioTrack", e); 122 mainHandler.post(() -> mAudioRecordedCallback.onAudioRecorded(false)); 123 return; 124 } 125 126 Log.i(TAG, String.format("AudioTrack state: %d", mAudioTrack.getState())); 127 128 int writeStatus = mAudioTrack.write(audioBuffer.array(), 0, numShorts, 129 AudioTrack.WRITE_BLOCKING); 130 if (writeStatus > 0) { 131 Log.i(TAG, String.format("Wrote %d bytes to an AudioTrack", numBytes)); 132 mainHandler.post(() -> mAudioRecordedCallback.onAudioRecorded(true)); 133 } else if (writeStatus == 0) { 134 Log.e(TAG, "Received empty audio buffer"); 135 } else { 136 Log.e(TAG, String.format("Error calling AudioTrack.write(): %d", writeStatus)); 137 mainHandler.post(() -> mAudioRecordedCallback.onAudioRecorded(false)); 138 } 139 } 140 141 142 /** Stops recording audio. */ stopRecording()143 public void stopRecording() { 144 if (mAudioReader != null) { 145 mAudioReader.stop(); 146 mAudioReader = null; 147 } 148 } 149 150 /** Stops recording audio, and discards the recorded audio. */ cancelRecording()151 public void cancelRecording() { 152 if (mAudioReader != null) { 153 mAudioReader.cancel(); 154 mAudioReader = null; 155 } 156 } 157 158 159 /** Plays the recorded audio. */ playAudio()160 public void playAudio() { 161 if (mAudioTrack == null) { 162 Log.e(TAG, "No audio track recorded"); 163 } else { 164 mAudioTrack.stop(); 165 mAudioTrack.reloadStaticData(); 166 mAudioTrack.setPlaybackHeadPosition(0); 167 mAudioTrack.setVolume(1.0f); 168 mAudioTrack.play(); 169 } 170 } 171 172 /** Writes the recorded audio to a WAV file. */ writeAudioToFile()173 public void writeAudioToFile() { 174 WavWriter.writeToFile(mContext.getExternalFilesDir(null), mAudioBuffer); 175 } 176 } 177