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