Skip to content

Commit 87794fc

Browse files
authored
Remove hardware limitation for wake word detection (#6625)
1 parent e69322a commit 87794fc

File tree

68 files changed

+1214
-2012
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1214
-2012
lines changed

.github/workflows/pr.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,23 +295,23 @@ jobs:
295295
arch: x86_64
296296
profile: "Nexus 5"
297297
target: "google_apis"
298-
gradle_target: ":app:connectedFullDebugAndroidTest :app:connectedMinimalDebugAndroidTest :common:connectedDebugAndroidTest :microfrontend:connectedDebugAndroidTest"
298+
gradle_target: ":app:connectedFullDebugAndroidTest :app:connectedMinimalDebugAndroidTest :common:connectedDebugAndroidTest :microwakeword:connectedDebugAndroidTest"
299299
- api-level: 36
300300
arch: x86_64
301301
profile: "pixel_7"
302302
target: "google_apis"
303-
gradle_target: ":app:connectedFullDebugAndroidTest :app:connectedMinimalDebugAndroidTest :common:connectedDebugAndroidTest :microfrontend:connectedDebugAndroidTest"
303+
gradle_target: ":app:connectedFullDebugAndroidTest :app:connectedMinimalDebugAndroidTest :common:connectedDebugAndroidTest :microwakeword:connectedDebugAndroidTest"
304304
- api-level: 33
305305
arch: x86_64
306306
profile: "automotive_1024p_landscape"
307307
target: "android-automotive"
308-
gradle_target: ":automotive:connectedFullDebugAndroidTest :automotive:connectedMinimalDebugAndroidTest :common:connectedDebugAndroidTest :microfrontend:connectedDebugAndroidTest"
308+
gradle_target: ":automotive:connectedFullDebugAndroidTest :automotive:connectedMinimalDebugAndroidTest :common:connectedDebugAndroidTest :microwakeword:connectedDebugAndroidTest"
309309
- api-level: "34"
310310
system-image-api-level: "34-ext9"
311311
arch: x86_64
312312
profile: "automotive_1024p_landscape"
313313
target: "android-automotive"
314-
gradle_target: ":automotive:connectedFullDebugAndroidTest :automotive:connectedMinimalDebugAndroidTest :common:connectedDebugAndroidTest :microfrontend:connectedDebugAndroidTest"
314+
gradle_target: ":automotive:connectedFullDebugAndroidTest :automotive:connectedMinimalDebugAndroidTest :common:connectedDebugAndroidTest :microwakeword:connectedDebugAndroidTest"
315315
- api-level: 26
316316
arch: x86
317317
profile: "wearos_square"
@@ -321,7 +321,7 @@ jobs:
321321
arch: x86_64
322322
profile: "wearos_small_round"
323323
target: "android-wear"
324-
gradle_target: ":wear:connectedDebugAndroidTest :common:connectedDebugAndroidTest :microfrontend:connectedDebugAndroidTest"
324+
gradle_target: ":wear:connectedDebugAndroidTest :common:connectedDebugAndroidTest :microwakeword:connectedDebugAndroidTest"
325325
steps:
326326
- name: Delete unnecessary tools 🔧
327327
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1

app/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ android {
2323
}
2424
}
2525

