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