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.GenericSoundModel; 27 import android.media.AudioAttributes; 28 import android.media.AudioFormat; 29 import android.media.AudioManager; 30 import android.media.AudioRecord; 31 import android.media.AudioTrack; 32 import android.media.MediaPlayer; 33 import android.media.soundtrigger.SoundTriggerDetector; 34 import android.net.Uri; 35 import android.os.Binder; 36 import android.os.IBinder; 37 import android.util.Log; 38 39 import java.io.File; 40 import java.io.FileInputStream; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.util.HashMap; 44 import java.util.Map; 45 import java.util.Properties; 46 import java.util.Random; 47 import java.util.UUID; 48 49 public 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 { addModel(UUID modelUuid, String state)61 void addModel(UUID modelUuid, String state); setModelState(UUID modelUuid, String state)62 void setModelState(UUID modelUuid, String state); showMessage(String msg, boolean showToast)63 void showMessage(String msg, boolean showToast); handleDetection(UUID modelUuid)64 void handleDetection(UUID modelUuid); 65 } 66 SoundTriggerTestService()67 public SoundTriggerTestService() { 68 super(); 69 mRandom = new Random(); 70 mModelInfoMap = new HashMap(); 71 mBinder = new SoundTriggerTestBinder(); 72 } 73 74 @Override onStartCommand(Intent intent, int flags, int startId)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 onCreate()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 onDestroy()101 public void onDestroy() { 102 super.onDestroy(); 103 stopAllRecognitionsAndUnload(); 104 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 getModelUuidFromIntent(Intent intent)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 onTaskRemoved(Intent rootIntent)172 public void onTaskRemoved(Intent rootIntent) { 173 super.onTaskRemoved(rootIntent); 174 stopAllRecognitionsAndUnload(); 175 stopSelf(); 176 } 177 178 @Override onBind(Intent intent)179 public synchronized IBinder onBind(Intent intent) { 180 return mBinder; 181 } 182 183 public class SoundTriggerTestBinder extends Binder { getService()184 SoundTriggerTestService getService() { 185 // Return instance of our parent so clients can call public methods. 186 return SoundTriggerTestService.this; 187 } 188 } 189 setUserActivity(UserActivity activity)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 stopAllRecognitionsAndUnload()200 private synchronized void stopAllRecognitionsAndUnload() { 201 Log.e(TAG, "Stop all recognitions"); 202 for (ModelInfo modelInfo : mModelInfoMap.values()) { 203 Log.e(TAG, "Loop " + modelInfo.modelUuid); 204 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 } 211 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 } 217 } 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 createNewSoundModel(ModelInfo modelInfo)235 private GenericSoundModel createNewSoundModel(ModelInfo modelInfo) { 236 return new GenericSoundModel(modelInfo.modelUuid, modelInfo.vendorUuid, 237 modelInfo.modelData); 238 } 239 loadModel(UUID modelUuid)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=" 254 + soundModel.getUuid()); 255 setModelState(modelInfo, "Loaded"); 256 } else { 257 postErrorToast("Failed to load " + modelInfo.name + ", UUID=" 258 + soundModel.getUuid() + "!"); 259 setModelState(modelInfo, "Failed to load"); 260 } 261 } 262 unloadModel(UUID modelUuid)263 public synchronized void unloadModel(UUID modelUuid) { 264 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 265 if (modelInfo == null) { 266 postError("Could not find model for: " + modelUuid.toString()); 267 return; 268 } 269 270 postMessage("Unloading model: " + modelInfo.name); 271 272 GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid); 273 if (soundModel == null) { 274 postErrorToast("Sound model not found for " + modelInfo.name + "!"); 275 return; 276 } 277 modelInfo.detector = null; 278 boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid); 279 if (status) { 280 postToast("Successfully unloaded " + modelInfo.name + ", UUID=" 281 + soundModel.getUuid()); 282 setModelState(modelInfo, "Unloaded"); 283 } else { 284 postErrorToast("Failed to unload " + 285 modelInfo.name + ", UUID=" + soundModel.getUuid() + "!"); 286 setModelState(modelInfo, "Failed to unload"); 287 } 288 } 289 reloadModel(UUID modelUuid)290 public synchronized void reloadModel(UUID modelUuid) { 291 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 292 if (modelInfo == null) { 293 postError("Could not find model for: " + modelUuid.toString()); 294 return; 295 } 296 postMessage("Reloading model: " + modelInfo.name); 297 GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid); 298 if (soundModel == null) { 299 postErrorToast("Sound model not found for " + modelInfo.name + "!"); 300 return; 301 } 302 GenericSoundModel updated = createNewSoundModel(modelInfo); 303 boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated); 304 if (status) { 305 postToast("Successfully reloaded " + modelInfo.name + ", UUID=" 306 + modelInfo.modelUuid); 307 setModelState(modelInfo, "Reloaded"); 308 } else { 309 postErrorToast("Failed to reload " 310 + modelInfo.name + ", UUID=" + modelInfo.modelUuid + "!"); 311 setModelState(modelInfo, "Failed to reload"); 312 } 313 } 314 startRecognition(UUID modelUuid)315 public synchronized void startRecognition(UUID modelUuid) { 316 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 317 if (modelInfo == null) { 318 postError("Could not find model for: " + modelUuid.toString()); 319 return; 320 } 321 322 if (modelInfo.detector == null) { 323 postMessage("Creating SoundTriggerDetector for " + modelInfo.name); 324 modelInfo.detector = mSoundTriggerUtil.createSoundTriggerDetector( 325 modelUuid, new DetectorCallback(modelInfo)); 326 } 327 328 postMessage("Starting recognition for " + modelInfo.name + ", UUID=" 329 + modelInfo.modelUuid); 330 if (modelInfo.detector.startRecognition(modelInfo.captureAudio ? 331 SoundTriggerDetector.RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO : 332 SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) { 333 setModelState(modelInfo, "Started"); 334 } else { 335 postErrorToast("Fast failure attempting to start recognition for " + 336 modelInfo.name + ", UUID=" + modelInfo.modelUuid); 337 setModelState(modelInfo, "Failed to start"); 338 } 339 } 340 stopRecognition(UUID modelUuid)341 public synchronized void stopRecognition(UUID modelUuid) { 342 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 343 if (modelInfo == null) { 344 postError("Could not find model for: " + modelUuid.toString()); 345 return; 346 } 347 348 if (modelInfo.detector == null) { 349 postErrorToast("Stop called on null detector for " + 350 modelInfo.name + ", UUID=" + modelInfo.modelUuid); 351 return; 352 } 353 postMessage("Triggering stop recognition for " + 354 modelInfo.name + ", UUID=" + modelInfo.modelUuid); 355 if (modelInfo.detector.stopRecognition()) { 356 setModelState(modelInfo, "Stopped"); 357 } else { 358 postErrorToast("Fast failure attempting to stop recognition for " + 359 modelInfo.name + ", UUID=" + modelInfo.modelUuid); 360 setModelState(modelInfo, "Failed to stop"); 361 } 362 } 363 playTriggerAudio(UUID modelUuid)364 public synchronized void playTriggerAudio(UUID modelUuid) { 365 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 366 if (modelInfo == null) { 367 postError("Could not find model for: " + modelUuid.toString()); 368 return; 369 } 370 if (modelInfo.triggerAudioPlayer != null) { 371 postMessage("Playing trigger audio for " + modelInfo.name); 372 modelInfo.triggerAudioPlayer.start(); 373 } else { 374 postMessage("No trigger audio for " + modelInfo.name); 375 } 376 } 377 playCapturedAudio(UUID modelUuid)378 public synchronized void playCapturedAudio(UUID modelUuid) { 379 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 380 if (modelInfo == null) { 381 postError("Could not find model for: " + modelUuid.toString()); 382 return; 383 } 384 if (modelInfo.captureAudioTrack != null) { 385 postMessage("Playing captured audio for " + modelInfo.name); 386 modelInfo.captureAudioTrack.stop(); 387 modelInfo.captureAudioTrack.reloadStaticData(); 388 modelInfo.captureAudioTrack.play(); 389 } else { 390 postMessage("No captured audio for " + modelInfo.name); 391 } 392 } 393 setCaptureAudioTimeout(UUID modelUuid, int captureTimeoutMs)394 public synchronized void setCaptureAudioTimeout(UUID modelUuid, int captureTimeoutMs) { 395 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 396 if (modelInfo == null) { 397 postError("Could not find model for: " + modelUuid.toString()); 398 return; 399 } 400 modelInfo.captureAudioMs = captureTimeoutMs; 401 Log.i(TAG, "Set " + modelInfo.name + " capture audio timeout to " + 402 captureTimeoutMs + "ms"); 403 } 404 setCaptureAudio(UUID modelUuid, boolean captureAudio)405 public synchronized void setCaptureAudio(UUID modelUuid, boolean captureAudio) { 406 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 407 if (modelInfo == null) { 408 postError("Could not find model for: " + modelUuid.toString()); 409 return; 410 } 411 modelInfo.captureAudio = captureAudio; 412 Log.i(TAG, "Set " + modelInfo.name + " capture audio to " + captureAudio); 413 } 414 hasMicrophonePermission()415 public synchronized boolean hasMicrophonePermission() { 416 return getBaseContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO) 417 == PackageManager.PERMISSION_GRANTED; 418 } 419 modelHasTriggerAudio(UUID modelUuid)420 public synchronized boolean modelHasTriggerAudio(UUID modelUuid) { 421 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 422 return modelInfo != null && modelInfo.triggerAudioPlayer != null; 423 } 424 modelWillCaptureTriggerAudio(UUID modelUuid)425 public synchronized boolean modelWillCaptureTriggerAudio(UUID modelUuid) { 426 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 427 return modelInfo != null && modelInfo.captureAudio; 428 } 429 modelHasCapturedAudio(UUID modelUuid)430 public synchronized boolean modelHasCapturedAudio(UUID modelUuid) { 431 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 432 return modelInfo != null && modelInfo.captureAudioTrack != null; 433 } 434 loadModelsInDataDir()435 private void loadModelsInDataDir() { 436 // Load all the models in the data dir. 437 boolean loadedModel = false; 438 for (File file : getFilesDir().listFiles()) { 439 // Find meta-data in .properties files, ignore everything else. 440 if (!file.getName().endsWith(".properties")) { 441 continue; 442 } 443 444 try (FileInputStream in = new FileInputStream(file)) { 445 Properties properties = new Properties(); 446 properties.load(in); 447 createModelInfo(properties); 448 loadedModel = true; 449 } catch (Exception e) { 450 Log.e(TAG, "Failed to load properties file " + file.getName()); 451 } 452 } 453 454 // Create a few dummy models if we didn't load anything. 455 if (!loadedModel) { 456 Properties dummyModelProperties = new Properties(); 457 for (String name : new String[]{"1", "2", "3"}) { 458 dummyModelProperties.setProperty("name", "Model " + name); 459 createModelInfo(dummyModelProperties); 460 } 461 } 462 } 463 464 /** Parses a Properties collection to generate a sound model. 465 * 466 * Missing keys are filled in with default/random values. 467 * @param properties Has the required 'name' property, but the remaining 'modelUuid', 468 * 'vendorUuid', 'triggerAudio', and 'dataFile' optional properties. 469 * 470 */ createModelInfo(Properties properties)471 private synchronized void createModelInfo(Properties properties) { 472 try { 473 ModelInfo modelInfo = new ModelInfo(); 474 475 if (!properties.containsKey("name")) { 476 throw new RuntimeException("must have a 'name' property"); 477 } 478 modelInfo.name = properties.getProperty("name"); 479 480 if (properties.containsKey("modelUuid")) { 481 modelInfo.modelUuid = UUID.fromString(properties.getProperty("modelUuid")); 482 } else { 483 modelInfo.modelUuid = UUID.randomUUID(); 484 } 485 486 if (properties.containsKey("vendorUuid")) { 487 modelInfo.vendorUuid = UUID.fromString(properties.getProperty("vendorUuid")); 488 } else { 489 modelInfo.vendorUuid = UUID.randomUUID(); 490 } 491 492 if (properties.containsKey("triggerAudio")) { 493 modelInfo.triggerAudioPlayer = MediaPlayer.create(this, Uri.parse( 494 getFilesDir().getPath() + "/" + properties.getProperty("triggerAudio"))); 495 if (modelInfo.triggerAudioPlayer.getDuration() == 0) { 496 modelInfo.triggerAudioPlayer.release(); 497 modelInfo.triggerAudioPlayer = null; 498 } 499 } 500 501 if (properties.containsKey("dataFile")) { 502 File modelDataFile = new File( 503 getFilesDir().getPath() + "/" 504 + properties.getProperty("dataFile")); 505 modelInfo.modelData = new byte[(int) modelDataFile.length()]; 506 FileInputStream input = new FileInputStream(modelDataFile); 507 input.read(modelInfo.modelData, 0, modelInfo.modelData.length); 508 } else { 509 modelInfo.modelData = new byte[1024]; 510 mRandom.nextBytes(modelInfo.modelData); 511 } 512 513 modelInfo.captureAudioMs = Integer.parseInt((String) properties.getOrDefault( 514 "captureAudioDurationMs", "5000")); 515 516 // TODO: Add property support for keyphrase models when they're exposed by the 517 // service. 518 519 // Update our maps containing the button -> id and id -> modelInfo. 520 mModelInfoMap.put(modelInfo.modelUuid, modelInfo); 521 if (mUserActivity != null) { 522 mUserActivity.addModel(modelInfo.modelUuid, modelInfo.name); 523 mUserActivity.setModelState(modelInfo.modelUuid, modelInfo.state); 524 } 525 } catch (IOException e) { 526 Log.e(TAG, "Error parsing properties for " + properties.getProperty("name"), e); 527 } 528 } 529 530 private class CaptureAudioRecorder implements Runnable { 531 private final ModelInfo mModelInfo; 532 private final SoundTriggerDetector.EventPayload mEvent; 533 CaptureAudioRecorder(ModelInfo modelInfo, SoundTriggerDetector.EventPayload event)534 public CaptureAudioRecorder(ModelInfo modelInfo, SoundTriggerDetector.EventPayload event) { 535 mModelInfo = modelInfo; 536 mEvent = event; 537 } 538 539 @Override run()540 public void run() { 541 AudioFormat format = mEvent.getCaptureAudioFormat(); 542 if (format == null) { 543 postErrorToast("No audio format in recognition event."); 544 return; 545 } 546 547 AudioRecord audioRecord = null; 548 AudioTrack playbackTrack = null; 549 try { 550 // Inform the audio flinger that we really do want the stream from the soundtrigger. 551 AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder(); 552 attributesBuilder.setInternalCapturePreset(1999); 553 AudioAttributes attributes = attributesBuilder.build(); 554 555 // Make sure we understand this kind of playback so we know how many bytes to read. 556 String encoding; 557 int bytesPerSample; 558 switch (format.getEncoding()) { 559 case AudioFormat.ENCODING_PCM_8BIT: 560 encoding = "8bit"; 561 bytesPerSample = 1; 562 break; 563 case AudioFormat.ENCODING_PCM_16BIT: 564 encoding = "16bit"; 565 bytesPerSample = 2; 566 break; 567 case AudioFormat.ENCODING_PCM_FLOAT: 568 encoding = "float"; 569 bytesPerSample = 4; 570 break; 571 default: 572 throw new RuntimeException("Unhandled audio format in event"); 573 } 574 575 int bytesRequired = format.getSampleRate() * format.getChannelCount() * 576 bytesPerSample * mModelInfo.captureAudioMs / 1000; 577 int minBufferSize = AudioRecord.getMinBufferSize( 578 format.getSampleRate(), format.getChannelMask(), format.getEncoding()); 579 if (minBufferSize > bytesRequired) { 580 bytesRequired = minBufferSize; 581 } 582 583 // Make an AudioTrack so we can play the data back out after it's finished 584 // recording. 585 try { 586 int channelConfig = AudioFormat.CHANNEL_OUT_MONO; 587 if (format.getChannelCount() == 2) { 588 channelConfig = AudioFormat.CHANNEL_OUT_STEREO; 589 } else if (format.getChannelCount() >= 3) { 590 throw new RuntimeException( 591 "Too many channels in captured audio for playback"); 592 } 593 594 playbackTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 595 format.getSampleRate(), channelConfig, format.getEncoding(), 596 bytesRequired, AudioTrack.MODE_STATIC); 597 } catch (Exception e) { 598 Log.e(TAG, "Exception creating playback track", e); 599 postErrorToast("Failed to create playback track: " + e.getMessage()); 600 } 601 602 audioRecord = new AudioRecord(attributes, format, bytesRequired, 603 mEvent.getCaptureSession()); 604 605 byte[] buffer = new byte[bytesRequired]; 606 607 // Create a file so we can save the output data there for analysis later. 608 FileOutputStream fos = null; 609 try { 610 fos = new FileOutputStream( new File( 611 getFilesDir() + File.separator 612 + mModelInfo.name.replace(' ', '_') 613 + "_capture_" + format.getChannelCount() + "ch_" 614 + format.getSampleRate() + "hz_" + encoding + ".pcm")); 615 } catch (IOException e) { 616 Log.e(TAG, "Failed to open output for saving PCM data", e); 617 postErrorToast("Failed to open output for saving PCM data: " 618 + e.getMessage()); 619 } 620 621 // Inform the user we're recording. 622 setModelState(mModelInfo, "Recording"); 623 audioRecord.startRecording(); 624 while (bytesRequired > 0) { 625 int bytesRead = audioRecord.read(buffer, 0, buffer.length); 626 if (bytesRead == -1) { 627 break; 628 } 629 if (fos != null) { 630 fos.write(buffer, 0, bytesRead); 631 } 632 if (playbackTrack != null) { 633 playbackTrack.write(buffer, 0, bytesRead); 634 } 635 bytesRequired -= bytesRead; 636 } 637 audioRecord.stop(); 638 } catch (Exception e) { 639 Log.e(TAG, "Error recording trigger audio", e); 640 postErrorToast("Error recording trigger audio: " + e.getMessage()); 641 } finally { 642 if (audioRecord != null) { 643 audioRecord.release(); 644 } 645 synchronized (SoundTriggerTestService.this) { 646 if (mModelInfo.captureAudioTrack != null) { 647 mModelInfo.captureAudioTrack.release(); 648 } 649 mModelInfo.captureAudioTrack = playbackTrack; 650 } 651 setModelState(mModelInfo, "Recording finished"); 652 } 653 } 654 } 655 656 // Implementation of SoundTriggerDetector.Callback. 657 private class DetectorCallback extends SoundTriggerDetector.Callback { 658 private final ModelInfo mModelInfo; 659 DetectorCallback(ModelInfo modelInfo)660 public DetectorCallback(ModelInfo modelInfo) { 661 mModelInfo = modelInfo; 662 } 663 onAvailabilityChanged(int status)664 public void onAvailabilityChanged(int status) { 665 postMessage(mModelInfo.name + " availability changed to: " + status); 666 } 667 onDetected(SoundTriggerDetector.EventPayload event)668 public void onDetected(SoundTriggerDetector.EventPayload event) { 669 postMessage(mModelInfo.name + " onDetected(): " + eventPayloadToString(event)); 670 synchronized (SoundTriggerTestService.this) { 671 if (mUserActivity != null) { 672 mUserActivity.handleDetection(mModelInfo.modelUuid); 673 } 674 if (mModelInfo.captureAudio) { 675 new Thread(new CaptureAudioRecorder(mModelInfo, event)).start(); 676 } 677 } 678 } 679 onError()680 public void onError() { 681 postMessage(mModelInfo.name + " onError()"); 682 setModelState(mModelInfo, "Error"); 683 } 684 onRecognitionPaused()685 public void onRecognitionPaused() { 686 postMessage(mModelInfo.name + " onRecognitionPaused()"); 687 setModelState(mModelInfo, "Paused"); 688 } 689 onRecognitionResumed()690 public void onRecognitionResumed() { 691 postMessage(mModelInfo.name + " onRecognitionResumed()"); 692 setModelState(mModelInfo, "Resumed"); 693 } 694 } 695 eventPayloadToString(SoundTriggerDetector.EventPayload event)696 private String eventPayloadToString(SoundTriggerDetector.EventPayload event) { 697 String result = "EventPayload("; 698 AudioFormat format = event.getCaptureAudioFormat(); 699 result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString()); 700 byte[] triggerAudio = event.getTriggerAudio(); 701 result = result + ", TriggerAudio: " 702 + (triggerAudio == null ? "null" : triggerAudio.length); 703 byte[] data = event.getData(); 704 result = result + ", Data: " + (data == null ? "null" : data.length); 705 if (data != null) { 706 try { 707 String decodedData = new String(data, "UTF-8"); 708 if (decodedData.chars().allMatch(c -> (c >= 32 && c < 128) || c == 0)) { 709 result = result + ", Decoded Data: '" + decodedData + "'"; 710 } 711 } catch (Exception e) { 712 Log.e(TAG, "Failed to decode data"); 713 } 714 } 715 result = result + ", CaptureSession: " + event.getCaptureSession(); 716 result += " )"; 717 return result; 718 } 719 postMessage(String msg)720 private void postMessage(String msg) { 721 showMessage(msg, Log.INFO, false); 722 } 723 postError(String msg)724 private void postError(String msg) { 725 showMessage(msg, Log.ERROR, false); 726 } 727 postToast(String msg)728 private void postToast(String msg) { 729 showMessage(msg, Log.INFO, true); 730 } 731 postErrorToast(String msg)732 private void postErrorToast(String msg) { 733 showMessage(msg, Log.ERROR, true); 734 } 735 736 /** Logs the message at the specified level, then forwards it to the activity if present. */ showMessage(String msg, int logLevel, boolean showToast)737 private synchronized void showMessage(String msg, int logLevel, boolean showToast) { 738 Log.println(logLevel, TAG, msg); 739 if (mUserActivity != null) { 740 mUserActivity.showMessage(msg, showToast); 741 } 742 } 743 setModelState(ModelInfo modelInfo, String state)744 private synchronized void setModelState(ModelInfo modelInfo, String state) { 745 modelInfo.state = state; 746 if (mUserActivity != null) { 747 mUserActivity.setModelState(modelInfo.modelUuid, modelInfo.state); 748 } 749 } 750 } 751