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
9 changes: 4 additions & 5 deletions app/src/main/java/com/example/ava/services/DeviceBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ import com.example.ava.server.ServerImpl
import com.example.ava.settings.MicrophoneSettingsStore
import com.example.ava.settings.PlayerSettingsStore
import com.example.ava.settings.VoiceSatelliteSettingsStore
import com.example.ava.settings.activeStopWords
import com.example.ava.settings.activeWakeWords
import com.example.ava.settings.availableStopWords
import com.example.ava.settings.availableWakeWords
import com.example.esphomeproto.api.VoiceAssistantFeature
import com.example.esphomeproto.api.deviceInfoResponse
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext

Expand Down Expand Up @@ -95,8 +94,8 @@ class DeviceBuilder @Inject constructor(
private fun MicrophoneSettingsStore.toVoiceInput() = VoiceInputImpl(
microphone = AudioRecordMicrophone(),
wakeWord = MicroWakeWord(),
availableWakeWords = { availableWakeWords.first() },
availableStopWords = { availableStopWords.first() },
availableWakeWords = { get().availableWakeWords(context) },
availableStopWords = { get().availableStopWords(context) },
activeWakeWords = activeWakeWords,
activeStopWords = activeStopWords,
muted = muted
Expand Down
139 changes: 59 additions & 80 deletions app/src/main/java/com/example/ava/settings/MicrophoneSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,22 @@ package com.example.ava.settings

import android.content.Context
import androidx.core.net.toUri
import com.example.ava.wakewords.models.WakeWordWithId
import androidx.datastore.dataStoreFile
import com.example.ava.wakewords.providers.AssetWakeWordProvider
import com.example.ava.wakewords.providers.DocumentTreeWakeWordProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.serialization.Serializable
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

private const val SETTINGS_FILE_NAME = "microphone_settings.json"

@Serializable
data class MicrophoneSettings(
val wakeWord: String = "okay_nabu",
Expand All @@ -37,114 +35,95 @@ private val DEFAULT = MicrophoneSettings()
@Suppress("unused")
@Module
@InstallIn(SingletonComponent::class)
abstract class MicrophoneSettingsModule() {
@Binds
abstract fun bindMicrophoneSettingsStore(microphoneSettingsStoreImpl: MicrophoneSettingsStoreImpl): MicrophoneSettingsStore
object MicrophoneSettingsModule {
@Provides
@Singleton
fun provideMicrophoneSettingsStore(@ApplicationContext context: Context): MicrophoneSettingsStore =
object : MicrophoneSettingsStore, SettingsStore<MicrophoneSettings> by SettingsStoreImpl(
default = DEFAULT,
produceFile = { context.dataStoreFile(SETTINGS_FILE_NAME) },
serializer = MicrophoneSettings.serializer()
) {}
}

interface MicrophoneSettingsStore : SettingsStore<MicrophoneSettings> {
/**
* The wake word to use for wake word detection.
*/
val wakeWord: SettingState<String>
get() = setting(get = { wakeWord }, set = { copy(wakeWord = it) })

/**
* Optional second wake word to use for wake word detection.
*/
val secondWakeWord: SettingState<String?>
get() = setting(get = { secondWakeWord }, set = { copy(secondWakeWord = it) })

/**
* The stop word to use for stop word detection.
*/
val stopWord: SettingState<String>
get() = setting(get = { stopWord }, set = { copy(stopWord = it) })

/**
* The Uri of the directory containing custom wake words or null if not set.
*/
val customWakeWordLocation: SettingState<String?>
get() = setting(
get = { customWakeWordLocation },
set = { copy(customWakeWordLocation = it) })

/**
* The muted state of the microphone.
*/
val muted: SettingState<Boolean>
get() = setting(get = { muted }, set = { copy(muted = it) })

/**
* Returns a list of available wake words from configured providers.
* Helper property that allows getting and setting [wakeWord] and [secondWakeWord] as a list.
*/
val availableWakeWords: Flow<List<WakeWordWithId>>
val activeWakeWords
get() = SettingState(
flow = combine(wakeWord, secondWakeWord) { wakeWord, secondWakeWord ->
listOfNotNull(wakeWord, secondWakeWord)
}
) {
if (it.size > 0) {
wakeWord.set(it[0])
secondWakeWord.set(it.getOrNull(1))
} else Timber.w("Attempted to set empty active wake word list")
}

/**
* Returns a list of available stop words from configured providers.
* Helper property that allows getting and setting [stopWord] as a list.
*/
val availableStopWords: Flow<List<WakeWordWithId>>
}

val MicrophoneSettingsStore.activeWakeWords
get() = SettingState(
flow = combine(wakeWord, secondWakeWord) { wakeWord, secondWakeWord ->
listOfNotNull(wakeWord, secondWakeWord)
}
) {
if (it.size > 0) {
wakeWord.set(it[0])
secondWakeWord.set(it.getOrNull(1))
} else Timber.w("Attempted to set empty active wake word list")
}

val MicrophoneSettingsStore.activeStopWords
get() = SettingState(
flow = stopWord.map { listOf(it) }
) {
if (it.size > 0) {
stopWord.set(it[0])
} else Timber.w("Attempted to set empty stop word list")
}

@Singleton
class MicrophoneSettingsStoreImpl @Inject constructor(@param:ApplicationContext private val context: Context) :
MicrophoneSettingsStore, SettingsStoreImpl<MicrophoneSettings>(
context = context,
default = DEFAULT,
fileName = "microphone_settings.json",
serializer = MicrophoneSettings.serializer()
) {
override val wakeWord = SettingState(getFlow().map { it.wakeWord }) { value ->
update { it.copy(wakeWord = value) }
}

override val secondWakeWord = SettingState(getFlow().map { it.secondWakeWord }) { value ->
update { it.copy(secondWakeWord = value) }
}

override val stopWord = SettingState(getFlow().map { it.stopWord }) { value ->
update { it.copy(stopWord = value) }
}

override val customWakeWordLocation =
SettingState(getFlow().map { it.customWakeWordLocation }) { value ->
update { it.copy(customWakeWordLocation = value) }
val activeStopWords
get() = SettingState(
flow = stopWord.map { listOf(it) }
) {
if (it.size > 0) {
stopWord.set(it[0])
} else Timber.w("Attempted to set empty stop word list")
}
}

override val muted = SettingState(getFlow().map { it.muted }) { value ->
update { it.copy(muted = value) }
}
/**
* Returns a list of available wake words from configured providers.
*/
suspend fun MicrophoneSettings.availableWakeWords(context: Context) =
if (customWakeWordLocation != null) {
AssetWakeWordProvider(assets = context.assets).get() + DocumentTreeWakeWordProvider(
context = context,
treeUri = customWakeWordLocation.toUri()
).get()
} else AssetWakeWordProvider(assets = context.assets).get()

override val availableWakeWords = customWakeWordLocation.mapLatest {
if (it != null)
AssetWakeWordProvider(context.assets).get() + DocumentTreeWakeWordProvider(
context,
it.toUri()
).get()
else
AssetWakeWordProvider(context.assets).get()
}

override val availableStopWords = flow {
emit(
AssetWakeWordProvider(
context.assets,
"stopWords"
).get()
)
}
}
/**
* Returns a list of available stop words from configured providers.
*/
suspend fun MicrophoneSettings.availableStopWords(context: Context) =
AssetWakeWordProvider(
assets = context.assets,
path = "stopWords"
).get()
71 changes: 23 additions & 48 deletions app/src/main/java/com/example/ava/settings/PlayerSettings.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package com.example.ava.settings

import android.content.Context
import dagger.Binds
import androidx.datastore.dataStoreFile
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.map
import kotlinx.serialization.Serializable
import javax.inject.Inject
import javax.inject.Singleton

private const val SETTINGS_FILE_NAME = "player_settings.json"

const val defaultWakeSound = "asset:///sounds/wake_word_triggered.flac"
const val defaultTimerFinishedSound = "asset:///sounds/timer_finished.flac"
const val defaultErrorSound = "asset:///sounds/error.flac"
Expand All @@ -35,91 +36,65 @@ private val DEFAULT = PlayerSettings()
@Suppress("unused")
@Module
@InstallIn(SingletonComponent::class)
abstract class PlayerSettingsModule() {
@Binds
abstract fun bindPlayerSettingsStore(playerSettingsStoreImpl: PlayerSettingsStoreImpl): PlayerSettingsStore
object PlayerSettingsModule {
@Provides
@Singleton
fun providePlayerSettingsStore(@ApplicationContext context: Context): PlayerSettingsStore =
object : PlayerSettingsStore, SettingsStore<PlayerSettings> by SettingsStoreImpl(
default = DEFAULT,
produceFile = { context.dataStoreFile(SETTINGS_FILE_NAME) },
serializer = PlayerSettings.serializer()
) {}
}

interface PlayerSettingsStore : SettingsStore<PlayerSettings> {
/**
* The volume of the player.
*/
val volume: SettingState<Float>
get() = setting(get = { volume }, set = { copy(volume = it) })

/**
* The muted state of the player.
*/
val muted: SettingState<Boolean>
get() = setting(get = { muted }, set = { copy(muted = it) })

/**
* Whether the wake sound should be played when the wake word is triggered.
*/
val enableWakeSound: SettingState<Boolean>
get() = setting(get = { enableWakeSound }, set = { copy(enableWakeSound = it) })

/**
* The path to the wake sound file.
*/
val wakeSound: SettingState<String>
get() = setting(get = { wakeSound }, set = { copy(wakeSound = it) })

/**
* The path to the timer finished sound file.
*/
val timerFinishedSound: SettingState<String>
get() = setting(get = { timerFinishedSound }, set = { copy(timerFinishedSound = it) })

/**
* Whether the timer alarm repeats until the user stops it.
*/
val repeatTimerFinishedSound: SettingState<Boolean>
get() = setting(
get = { repeatTimerFinishedSound },
set = { copy(repeatTimerFinishedSound = it) })

/**
* Whether the error sound should be played when an error occurs.
*/
val enableErrorSound: SettingState<Boolean>
get() = setting(get = { enableErrorSound }, set = { copy(enableErrorSound = it) })

/**
* The path to the error sound file.
*/
val errorSound: SettingState<String>
}

@Singleton
class PlayerSettingsStoreImpl @Inject constructor(@ApplicationContext context: Context) :
PlayerSettingsStore, SettingsStoreImpl<PlayerSettings>(
context = context,
default = DEFAULT,
fileName = "player_settings.json",
serializer = PlayerSettings.serializer()
) {
override val volume = SettingState(getFlow().map { it.volume }) { value ->
update { it.copy(volume = value) }
}

override val muted = SettingState(getFlow().map { it.muted }) { value ->
update { it.copy(muted = value) }
}

override val enableWakeSound = SettingState(getFlow().map { it.enableWakeSound }) { value ->
update { it.copy(enableWakeSound = value) }
}
override val wakeSound = SettingState(getFlow().map { it.wakeSound }) { value ->
update { it.copy(wakeSound = value) }
}

override val timerFinishedSound =
SettingState(getFlow().map { it.timerFinishedSound }) { value ->
update { it.copy(timerFinishedSound = value) }
}

override val repeatTimerFinishedSound =
SettingState(getFlow().map { it.repeatTimerFinishedSound }) { value ->
update { it.copy(repeatTimerFinishedSound = value) }
}

override val enableErrorSound = SettingState(getFlow().map { it.enableErrorSound }) { value ->
update { it.copy(enableErrorSound = value) }
}

override val errorSound = SettingState(getFlow().map { it.errorSound }) { value ->
update { it.copy(errorSound = value) }
}
get() = setting(get = { errorSound }, set = { copy(errorSound = it) })
}
Loading
Loading