Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class FakeSttInputDeviceWrapper : SttInputDeviceWrapper {
override fun onClick(eventListener: (InputEvent) -> Unit) {
}

override fun releaseResources() {
override fun reinitializeToReleaseResources() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import org.stypox.dicio.io.wake.WakeState

class FakeWakeDeviceWrapper : WakeDeviceWrapper {
override val state: StateFlow<WakeState?> = MutableStateFlow(null)
override val isHeyDicio: StateFlow<Boolean> = MutableStateFlow(true)

override fun download() {}

override fun processFrame(audio16bitPcm: ShortArray): Boolean = false

override fun frameSize(): Int = 1312

override fun releaseResources() {}
override fun reinitializeToReleaseResources() {}
}
3 changes: 1 addition & 2 deletions app/src/main/kotlin/org/stypox/dicio/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import org.stypox.dicio.di.SttInputDeviceWrapper
import org.stypox.dicio.di.WakeDeviceWrapper
import org.stypox.dicio.eval.SkillEvaluator
import org.stypox.dicio.io.wake.WakeService
import org.stypox.dicio.io.wake.WakeState
import org.stypox.dicio.io.wake.WakeState.Loaded
import org.stypox.dicio.io.wake.WakeState.Loading
import org.stypox.dicio.io.wake.WakeState.NotLoaded
Expand Down Expand Up @@ -165,7 +164,7 @@ class MainActivity : BaseActivity() {
override fun onDestroy() {
// the wake word service remains active in the background,
// so we need to release resources that it does not need manually
sttInputDevice.releaseResources()
sttInputDevice.reinitializeToReleaseResources()
isCreated -= 1
super.onDestroy()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ interface SttInputDeviceWrapper {

fun onClick(eventListener: (InputEvent) -> Unit)

fun releaseResources()
fun reinitializeToReleaseResources()
}

class SttInputDeviceWrapperImpl(
Expand Down Expand Up @@ -154,7 +154,7 @@ class SttInputDeviceWrapperImpl(
sttInputDevice?.onClick(eventListener)
}

override fun releaseResources() {
override fun reinitializeToReleaseResources() {
scope.launch { changeInputDeviceTo(inputDeviceSetting) }
}
}
Expand Down
77 changes: 38 additions & 39 deletions app/src/main/kotlin/org/stypox/dicio/di/WakeDeviceWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,32 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.stypox.dicio.io.wake.WakeDevice
import org.stypox.dicio.io.wake.WakeState
import org.stypox.dicio.io.wake.oww.OpenWakeWordDevice
import org.stypox.dicio.settings.datastore.UserSettings
import org.stypox.dicio.settings.datastore.WakeDevice.*
import org.stypox.dicio.settings.datastore.WakeDevice.UNRECOGNIZED
import org.stypox.dicio.settings.datastore.WakeDevice.WAKE_DEVICE_NOTHING
import org.stypox.dicio.settings.datastore.WakeDevice.WAKE_DEVICE_OWW
import org.stypox.dicio.settings.datastore.WakeDevice.WAKE_DEVICE_UNSET
import org.stypox.dicio.util.distinctUntilChangedBlockingFirst
import javax.inject.Singleton

interface WakeDeviceWrapper {
val state: StateFlow<WakeState?>
val isHeyDicio: StateFlow<Boolean>

fun download()

fun processFrame(audio16bitPcm: ShortArray): Boolean

fun frameSize(): Int

fun releaseResources()
fun reinitializeToReleaseResources()
}

typealias DataStoreWakeDevice = org.stypox.dicio.settings.datastore.WakeDevice
Expand All @@ -43,43 +45,54 @@ class WakeDeviceWrapperImpl(
private val okHttpClient: OkHttpClient,
) : WakeDeviceWrapper {
private val scope = CoroutineScope(Dispatchers.Default)
private var stateJob: Job? = null

private var currentSetting: DataStoreWakeDevice
private var wakeDevice: WakeDevice?
private var lastFrameHadWrongSize = false

// null means that the user has not enabled any STT input device
private val _state: MutableStateFlow<WakeState?> = MutableStateFlow(null)
override val state: StateFlow<WakeState?> = _state


private val _isHeyDicio: MutableStateFlow<Boolean>
override val isHeyDicio: StateFlow<Boolean>
private val currentDevice: MutableStateFlow<WakeDevice?>

init {
// Run blocking, because the data store is always available right away since LocaleManager
// also initializes in a blocking way from the same data store.
val (firstWakeDevice, nextWakeDeviceFlow) = dataStore.data
val (firstWakeDeviceSetting, nextWakeDeviceFlow) = dataStore.data
.map { it.wakeDevice }
.distinctUntilChangedBlockingFirst()

currentSetting = firstWakeDevice
wakeDevice = buildInputDevice(firstWakeDevice)
currentSetting = firstWakeDeviceSetting
val firstWakeDevice = buildInputDevice(firstWakeDeviceSetting)
currentDevice = MutableStateFlow(firstWakeDevice)
_isHeyDicio = MutableStateFlow(firstWakeDevice?.isHeyDicio() ?: true)
isHeyDicio = _isHeyDicio

scope.launch {
restartUiStateJob()
currentDevice.collectLatest { newWakeDevice ->
_isHeyDicio.emit(newWakeDevice?.isHeyDicio() ?: true)
if (newWakeDevice == null) {
_state.emit(null)
} else {
newWakeDevice.state.collect { _state.emit(it) }
}
}
}

scope.launch {
nextWakeDeviceFlow.collect(::changeWakeDeviceTo)
}
}

private suspend fun changeWakeDeviceTo(setting: DataStoreWakeDevice) {
val prevWakeDevice = wakeDevice
private fun changeWakeDeviceTo(setting: DataStoreWakeDevice) {
currentSetting = setting
wakeDevice = buildInputDevice(setting)
val newWakeDevice = buildInputDevice(setting)
lastFrameHadWrongSize = false
prevWakeDevice?.destroy()
restartUiStateJob()
currentDevice.update { prevWakeDevice ->
prevWakeDevice?.destroy()
newWakeDevice
}
}

private fun buildInputDevice(setting: DataStoreWakeDevice): WakeDevice? {
Expand All @@ -91,25 +104,13 @@ class WakeDeviceWrapperImpl(
}
}

private suspend fun restartUiStateJob() {
stateJob?.cancel()
val newWakeDevice = wakeDevice
if (newWakeDevice == null) {
stateJob = null
_state.emit(null)
} else {
stateJob = scope.launch {
newWakeDevice.state.collect { _state.emit(it) }
}
}
}

override fun download() {
wakeDevice?.download()
currentDevice.value?.download()
}

override fun processFrame(audio16bitPcm: ShortArray): Boolean {
val device = wakeDevice ?: throw IllegalArgumentException("No wake word device is enabled")
val device = currentDevice.value
?: throw IllegalArgumentException("No wake word device is enabled")

if (audio16bitPcm.size != device.frameSize()) {
if (lastFrameHadWrongSize) {
Expand All @@ -129,13 +130,11 @@ class WakeDeviceWrapperImpl(
}

override fun frameSize(): Int {
return wakeDevice?.frameSize() ?: 0
return currentDevice.value?.frameSize() ?: 0
}

override fun releaseResources() {
scope.launch {
changeWakeDeviceTo(currentSetting)
}
override fun reinitializeToReleaseResources() {
changeWakeDeviceTo(currentSetting)
}
}

Expand Down
5 changes: 5 additions & 0 deletions app/src/main/kotlin/org/stypox/dicio/io/wake/WakeDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ interface WakeDevice {
fun frameSize(): Int

fun destroy()

/**
* Returns `true` if the wake word is "Hey Dicio", `false` if a custom model is being used
*/
fun isHeyDicio(): Boolean
}
25 changes: 20 additions & 5 deletions app/src/main/kotlin/org/stypox/dicio/io/wake/WakeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import org.stypox.dicio.MainActivity
import org.stypox.dicio.MainActivity.Companion.ACTION_WAKE_WORD
Expand Down Expand Up @@ -58,7 +59,7 @@ class WakeService : Service() {
if (MainActivity.isCreated <= 0) {
// if the main activity is neither visible nor in the background,
// then unload the STT after a while because it would be using resources uselessly
sttInputDevice.releaseResources()
sttInputDevice.reinitializeToReleaseResources()
}
}

Expand All @@ -71,6 +72,15 @@ class WakeService : Service() {
override fun onCreate() {
super.onCreate()
notificationManager = getSystemService(this, NotificationManager::class.java)!!

scope.launch {
// Recreate the notification so that it says the correct thing (i.e. there is a
// different string for the "Hey Dicio" wake word and for a custom one).
// Ignore the first one (i.e. the current value), which is handled in onStartCommand.
wakeDevice.isHeyDicio.drop(1).collect { isHeyDicio ->
createForegroundNotification(isHeyDicio)
}
}
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Expand All @@ -80,7 +90,7 @@ class WakeService : Service() {
}

try {
createForegroundNotification()
createForegroundNotification(wakeDevice.isHeyDicio.value)
} catch (t: Throwable) {
stopWithMessage("could not create WakeService foreground notification", t)
return START_NOT_STICKY
Expand Down Expand Up @@ -120,7 +130,7 @@ class WakeService : Service() {
override fun onDestroy() {
listening.set(false)
job.cancel()
wakeDevice.releaseResources()
wakeDevice.reinitializeToReleaseResources()
super.onDestroy()
}

Expand All @@ -135,7 +145,7 @@ class WakeService : Service() {
}
}

private fun createForegroundNotification() {
private fun createForegroundNotification(isHeyDicio: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
FOREGROUND_NOTIFICATION_CHANNEL_ID,
Expand All @@ -148,7 +158,12 @@ class WakeService : Service() {

val notification = NotificationCompat.Builder(this, FOREGROUND_NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_hearing_white)
.setContentTitle(getString(R.string.wake_service_foreground_notification))
.setContentTitle(
getString(
if (isHeyDicio) R.string.wake_service_foreground_notification
else R.string.wake_custom_service_foreground_notification
)
)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setShowWhen(false)
Expand Down
Loading