Home | History | Annotate | Download | only in soundtrigger
      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 {
     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();
    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 
    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);
    174         stopAllRecognitionsAndUnload();
    175         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 
    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 
    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             }
    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      */
    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) {
    657             postMessage(mModelInfo.name + " availability changed to: " + status);
    658         }
    659 
    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 
    672         public void onError() {
    673             postMessage(mModelInfo.name + " onError()");
    674             setModelState(mModelInfo, "Error");
    675         }
    676 
    677         public void onRecognitionPaused() {
    678             postMessage(mModelInfo.name + " onRecognitionPaused()");
    679             setModelState(mModelInfo, "Paused");
    680         }
    681 
    682         public void onRecognitionResumed() {
    683             postMessage(mModelInfo.name + " onRecognitionResumed()");
    684             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();
    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 
    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 }
    742