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=" + 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 unloadModel(UUID modelUuid)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 reloadModel(UUID modelUuid)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 startRecognition(UUID modelUuid)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 stopRecognition(UUID modelUuid)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 playTriggerAudio(UUID modelUuid)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 playCapturedAudio(UUID modelUuid)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 setCaptureAudioTimeout(UUID modelUuid, int captureTimeoutMs)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 setCaptureAudio(UUID modelUuid, boolean captureAudio)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 hasMicrophonePermission()410 public synchronized boolean hasMicrophonePermission() { 411 return getBaseContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO) 412 == PackageManager.PERMISSION_GRANTED; 413 } 414 modelHasTriggerAudio(UUID modelUuid)415 public synchronized boolean modelHasTriggerAudio(UUID modelUuid) { 416 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 417 return modelInfo != null && modelInfo.triggerAudioPlayer != null; 418 } 419 modelWillCaptureTriggerAudio(UUID modelUuid)420 public synchronized boolean modelWillCaptureTriggerAudio(UUID modelUuid) { 421 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 422 return modelInfo != null && modelInfo.captureAudio; 423 } 424 modelHasCapturedAudio(UUID modelUuid)425 public synchronized boolean modelHasCapturedAudio(UUID modelUuid) { 426 ModelInfo modelInfo = mModelInfoMap.get(modelUuid); 427 return modelInfo != null && modelInfo.captureAudioTrack != null; 428 } 429 loadModelsInDataDir()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 } 438 439 try (FileInputStream in = new FileInputStream(file)) { 440 Properties properties = new Properties(); 441 properties.load(in); 442 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 */ createModelInfo(Properties properties)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 CaptureAudioRecorder(ModelInfo modelInfo, SoundTriggerDetector.EventPayload event)528 public CaptureAudioRecorder(ModelInfo modelInfo, SoundTriggerDetector.EventPayload event) { 529 mModelInfo = modelInfo; 530 mEvent = event; 531 } 532 533 @Override run()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 DetectorCallback(ModelInfo modelInfo)652 public DetectorCallback(ModelInfo modelInfo) { 653 mModelInfo = modelInfo; 654 } 655 onAvailabilityChanged(int status)656 public void onAvailabilityChanged(int status) { 657 postMessage(mModelInfo.name + " availability changed to: " + status); 658 } 659 onDetected(SoundTriggerDetector.EventPayload event)660 public void onDetected(SoundTriggerDetector.EventPayload event) { 661 postMessage(mModelInfo.name + " onDetected(): " + eventPayloadToString(event)); 662 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 onError()672 public void onError() { 673 postMessage(mModelInfo.name + " onError()"); 674 setModelState(mModelInfo, "Error"); 675 } 676 onRecognitionPaused()677 public void onRecognitionPaused() { 678 postMessage(mModelInfo.name + " onRecognitionPaused()"); 679 setModelState(mModelInfo, "Paused"); 680 } 681 onRecognitionResumed()682 public void onRecognitionResumed() { 683 postMessage(mModelInfo.name + " onRecognitionResumed()"); 684 setModelState(mModelInfo, "Resumed"); 685 } 686 } 687 eventPayloadToString(SoundTriggerDetector.EventPayload event)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(); 693 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(); 707 result += " )"; 708 return result; 709 } 710 postMessage(String msg)711 private void postMessage(String msg) { 712 showMessage(msg, Log.INFO, false); 713 } 714 postError(String msg)715 private void postError(String msg) { 716 showMessage(msg, Log.ERROR, false); 717 } 718 postToast(String msg)719 private void postToast(String msg) { 720 showMessage(msg, Log.INFO, true); 721 } 722 postErrorToast(String msg)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. */ showMessage(String msg, int logLevel, boolean showToast)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 setModelState(ModelInfo modelInfo, String state)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 } 742