1 /* 2 * Copyright (C) 2021 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.example.android.voiceinteractor; 18 19 import android.media.AudioAttributes; 20 import android.media.AudioRecord; 21 import android.media.MediaRecorder; 22 import android.os.Handler; 23 import android.os.Looper; 24 import android.os.PersistableBundle; 25 import android.os.SharedMemory; 26 import android.os.Trace; 27 import android.service.voice.AlwaysOnHotwordDetector; 28 import android.service.voice.HotwordDetectedResult; 29 import android.service.voice.HotwordDetectionService; 30 import android.service.voice.HotwordRejectedResult; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 36 import java.lang.reflect.InvocationTargetException; 37 import java.lang.reflect.Method; 38 import java.time.Duration; 39 import java.util.function.IntConsumer; 40 41 public class SampleHotwordDetectionService extends HotwordDetectionService { 42 static final String TAG = "SHotwordDetectionSrvc"; 43 44 // AudioRecord config 45 private static final Duration AUDIO_RECORD_BUFFER_DURATION = Duration.ofSeconds(5); 46 private static final Duration DSP_AUDIO_READ_DURATION = Duration.ofSeconds(3); 47 private static final Duration AUDIO_RECORD_RELEASE_TIMEOUT = Duration.ofSeconds(10); 48 createAudioRecord(AlwaysOnHotwordDetector.EventPayload eventPayload, int bytesPerSecond, int sessionId)49 private static AudioRecord createAudioRecord(AlwaysOnHotwordDetector.EventPayload eventPayload, 50 int bytesPerSecond, 51 int sessionId) { 52 int audioRecordBufferSize = getBufferSizeInBytes(bytesPerSecond, 53 AUDIO_RECORD_BUFFER_DURATION.getSeconds()); 54 Log.d(TAG, "creating AudioRecord: bytes=" + audioRecordBufferSize 55 + ", lengthSeconds=" + (audioRecordBufferSize / bytesPerSecond)); 56 return new AudioRecord.Builder() 57 .setAudioAttributes( 58 new AudioAttributes.Builder() 59 .setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD) 60 // TODO see what happens if this is too small 61 .build()) 62 .setAudioFormat(eventPayload.getCaptureAudioFormat()) 63 .setBufferSizeInBytes(audioRecordBufferSize) 64 .setSessionId(sessionId) 65 .setMaxSharedAudioHistoryMillis(AudioRecord.getMaxSharedAudioHistoryMillis()) 66 .build(); 67 } 68 getBufferSizeInBytes(int bytesPerSecond, float bufferLengthSeconds)69 private static int getBufferSizeInBytes(int bytesPerSecond, float bufferLengthSeconds) { 70 return (int) (bytesPerSecond * bufferLengthSeconds); 71 } 72 73 @Override onUpdateState(@ullable PersistableBundle options, @Nullable SharedMemory sharedMemory, long callbackTimeoutMillis, @Nullable IntConsumer statusCallback)74 public void onUpdateState(@Nullable PersistableBundle options, 75 @Nullable SharedMemory sharedMemory, long callbackTimeoutMillis, 76 @Nullable IntConsumer statusCallback) { 77 Log.i(TAG, "onUpdateState"); 78 if (statusCallback != null) { 79 statusCallback.accept(0); 80 } 81 } 82 83 @Override onDetect( @onNull AlwaysOnHotwordDetector.EventPayload eventPayload, long timeoutMillis, @NonNull Callback callback)84 public void onDetect( 85 @NonNull AlwaysOnHotwordDetector.EventPayload eventPayload, 86 long timeoutMillis, 87 @NonNull Callback callback) { 88 Log.d(TAG, "onDetect (Hardware trigger): " + eventPayload); 89 Trace.beginAsyncSection("HDS.onDetected", 0); 90 91 int sampleRate = eventPayload.getCaptureAudioFormat().getSampleRate(); 92 int bytesPerSecond = 93 eventPayload.getCaptureAudioFormat().getFrameSizeInBytes() * sampleRate; 94 95 Integer captureSession = 0; 96 try { 97 Method getCaptureSessionMethod = eventPayload.getClass().getMethod("getCaptureSession"); 98 captureSession = (Integer) getCaptureSessionMethod.invoke(eventPayload); 99 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 100 e.printStackTrace(); 101 } 102 int sessionId = 103 // generateSessionId ? 104 // AudioManager.AUDIO_SESSION_ID_GENERATE : 105 captureSession; 106 Trace.beginAsyncSection("HDS.createAudioRecord", 1); 107 AudioRecord record = createAudioRecord(eventPayload, bytesPerSecond, sessionId); 108 Trace.endAsyncSection("HDS.createAudioRecord", 1); 109 if (record.getState() != AudioRecord.STATE_INITIALIZED) { 110 Log.e(TAG, "Failed to init first AudioRecord."); 111 callback.onRejected(new HotwordRejectedResult.Builder().build()); 112 return; 113 } 114 115 byte[] buffer = new byte[bytesPerSecond * (int) DSP_AUDIO_READ_DURATION.getSeconds()]; 116 Log.d(TAG, "starting read: bytesPerSecond=" + bytesPerSecond 117 + ", totalBufferSize=" + buffer.length); 118 Trace.beginAsyncSection("HDS.startRecording", 1); 119 record.startRecording(); 120 Trace.endAsyncSection("HDS.startRecording", 1); 121 Trace.beginAsyncSection("AudioUtils.read", 1); 122 AudioUtils.read(record, bytesPerSecond, DSP_AUDIO_READ_DURATION.getSeconds(), buffer); 123 Trace.endAsyncSection("AudioUtils.read", 1); 124 125 callback.onDetected( 126 new HotwordDetectedResult.Builder() 127 .setMediaSyncEvent( 128 record.shareAudioHistory("com.example.android.voiceinteractor", 0)) 129 .setHotwordPhraseId(getKeyphraseId(eventPayload)) 130 .build()); 131 new Handler(Looper.getMainLooper()).postDelayed(() -> { 132 Log.i(TAG, "Releasing audio record"); 133 record.stop(); 134 record.release(); 135 }, AUDIO_RECORD_RELEASE_TIMEOUT.toMillis()); 136 Trace.endAsyncSection("HDS.onDetected", 0); 137 } 138 getKeyphraseId(AlwaysOnHotwordDetector.EventPayload payload)139 private int getKeyphraseId(AlwaysOnHotwordDetector.EventPayload payload) { 140 return 0; 141 // if (payload.getKeyphraseRecognitionExtras().isEmpty()) { 142 // return 0; 143 // } 144 // return payload.getKeyphraseRecognitionExtras().get(0).getKeyphraseId(); 145 } 146 147 @Override onDetect(@onNull Callback callback)148 public void onDetect(@NonNull Callback callback) { 149 Log.w(TAG, "onDetect called for microphone trigger"); 150 } 151 152 } 153