1 /*
2  * Copyright (C) 2014 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.test.soundtrigger;
18 
19 import android.Manifest;
20 import android.app.Service;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.pm.PackageManager;
26 import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent;
27 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
28 import android.media.AudioAttributes;
29 import android.media.AudioFormat;
30 import android.media.AudioManager;
31 import android.media.AudioRecord;
32 import android.media.AudioTrack;
33 import android.media.MediaPlayer;
34 import android.media.soundtrigger.SoundTriggerDetector;
35 import android.media.soundtrigger.SoundTriggerManager;
36 import android.net.Uri;
37 import android.os.Binder;
38 import android.os.IBinder;
39 import android.util.Log;
40 
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.util.HashMap;
46 import java.util.Map;
47 import java.util.Properties;
48 import java.util.Random;
49 import java.util.UUID;
50 
51 
52 public class SoundTriggerTestService extends Service {
53     private static final String TAG = "SoundTriggerTestSrv";
54     private static final String INTENT_ACTION = "com.android.intent.action.MANAGE_SOUND_TRIGGER";
55 
56     // Binder given to clients.
57     private final IBinder mBinder;
58     private final Map<UUID, ModelInfo> mModelInfoMap;
59     private SoundTriggerUtil mSoundTriggerUtil;
60     private Random mRandom;
61     private UserActivity mUserActivity;
62 
63     private static int captureCount;
64 
65     public interface UserActivity {
addModel(UUID modelUuid, String state)66         void addModel(UUID modelUuid, String state);
setModelState(UUID modelUuid, String state)67         void setModelState(UUID modelUuid, String state);
showMessage(String msg, boolean showToast)68         void showMessage(String msg, boolean showToast);
handleDetection(UUID modelUuid)69         void handleDetection(UUID modelUuid);
70     }
71 
SoundTriggerTestService()72     public SoundTriggerTestService() {
73         super();
74         mRandom = new Random();
75         mModelInfoMap = new HashMap();
76         mBinder = new SoundTriggerTestBinder();
77     }
78 
79     @Override
onStartCommand(Intent intent, int flags, int startId)80     public synchronized int onStartCommand(Intent intent, int flags, int startId) {
81         if (mModelInfoMap.isEmpty()) {
82             mSoundTriggerUtil = new SoundTriggerUtil(this);
83             loadModelsInDataDir();
84         }
85 
86         // If we get killed, after returning from here, restart
87         return START_STICKY;
88     }
89 
90     @Override
onCreate()91     public void onCreate() {
92         super.onCreate();
93         IntentFilter filter = new IntentFilter();
94         filter.addAction(INTENT_ACTION);
95         registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
96 
97         // Make sure the data directory exists, and we're the owner of it.
98         try {
99             getFilesDir().mkdir();
100         } catch (Exception e) {
101             // Don't care - we either made it, or it already exists.
102         }
103     }
104 
105     @Override
onDestroy()106     public void onDestroy() {
107         super.onDestroy();
108         stopAllRecognitionsAndUnload();
109         unregisterReceiver(mBroadcastReceiver);
110     }
111 
112     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
113         @Override
114         public void onReceive(Context context, Intent intent) {
115             if (intent != null && INTENT_ACTION.equals(intent.getAction())) {
116                 String command = intent.getStringExtra("command");
117                 if (command == null) {
118                     Log.e(TAG, "No 'command' specified in " + INTENT_ACTION);
119                 } else {
120                     try {
121                         if (command.equals("load")) {
122                             loadModel(getModelUuidFromIntent(intent));
123                         } else if (command.equals("unload")) {
124                             unloadModel(getModelUuidFromIntent(intent));
125                         } else if (command.equals("start")) {
126                             startRecognition(getModelUuidFromIntent(intent));
127                         } else if (command.equals("stop")) {
128                             stopRecognition(getModelUuidFromIntent(intent));
129                         } else if (command.equals("play_trigger")) {
130                             playTriggerAudio(getModelUuidFromIntent(intent));
131                         } else if (command.equals("play_captured")) {
132                             playCapturedAudio(getModelUuidFromIntent(intent));
133                         } else if (command.equals("set_capture")) {
134                             setCaptureAudio(getModelUuidFromIntent(intent),
135                                     intent.getBooleanExtra("enabled", true));
136                         } else if (command.equals("set_capture_timeout")) {
137                             setCaptureAudioTimeout(getModelUuidFromIntent(intent),
138                                     intent.getIntExtra("timeout", 5000));
139                         } else if (command.equals("get_model_state")) {
140                             getModelState(getModelUuidFromIntent(intent));
141                         } else {
142                             Log.e(TAG, "Unknown command '" + command + "'");
143                         }
144                     } catch (Exception e) {
145                         Log.e(TAG, "Failed to process " + command, e);
146                     }
147                 }
148             }
149         }
150     };
151 
getModelUuidFromIntent(Intent intent)152     private UUID getModelUuidFromIntent(Intent intent) {
153         // First, see if the specified the UUID straight up.
154         String value = intent.getStringExtra("modelUuid");
155         if (value != null) {
156             return UUID.fromString(value);
157         }
158 
159         // If they specified a name, use that to iterate through the map of models and find it.
160         value = intent.getStringExtra("name");
161         if (value != null) {
162             for (ModelInfo modelInfo : mModelInfoMap.values()) {
163                 if (value.equals(modelInfo.name)) {
164                     return modelInfo.modelUuid;
165                 }
166             }
167             Log.e(TAG, "Failed to find a matching model with name '" + value + "'");
168         }
169 
170         // We couldn't figure out what they were asking for.
171         throw new RuntimeException("Failed to get model from intent - specify either " +
172                 "'modelUuid' or 'name'");
173     }
174 
175     /**
176      * Will be called when the service is killed (through swipe aways, not if we're force killed).
177      */
178     @Override
onTaskRemoved(Intent rootIntent)179     public void onTaskRemoved(Intent rootIntent) {
180         super.onTaskRemoved(rootIntent);
181         stopAllRecognitionsAndUnload();
182         stopSelf();
183     }
184 
185     @Override
onBind(Intent intent)186     public synchronized IBinder onBind(Intent intent) {
187         return mBinder;
188     }
189 
190     public class SoundTriggerTestBinder extends Binder {
getService()191         SoundTriggerTestService getService() {
192             // Return instance of our parent so clients can call public methods.
193             return SoundTriggerTestService.this;
194         }
195     }
196 
setUserActivity(UserActivity activity)197     public synchronized void setUserActivity(UserActivity activity) {
198         mUserActivity = activity;
199         if (mUserActivity != null) {
200             for (Map.Entry<UUID, ModelInfo> entry : mModelInfoMap.entrySet()) {
201                 mUserActivity.addModel(entry.getKey(), entry.getValue().name);
202                 mUserActivity.setModelState(entry.getKey(), entry.getValue().state);
203             }
204         }
205     }
206 
stopAllRecognitionsAndUnload()207     private synchronized void stopAllRecognitionsAndUnload() {
208         Log.e(TAG, "Stop all recognitions");
209         for (ModelInfo modelInfo : mModelInfoMap.values()) {
210             Log.e(TAG, "Loop " + modelInfo.modelUuid);
211             if (modelInfo.detector != null) {
212                 Log.i(TAG, "Stopping recognition for " + modelInfo.name);
213                 try {
214                     modelInfo.detector.stopRecognition();
215                 } catch (Exception e) {
216                     Log.e(TAG, "Failed to stop recognition", e);
217                 }
218                 try {
219                     mSoundTriggerUtil.deleteSoundModel(modelInfo.modelUuid);
220                     modelInfo.detector = null;
221                 } catch (Exception e) {
222                     Log.e(TAG, "Failed to unload sound model", e);
223                 }
224             }
225         }
226     }
227 
228     // Helper struct for holding information about a model.
229     public static class ModelInfo {
230         public String name;
231         public String state;
232         public UUID modelUuid;
233         public UUID vendorUuid;
234         public MediaPlayer triggerAudioPlayer;
235         public SoundTriggerDetector detector;
236         public byte modelData[];
237         public boolean captureAudio;
238         public int captureAudioMs;
239         public AudioTrack captureAudioTrack;
240     }
241 
createNewSoundModel(ModelInfo modelInfo)242     private SoundTriggerManager.Model createNewSoundModel(ModelInfo modelInfo) {
243         return SoundTriggerManager.Model.create(modelInfo.modelUuid, modelInfo.vendorUuid,
244                 modelInfo.modelData);
245     }
246 
loadModel(UUID modelUuid)247     public synchronized void loadModel(UUID modelUuid) {
248         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
249         if (modelInfo == null) {
250             postError("Could not find model for: " + modelUuid.toString());
251             return;
252         }
253 
254         postMessage("Loading model: " + modelInfo.name);
255 
256         SoundTriggerManager.Model soundModel = createNewSoundModel(modelInfo);
257 
258         boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(soundModel);
259         if (status) {
260             postToast("Successfully loaded " + modelInfo.name + ", UUID="
261                     + soundModel.getModelUuid());
262             setModelState(modelInfo, "Loaded");
263         } else {
264             postErrorToast("Failed to load " + modelInfo.name + ", UUID="
265                     + soundModel.getModelUuid() + "!");
266             setModelState(modelInfo, "Failed to load");
267         }
268     }
269 
unloadModel(UUID modelUuid)270     public synchronized void unloadModel(UUID modelUuid) {
271         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
272         if (modelInfo == null) {
273             postError("Could not find model for: " + modelUuid.toString());
274             return;
275         }
276 
277         postMessage("Unloading model: " + modelInfo.name);
278 
279         SoundTriggerManager.Model soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
280         if (soundModel == null) {
281             postErrorToast("Sound model not found for " + modelInfo.name + "!");
282             return;
283         }
284         modelInfo.detector = null;
285         boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid);
286         if (status) {
287             postToast("Successfully unloaded " + modelInfo.name + ", UUID="
288                     + soundModel.getModelUuid());
289             setModelState(modelInfo, "Unloaded");
290         } else {
291             postErrorToast("Failed to unload " +
292                     modelInfo.name + ", UUID=" + soundModel.getModelUuid() + "!");
293             setModelState(modelInfo, "Failed to unload");
294         }
295     }
296 
reloadModel(UUID modelUuid)297     public synchronized void reloadModel(UUID modelUuid) {
298         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
299         if (modelInfo == null) {
300             postError("Could not find model for: " + modelUuid.toString());
301             return;
302         }
303         postMessage("Reloading model: " + modelInfo.name);
304         SoundTriggerManager.Model soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
305         if (soundModel == null) {
306             postErrorToast("Sound model not found for " + modelInfo.name + "!");
307             return;
308         }
309         SoundTriggerManager.Model updated = createNewSoundModel(modelInfo);
310         boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated);
311         if (status) {
312             postToast("Successfully reloaded " + modelInfo.name + ", UUID="
313                     + modelInfo.modelUuid);
314             setModelState(modelInfo, "Reloaded");
315         } else {
316             postErrorToast("Failed to reload "
317                     + modelInfo.name + ", UUID=" + modelInfo.modelUuid + "!");
318             setModelState(modelInfo, "Failed to reload");
319         }
320     }
321 
startRecognition(UUID modelUuid)322     public synchronized void startRecognition(UUID modelUuid) {
323         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
324         if (modelInfo == null) {
325             postError("Could not find model for: " + modelUuid.toString());
326             return;
327         }
328 
329         if (modelInfo.detector == null) {
330             postMessage("Creating SoundTriggerDetector for " + modelInfo.name);
331             modelInfo.detector = mSoundTriggerUtil.createSoundTriggerDetector(
332                     modelUuid, new DetectorCallback(modelInfo));
333         }
334 
335         postMessage("Starting recognition for " + modelInfo.name + ", UUID="
336                 + modelInfo.modelUuid);
337         if (modelInfo.detector.startRecognition(modelInfo.captureAudio ?
338                 SoundTriggerDetector.RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO :
339                 SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) {
340             setModelState(modelInfo, "Started");
341         } else {
342             postErrorToast("Fast failure attempting to start recognition for " +
343                     modelInfo.name + ", UUID=" + modelInfo.modelUuid);
344             setModelState(modelInfo, "Failed to start");
345         }
346     }
347 
stopRecognition(UUID modelUuid)348     public synchronized void stopRecognition(UUID modelUuid) {
349         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
350         if (modelInfo == null) {
351             postError("Could not find model for: " + modelUuid.toString());
352             return;
353         }
354 
355         if (modelInfo.detector == null) {
356             postErrorToast("Stop called on null detector for " +
357                     modelInfo.name + ", UUID=" + modelInfo.modelUuid);
358             return;
359         }
360         postMessage("Triggering stop recognition for " +
361                 modelInfo.name + ", UUID=" + modelInfo.modelUuid);
362         if (modelInfo.detector.stopRecognition()) {
363             setModelState(modelInfo, "Stopped");
364         } else {
365             postErrorToast("Fast failure attempting to stop recognition for " +
366                     modelInfo.name + ", UUID=" + modelInfo.modelUuid);
367             setModelState(modelInfo, "Failed to stop");
368         }
369     }
370 
playTriggerAudio(UUID modelUuid)371     public synchronized void playTriggerAudio(UUID modelUuid) {
372         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
373         if (modelInfo == null) {
374             postError("Could not find model for: " + modelUuid.toString());
375             return;
376         }
377         if (modelInfo.triggerAudioPlayer != null) {
378             postMessage("Playing trigger audio for " + modelInfo.name);
379             modelInfo.triggerAudioPlayer.start();
380         } else {
381             postMessage("No trigger audio for " + modelInfo.name);
382         }
383     }
384 
playCapturedAudio(UUID modelUuid)385     public synchronized void playCapturedAudio(UUID modelUuid) {
386         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
387         if (modelInfo == null) {
388             postError("Could not find model for: " + modelUuid.toString());
389             return;
390         }
391         if (modelInfo.captureAudioTrack != null) {
392             postMessage("Playing captured audio for " + modelInfo.name);
393             modelInfo.captureAudioTrack.stop();
394             modelInfo.captureAudioTrack.reloadStaticData();
395             modelInfo.captureAudioTrack.play();
396         } else {
397             postMessage("No captured audio for " + modelInfo.name);
398         }
399     }
400 
setCaptureAudioTimeout(UUID modelUuid, int captureTimeoutMs)401     public synchronized void setCaptureAudioTimeout(UUID modelUuid, int captureTimeoutMs) {
402         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
403         if (modelInfo == null) {
404             postError("Could not find model for: " + modelUuid.toString());
405             return;
406         }
407         modelInfo.captureAudioMs = captureTimeoutMs;
408         Log.i(TAG, "Set " + modelInfo.name + " capture audio timeout to " +
409                 captureTimeoutMs + "ms");
410     }
411 
setCaptureAudio(UUID modelUuid, boolean captureAudio)412     public synchronized void setCaptureAudio(UUID modelUuid, boolean captureAudio) {
413         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
414         if (modelInfo == null) {
415             postError("Could not find model for: " + modelUuid.toString());
416             return;
417         }
418         modelInfo.captureAudio = captureAudio;
419         Log.i(TAG, "Set " + modelInfo.name + " capture audio to " + captureAudio);
420     }
421 
hasMicrophonePermission()422     public synchronized boolean hasMicrophonePermission() {
423         return getBaseContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO)
424                 == PackageManager.PERMISSION_GRANTED;
425     }
426 
modelHasTriggerAudio(UUID modelUuid)427     public synchronized boolean modelHasTriggerAudio(UUID modelUuid) {
428         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
429         return modelInfo != null && modelInfo.triggerAudioPlayer != null;
430     }
431 
modelWillCaptureTriggerAudio(UUID modelUuid)432     public synchronized boolean modelWillCaptureTriggerAudio(UUID modelUuid) {
433         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
434         return modelInfo != null && modelInfo.captureAudio;
435     }
436 
modelHasCapturedAudio(UUID modelUuid)437     public synchronized boolean modelHasCapturedAudio(UUID modelUuid) {
438         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
439         return modelInfo != null && modelInfo.captureAudioTrack != null;
440     }
441 
getModelState(UUID modelUuid)442     public synchronized void getModelState(UUID modelUuid) {
443         ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
444         if (modelInfo == null) {
445             postError("Could not find model for: " + modelUuid.toString());
446             return;
447         }
448         int status = mSoundTriggerUtil.getModelState(modelUuid);
449         postMessage("GetModelState for: " + modelInfo.name + " returns: "
450             + status);
451     }
452 
loadModelsInDataDir()453     private void loadModelsInDataDir() {
454         // Load all the models in the data dir.
455         boolean loadedModel = false;
456         for (File file : getFilesDir().listFiles()) {
457             // Find meta-data in .properties files, ignore everything else.
458             if (!file.getName().endsWith(".properties")) {
459                 continue;
460             }
461 
462             try (FileInputStream in = new FileInputStream(file)) {
463                 Properties properties = new Properties();
464                 properties.load(in);
465                 createModelInfo(properties);
466                 loadedModel = true;
467             } catch (Exception e) {
468                 Log.e(TAG, "Failed to load properties file " + file.getName());
469             }
470         }
471 
472         // Create a few placeholder models if we didn't load anything.
473         if (!loadedModel) {
474             Properties dummyModelProperties = new Properties();
475             for (String name : new String[]{"1", "2", "3"}) {
476                 dummyModelProperties.setProperty("name", "Model " + name);
477                 createModelInfo(dummyModelProperties);
478             }
479         }
480     }
481 
482     /** Parses a Properties collection to generate a sound model.
483      *
484      * Missing keys are filled in with default/random values.
485      * @param properties Has the required 'name' property, but the remaining 'modelUuid',
486      *                   'vendorUuid', 'triggerAudio', and 'dataFile' optional properties.
487      *
488      */
createModelInfo(Properties properties)489     private synchronized void createModelInfo(Properties properties) {
490         try {
491             ModelInfo modelInfo = new ModelInfo();
492 
493             if (!properties.containsKey("name")) {
494                 throw new RuntimeException("must have a 'name' property");
495             }
496             modelInfo.name = properties.getProperty("name");
497 
498             if (properties.containsKey("modelUuid")) {
499                 modelInfo.modelUuid = UUID.fromString(properties.getProperty("modelUuid"));
500             } else {
501                 modelInfo.modelUuid = UUID.randomUUID();
502             }
503 
504             if (properties.containsKey("vendorUuid")) {
505                 modelInfo.vendorUuid = UUID.fromString(properties.getProperty("vendorUuid"));
506             } else {
507                 modelInfo.vendorUuid = UUID.randomUUID();
508             }
509 
510             if (properties.containsKey("triggerAudio")) {
511                 modelInfo.triggerAudioPlayer = MediaPlayer.create(this, Uri.parse(
512                         getFilesDir().getPath() + "/" + properties.getProperty("triggerAudio")));
513                 if (modelInfo.triggerAudioPlayer.getDuration() == 0) {
514                     modelInfo.triggerAudioPlayer.release();
515                     modelInfo.triggerAudioPlayer = null;
516                 }
517             }
518 
519             if (properties.containsKey("dataFile")) {
520                 File modelDataFile = new File(
521                         getFilesDir().getPath() + "/"
522                                 + properties.getProperty("dataFile"));
523                 modelInfo.modelData = new byte[(int) modelDataFile.length()];
524                 FileInputStream input = new FileInputStream(modelDataFile);
525                 input.read(modelInfo.modelData, 0, modelInfo.modelData.length);
526             } else {
527                 modelInfo.modelData = new byte[1024];
528                 mRandom.nextBytes(modelInfo.modelData);
529             }
530 
531             modelInfo.captureAudioMs = Integer.parseInt((String) properties.getOrDefault(
532                     "captureAudioDurationMs", "5000"));
533 
534             // TODO: Add property support for keyphrase models when they're exposed by the
535             // service.
536 
537             // Update our maps containing the button -> id and id -> modelInfo.
538             mModelInfoMap.put(modelInfo.modelUuid, modelInfo);
539             if (mUserActivity != null) {
540                 mUserActivity.addModel(modelInfo.modelUuid, modelInfo.name);
541                 mUserActivity.setModelState(modelInfo.modelUuid, modelInfo.state);
542             }
543         } catch (IOException e) {
544             Log.e(TAG, "Error parsing properties for " + properties.getProperty("name"), e);
545         }
546     }
547 
548 
549     private class CaptureAudioRecorder implements Runnable {
550         private final ModelInfo mModelInfo;
551 
552         // EventPayload and RecognitionEvent are equivalant.  Only one will be non-null.
553         private final SoundTriggerDetector.EventPayload mEvent;
554         private final RecognitionEvent mRecognitionEvent;
555 
CaptureAudioRecorder(ModelInfo modelInfo, SoundTriggerDetector.EventPayload event)556         public CaptureAudioRecorder(ModelInfo modelInfo, SoundTriggerDetector.EventPayload event) {
557             mModelInfo = modelInfo;
558             mEvent = event;
559             mRecognitionEvent = null;
560         }
561 
CaptureAudioRecorder(ModelInfo modelInfo, RecognitionEvent event)562         public CaptureAudioRecorder(ModelInfo modelInfo, RecognitionEvent event) {
563             mModelInfo = modelInfo;
564             mEvent = null;
565             mRecognitionEvent = event;
566         }
567 
568         @Override
run()569         public void run() {
570             AudioFormat format = getAudioFormat();
571             if (format == null) {
572                 postErrorToast("No audio format in recognition event.");
573                 return;
574             }
575 
576             AudioRecord audioRecord = null;
577             AudioTrack playbackTrack = null;
578             try {
579                 // Inform the audio flinger that we really do want the stream from the soundtrigger.
580                 AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
581                 attributesBuilder.setInternalCapturePreset(1999);
582                 AudioAttributes attributes = attributesBuilder.build();
583 
584                 // Make sure we understand this kind of playback so we know how many bytes to read.
585                 String encoding;
586                 int bytesPerSample;
587                 switch (format.getEncoding()) {
588                     case AudioFormat.ENCODING_PCM_8BIT:
589                         encoding = "8bit";
590                         bytesPerSample = 1;
591                         break;
592                     case AudioFormat.ENCODING_PCM_16BIT:
593                         encoding = "16bit";
594                         bytesPerSample = 2;
595                         break;
596                     case AudioFormat.ENCODING_PCM_FLOAT:
597                         encoding = "float";
598                         bytesPerSample = 4;
599                         break;
600                     default:
601                         throw new RuntimeException("Unhandled audio format in event");
602                 }
603 
604                 int bytesRequired = format.getSampleRate() * format.getChannelCount() *
605                         bytesPerSample * mModelInfo.captureAudioMs / 1000;
606                 int minBufferSize = AudioRecord.getMinBufferSize(
607                         format.getSampleRate(), format.getChannelMask(), format.getEncoding());
608                 if (minBufferSize > bytesRequired) {
609                     bytesRequired = minBufferSize;
610                 }
611 
612                 // Make an AudioTrack so we can play the data back out after it's finished
613                 // recording.
614                 try {
615                     int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
616                     if (format.getChannelCount() == 2) {
617                         channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
618                     } else if (format.getChannelCount() >= 3) {
619                         throw new RuntimeException(
620                                 "Too many channels in captured audio for playback");
621                     }
622 
623                     playbackTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
624                             format.getSampleRate(), channelConfig, format.getEncoding(),
625                             bytesRequired, AudioTrack.MODE_STATIC);
626                 } catch (Exception e) {
627                     Log.e(TAG, "Exception creating playback track", e);
628                     postErrorToast("Failed to create playback track: " + e.getMessage());
629                 }
630 
631                 audioRecord = new AudioRecord(attributes, format, bytesRequired,
632                         getCaptureSession());
633 
634                 byte[] buffer = new byte[bytesRequired];
635 
636                 // Create a file so we can save the output data there for analysis later.
637                 FileOutputStream fos  = null;
638                 try {
639                     File file = new File(
640                         getFilesDir() + File.separator
641                         + mModelInfo.name.replace(' ', '_')
642                         + "_capture_" + format.getChannelCount() + "ch_"
643                         + format.getSampleRate() + "hz_" + encoding
644                         + "_" + (++captureCount) + ".pcm");
645                     Log.i(TAG, "Writing audio to: " + file);
646                     fos = new FileOutputStream(file);
647                 } catch (IOException e) {
648                     Log.e(TAG, "Failed to open output for saving PCM data", e);
649                     postErrorToast("Failed to open output for saving PCM data: "
650                             + e.getMessage());
651                 }
652 
653                 // Inform the user we're recording.
654                 setModelState(mModelInfo, "Recording");
655                 audioRecord.startRecording();
656                 while (bytesRequired > 0) {
657                     int bytesRead = audioRecord.read(buffer, 0, buffer.length);
658                     if (bytesRead == -1) {
659                         break;
660                     }
661                     if (fos != null) {
662                         fos.write(buffer, 0, bytesRead);
663                     }
664                     if (playbackTrack != null) {
665                         playbackTrack.write(buffer, 0, bytesRead);
666                     }
667                     bytesRequired -= bytesRead;
668                 }
669                 audioRecord.stop();
670                 if (fos != null) {
671                   fos.flush();
672                   fos.close();
673                 }
674             } catch (Exception e) {
675                 Log.e(TAG, "Error recording trigger audio", e);
676                 postErrorToast("Error recording trigger audio: " + e.getMessage());
677             } finally {
678                 if (audioRecord != null) {
679                     audioRecord.release();
680                 }
681                 synchronized (SoundTriggerTestService.this) {
682                     if (mModelInfo.captureAudioTrack != null) {
683                         mModelInfo.captureAudioTrack.release();
684                     }
685                     mModelInfo.captureAudioTrack = playbackTrack;
686                 }
687                 setModelState(mModelInfo, "Recording finished");
688             }
689         }
690 
getAudioFormat()691         private AudioFormat getAudioFormat() {
692             if (mEvent != null) {
693                 return mEvent.getCaptureAudioFormat();
694             }
695             if (mRecognitionEvent != null) {
696                 return mRecognitionEvent.captureFormat;
697             }
698             return null;
699         }
700 
getCaptureSession()701         private int getCaptureSession() {
702             if (mEvent != null) {
703                 return mEvent.getCaptureSession();
704             }
705             if (mRecognitionEvent != null) {
706                 return mRecognitionEvent.captureSession;
707             }
708             return 0;
709         }
710     }
711 
712     // Implementation of SoundTriggerDetector.Callback.
713     private class DetectorCallback extends SoundTriggerDetector.Callback {
714         private final ModelInfo mModelInfo;
715 
DetectorCallback(ModelInfo modelInfo)716         public DetectorCallback(ModelInfo modelInfo) {
717             mModelInfo = modelInfo;
718         }
719 
onAvailabilityChanged(int status)720         public void onAvailabilityChanged(int status) {
721             postMessage(mModelInfo.name + " availability changed to: " + status);
722         }
723 
onDetected(SoundTriggerDetector.EventPayload event)724         public void onDetected(SoundTriggerDetector.EventPayload event) {
725             postMessage(mModelInfo.name + " onDetected(): " + eventPayloadToString(event));
726             synchronized (SoundTriggerTestService.this) {
727                 if (mUserActivity != null) {
728                     mUserActivity.handleDetection(mModelInfo.modelUuid);
729                 }
730                 if (mModelInfo.captureAudio) {
731                     new Thread(new CaptureAudioRecorder(mModelInfo, event)).start();
732                 }
733             }
734         }
735 
onError()736         public void onError() {
737             postMessage(mModelInfo.name + " onError()");
738             setModelState(mModelInfo, "Error");
739         }
740 
onRecognitionPaused()741         public void onRecognitionPaused() {
742             postMessage(mModelInfo.name + " onRecognitionPaused()");
743             setModelState(mModelInfo, "Paused");
744         }
745 
onRecognitionResumed()746         public void onRecognitionResumed() {
747             postMessage(mModelInfo.name + " onRecognitionResumed()");
748             setModelState(mModelInfo, "Resumed");
749         }
750     }
751 
eventPayloadToString(SoundTriggerDetector.EventPayload event)752     private String eventPayloadToString(SoundTriggerDetector.EventPayload event) {
753         String result = "EventPayload(";
754         AudioFormat format =  event.getCaptureAudioFormat();
755         result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString());
756         byte[] triggerAudio = event.getTriggerAudio();
757         result = result + ", TriggerAudio: "
758                 + (triggerAudio == null ? "null" : triggerAudio.length);
759         byte[] data = event.getData();
760         result = result + ", Data: " + (data == null ? "null" : data.length);
761         if (data != null) {
762           try {
763             String decodedData = new String(data, "UTF-8");
764             if (decodedData.chars().allMatch(c -> (c >= 32 && c < 128) || c == 0)) {
765                 result = result + ", Decoded Data: '" + decodedData + "'";
766             }
767           } catch (Exception e) {
768             Log.e(TAG, "Failed to decode data");
769           }
770         }
771         result = result + ", CaptureSession: " + event.getCaptureSession();
772         result += " )";
773         return result;
774     }
775 
postMessage(String msg)776     private void postMessage(String msg) {
777         showMessage(msg, Log.INFO, false);
778     }
779 
postError(String msg)780     private void postError(String msg) {
781         showMessage(msg, Log.ERROR, false);
782     }
783 
postToast(String msg)784     private void postToast(String msg) {
785         showMessage(msg, Log.INFO, true);
786     }
787 
postErrorToast(String msg)788     private void postErrorToast(String msg) {
789         showMessage(msg, Log.ERROR, true);
790     }
791 
792     /** Logs the message at the specified level, then forwards it to the activity if present. */
showMessage(String msg, int logLevel, boolean showToast)793     private synchronized void showMessage(String msg, int logLevel, boolean showToast) {
794         Log.println(logLevel, TAG, msg);
795         if (mUserActivity != null) {
796             mUserActivity.showMessage(msg, showToast);
797         }
798     }
799 
setModelState(ModelInfo modelInfo, String state)800     private synchronized void setModelState(ModelInfo modelInfo, String state) {
801         modelInfo.state = state;
802         if (mUserActivity != null) {
803             mUserActivity.setModelState(modelInfo.modelUuid, modelInfo.state);
804         }
805     }
806 }
807