26+
buildTypes {
27+
debug {
28+
// Required for HWASan wrap.sh to be included uncompressed in the APK
29+
// See https://developer.android.com/ndk/guides/hwasan
30+
packaging {
31+
jniLibs {
32+
useLegacyPackaging = true
33+
}
34+
}
35+
}
36+
}
37+
2638
lint {
2739
// Until we fully migrate to Material3 this lint issue is too verbose https://github.com/home-assistant/android/issues/5420
2840
disable += listOf("UsingMaterialAndMaterial3Libraries")

app/gradle.lockfile

Lines changed: 12 additions & 22 deletions
Large diffs are not rendered by default.

app/src/androidTest/kotlin/io/homeassistant/companion/android/assist/wakeword/MicroWakeWordModelTest.kt

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,22 @@ package io.homeassistant.companion.android.assist.wakeword
33
import android.content.Context
44
import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.platform.app.InstrumentationRegistry
6-
import io.homeassistant.companion.android.BuildConfig
6+
import io.homeassistant.companion.android.microwakeword.MicroWakeWord
7+
import java.nio.ByteBuffer
8+
import java.nio.ByteOrder
9+
import java.nio.channels.FileChannel
710
import kotlinx.coroutines.test.runTest
811
import org.junit.Assert.assertFalse
912
import org.junit.Assert.assertTrue
10-
import org.junit.Assume.assumeTrue
1113
import org.junit.Test
1214
import org.junit.runner.RunWith
13-
import timber.log.Timber
1415

1516
@RunWith(AndroidJUnit4::class)
1617
class MicroWakeWordModelTest {
1718

1819
private val appContext: Context
1920
get() = InstrumentationRegistry.getInstrumentation().targetContext
2021

21-
/**
22-
* Initializes TfLite and returns true if successful.
23-
* Returns false if initialization fails (e.g., full flavor without GMS).
24-
*/
25-
private suspend fun tryInitializeTfLite(): Boolean {
26-
return try {
27-
TfLiteInitializerImpl().initialize(appContext)
28-
true
29-
} catch (e: Exception) {
30-
Timber.w(e, "TfLite initialization failed, skipping test")
31-
if (BuildConfig.FLAVOR == "full") {
32-
false
33-
} else {
34-
// In minimal the test should run since we use the embedded version of TfLite
35-
throw e
36-
}
37-
}
38-
}
39-
4022
@Test
4123
fun loadsModelsFromAppAssets_verify_models_config_files() = runTest {
4224
val models = MicroWakeWordModelConfig.loadAvailableModels(appContext)
@@ -59,13 +41,18 @@ class MicroWakeWordModelTest {
5941

6042
@Test
6143
fun microWakeWord_loadsAndProcessesAudio_withAllModels() = runTest {
62-
assumeTrue("TfLite not available", tryInitializeTfLite())
6344
val models = MicroWakeWordModelConfig.loadAvailableModels(appContext)
6445

6546
for (model in models) {
66-
val detector = MicroWakeWord.create(appContext, model)
47+
val modelBuffer = loadModelFile(appContext, model.modelAssetPath)
48+
val detector = MicroWakeWord(
49+
modelBuffer = modelBuffer,
50+
featureStepSizeMs = model.micro.featureStepSize,
51+
probabilityCutoff = model.micro.probabilityCutoff,
52+
slidingWindowSize = model.micro.slidingWindowSize,
53+
)
6754

68-
try {
55+
detector.use { detector ->
6956
// Process silent audio (160 samples = 10ms at 16kHz)
7057
val silentAudio = ShortArray(160)
7158
val detected = detector.processAudio(silentAudio)
@@ -75,20 +62,23 @@ class MicroWakeWordModelTest {
7562
"Silent audio should not trigger '${model.wakeWord}' detection",
7663
detected,
7764
)
78-
} finally {
79-
detector.close()
8065
}
8166
}
8267
}
8368

8469
@Test
8570
fun microWakeWord_canResetState_withoutCrashing() = runTest {
86-
assumeTrue("TfLite not available", tryInitializeTfLite())
8771
val models = MicroWakeWordModelConfig.loadAvailableModels(appContext)
8872
val model = models.first()
8973

90-
val detector = MicroWakeWord.create(appContext, model)
91-
try {
74+
val modelBuffer = loadModelFile(appContext, model.modelAssetPath)
75+
val detector = MicroWakeWord(
76+
modelBuffer = modelBuffer,
77+
featureStepSizeMs = model.micro.featureStepSize,
78+
probabilityCutoff = model.micro.probabilityCutoff,
79+
slidingWindowSize = model.micro.slidingWindowSize,
80+
)
81+
detector.use { detector ->
9282
// Process some audio
9383
detector.processAudio(ShortArray(160))
9484
detector.processAudio(ShortArray(160))
@@ -99,8 +89,15 @@ class MicroWakeWordModelTest {
9989
// Should be able to process more audio after reset
10090
val detected = detector.processAudio(ShortArray(160))
10191
assertFalse(detected)
102-
} finally {
103-
detector.close()
10492
}
10593
}
94+
95+
private fun loadModelFile(context: Context, assetPath: String): ByteBuffer {
96+
val assetFd = context.assets.openFd(assetPath)
97+
val inputStream = assetFd.createInputStream()
98+
val fileChannel = inputStream.channel
99+
val mapped = fileChannel.map(FileChannel.MapMode.READ_ONLY, assetFd.startOffset, assetFd.declaredLength)
100+
mapped.order(ByteOrder.nativeOrder())
101+
return mapped
102+
}
106103
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/system/bin/sh
2+
# Enables HWAddressSanitizer (HWASan) for debug builds on ARM64 devices running Android 14+.
3+
# HWASan detects memory errors (buffer overflows, use-after-free, double free, stack-use-after-return).
4+
# See https://developer.android.com/ndk/guides/hwasan
5+
LD_HWASAN=1 exec "$@"

app/src/full/kotlin/io/homeassistant/companion/android/assist/wakeword/TfLiteInitializerImpl.kt

Lines changed: 0 additions & 31 deletions
This file was deleted.

app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistAudioStrategyFactory.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class AssistAudioStrategyFactory @Inject constructor(
4545
wakeWordListenerFactory = wakeWordListenerFactory,
4646
assistConfigManager = assistConfigManager,
4747
wakeWordPhrase = wakeWordPhrase,
48+
audioManager = context.getSystemService(),
4849
onListenerStopped = {
4950
AssistVoiceInteractionService.resumeListening(context)
5051
},

app/src/main/kotlin/io/homeassistant/companion/android/assist/service/AssistVoiceInteractionService.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ class AssistVoiceInteractionService : VoiceInteractionService() {
6464
onWakeWordDetected = ::onWakeWordDetected,
6565
onListenerReady = ::onListenerReady,
6666
onListenerStopped = ::onListenerStopped,
67-
onListenerFailed = ::onListenerFailed,
6867
)
6968
}
7069
private var isServiceReady = false
@@ -142,10 +141,6 @@ class AssistVoiceInteractionService : VoiceInteractionService() {
142141
*/
143142
@SuppressLint("MissingPermission")
144143
private fun startListening() {
145-
if (!assistConfigManager.isWakeWordSupported()) {
146-
Timber.d("Wake word detection is not supported on this device")
147-
return
148-
}
149144
if (!hasRecordAudioPermission()) {
150145
Timber.w("RECORD_AUDIO permission not granted, cannot start listening")
151146
return
@@ -186,14 +181,6 @@ class AssistVoiceInteractionService : VoiceInteractionService() {
186181
stopForegroundCompat()
187182
}
188183

189-
private fun onListenerFailed() {
190-
serviceScope.launch {
191-
Timber.w("Wake word listener failed, disabling wake word to prevent issue")
192-
@SuppressLint("MissingPermission")
193-
assistConfigManager.setWakeWordEnabled(false)
194-
}
195-
}
196-
197184
/**
198185
* Stop listening for wake word.
199186
*/

0 commit comments

Comments
 (0)