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