1 /*
2  * Copyright (C) 2015 Google Inc. All Rights Reserved.
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.example.android.wearable.speaker;
18 
19 import android.content.Context;
20 import android.media.AudioFormat;
21 import android.media.AudioManager;
22 import android.media.AudioRecord;
23 import android.media.AudioTrack;
24 import android.media.MediaRecorder;
25 import android.os.AsyncTask;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.util.Log;
29 
30 import java.io.BufferedInputStream;
31 import java.io.BufferedOutputStream;
32 import java.io.File;
33 import java.io.FileInputStream;
34 import java.io.IOException;
35 
36 /**
37  * A helper class to provide methods to record audio input from the MIC to the internal storage
38  * and to playback the same recorded audio file.
39  */
40 public class SoundRecorder {
41 
42     private static final String TAG = "SoundRecorder";
43     private static final int RECORDING_RATE = 8000; // can go up to 44K, if needed
44     private static final int CHANNEL_IN = AudioFormat.CHANNEL_IN_MONO;
45     private static final int CHANNELS_OUT = AudioFormat.CHANNEL_OUT_MONO;
46     private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
47     private static int BUFFER_SIZE = AudioRecord
48             .getMinBufferSize(RECORDING_RATE, CHANNEL_IN, FORMAT);
49 
50     private final String mOutputFileName;
51     private final AudioManager mAudioManager;
52     private final Handler mHandler;
53     private final Context mContext;
54     private State mState = State.IDLE;
55 
56     private OnVoicePlaybackStateChangedListener mListener;
57     private AsyncTask<Void, Void, Void> mRecordingAsyncTask;
58     private AsyncTask<Void, Void, Void> mPlayingAsyncTask;
59 
60     enum State {
61         IDLE, RECORDING, PLAYING
62     }
63 
SoundRecorder(Context context, String outputFileName, OnVoicePlaybackStateChangedListener listener)64     public SoundRecorder(Context context, String outputFileName,
65             OnVoicePlaybackStateChangedListener listener) {
66         mOutputFileName = outputFileName;
67         mListener = listener;
68         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
69         mHandler = new Handler(Looper.getMainLooper());
70         mContext = context;
71     }
72 
73     /**
74      * Starts recording from the MIC.
75      */
startRecording()76     public void startRecording() {
77         if (mState != State.IDLE) {
78             Log.w(TAG, "Requesting to start recording while state was not IDLE");
79             return;
80         }
81 
82         mRecordingAsyncTask = new AsyncTask<Void, Void, Void>() {
83 
84             private AudioRecord mAudioRecord;
85 
86             @Override
87             protected void onPreExecute() {
88                 mState = State.RECORDING;
89             }
90 
91             @Override
92             protected Void doInBackground(Void... params) {
93                 mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
94                         RECORDING_RATE, CHANNEL_IN, FORMAT, BUFFER_SIZE * 3);
95                 BufferedOutputStream bufferedOutputStream = null;
96                 try {
97                     bufferedOutputStream = new BufferedOutputStream(
98                             mContext.openFileOutput(mOutputFileName, Context.MODE_PRIVATE));
99                     byte[] buffer = new byte[BUFFER_SIZE];
100                     mAudioRecord.startRecording();
101                     while (!isCancelled()) {
102                         int read = mAudioRecord.read(buffer, 0, buffer.length);
103                         bufferedOutputStream.write(buffer, 0, read);
104                     }
105                 } catch (IOException | NullPointerException | IndexOutOfBoundsException e) {
106                     Log.e(TAG, "Failed to record data: " + e);
107                 } finally {
108                     if (bufferedOutputStream != null) {
109                         try {
110                             bufferedOutputStream.close();
111                         } catch (IOException e) {
112                             // ignore
113                         }
114                     }
115                     mAudioRecord.release();
116                     mAudioRecord = null;
117                 }
118                 return null;
119             }
120 
121             @Override
122             protected void onPostExecute(Void aVoid) {
123                 mState = State.IDLE;
124                 mRecordingAsyncTask = null;
125             }
126 
127             @Override
128             protected void onCancelled() {
129                 if (mState == State.RECORDING) {
130                     Log.d(TAG, "Stopping the recording ...");
131                     mState = State.IDLE;
132                 } else {
133                     Log.w(TAG, "Requesting to stop recording while state was not RECORDING");
134                 }
135                 mRecordingAsyncTask = null;
136             }
137         };
138 
139         mRecordingAsyncTask.execute();
140     }
141 
stopRecording()142     public void stopRecording() {
143         if (mRecordingAsyncTask != null) {
144             mRecordingAsyncTask.cancel(true);
145         }
146     }
147 
stopPlaying()148     public void stopPlaying() {
149         if (mPlayingAsyncTask != null) {
150             mPlayingAsyncTask.cancel(true);
151         }
152     }
153 
154     /**
155      * Starts playback of the recorded audio file.
156      */
startPlay()157     public void startPlay() {
158         if (mState != State.IDLE) {
159             Log.w(TAG, "Requesting to play while state was not IDLE");
160             return;
161         }
162 
163         if (!new File(mContext.getFilesDir(), mOutputFileName).exists()) {
164             // there is no recording to play
165             if (mListener != null) {
166                 mHandler.post(new Runnable() {
167                     @Override
168                     public void run() {
169                         mListener.onPlaybackStopped();
170                     }
171                 });
172             }
173             return;
174         }
175         final int intSize = AudioTrack.getMinBufferSize(RECORDING_RATE, CHANNELS_OUT, FORMAT);
176 
177         mPlayingAsyncTask = new AsyncTask<Void, Void, Void>() {
178 
179             private AudioTrack mAudioTrack;
180 
181             @Override
182             protected void onPreExecute() {
183                 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
184                         mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0 /* flags */);
185                 mState = State.PLAYING;
186             }
187 
188             @Override
189             protected Void doInBackground(Void... params) {
190                 try {
191                     mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, RECORDING_RATE,
192                             CHANNELS_OUT, FORMAT, intSize, AudioTrack.MODE_STREAM);
193                     byte[] buffer = new byte[intSize * 2];
194                     FileInputStream in = null;
195                     BufferedInputStream bis = null;
196                     mAudioTrack.setVolume(AudioTrack.getMaxVolume());
197                     mAudioTrack.play();
198                     try {
199                         in = mContext.openFileInput(mOutputFileName);
200                         bis = new BufferedInputStream(in);
201                         int read;
202                         while (!isCancelled() && (read = bis.read(buffer, 0, buffer.length)) > 0) {
203                             mAudioTrack.write(buffer, 0, read);
204                         }
205                     } catch (IOException e) {
206                         Log.e(TAG, "Failed to read the sound file into a byte array", e);
207                     } finally {
208                         try {
209                             if (in != null) {
210                                 in.close();
211                             }
212                             if (bis != null) {
213                                 bis.close();
214                             }
215                         } catch (IOException e) { /* ignore */}
216 
217                         mAudioTrack.release();
218                     }
219                 } catch (IllegalStateException e) {
220                     Log.e(TAG, "Failed to start playback", e);
221                 }
222                 return null;
223             }
224 
225             @Override
226             protected void onPostExecute(Void aVoid) {
227                 cleanup();
228             }
229 
230             @Override
231             protected void onCancelled() {
232                 cleanup();
233             }
234 
235             private void cleanup() {
236                 if (mListener != null) {
237                     mListener.onPlaybackStopped();
238                 }
239                 mState = State.IDLE;
240                 mPlayingAsyncTask = null;
241             }
242         };
243 
244         mPlayingAsyncTask.execute();
245     }
246 
247     public interface OnVoicePlaybackStateChangedListener {
248 
249         /**
250          * Called when the playback of the audio file ends. This should be called on the UI thread.
251          */
onPlaybackStopped()252         void onPlaybackStopped();
253     }
254 
255     /**
256      * Cleans up some resources related to {@link AudioTrack} and {@link AudioRecord}
257      */
cleanup()258     public void cleanup() {
259         Log.d(TAG, "cleanup() is called");
260         stopPlaying();
261         stopRecording();
262     }
263 }
264