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