Garmin/app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt
2024-09-21 00:22:53 +02:00

129 lines
4.9 KiB
Kotlin

/*
* Garmin HUD Companion Application
* Copyright (C) 2022 Piotr Dec / ztsh.eu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.ztsh.garmin.mapbox
import android.content.Context
import com.mapbox.bindgen.Expected
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer
import com.mapbox.navigation.voice.api.MapboxSpeechApi
import com.mapbox.navigation.voice.api.MapboxVoiceInstructionsPlayer
import com.mapbox.navigation.voice.model.SpeechAnnouncement
import com.mapbox.navigation.voice.model.SpeechError
import com.mapbox.navigation.voice.model.SpeechValue
import com.mapbox.navigation.voice.model.SpeechVolume
import eu.ztsh.garmin.UI
import java.util.*
class VoiceControl(private val ui: UI, context: Context) {
/**
* Extracts message that should be communicated to the driver about the upcoming maneuver.
* When possible, downloads a synthesized audio file that can be played back to the driver.
*/
private lateinit var speechApi: MapboxSpeechApi
/**
* Plays the synthesized audio files with upcoming maneuver instructions
* or uses an on-device Text-To-Speech engine to communicate the message to the driver.
* NOTE: do not use lazy initialization for this class since it takes some time to initialize
* the system services required for on-device speech synthesis. With lazy initialization
* there is a high risk that said services will not be available when the first instruction
* has to be played. [MapboxVoiceInstructionsPlayer] should be instantiated in
* `Activity#onCreate`.
*/
private lateinit var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer
init {
speechApi = MapboxSpeechApi(
context,
Locale("pl").language
)
voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer(
context,
Locale("pl").language
)
ui.soundButton.setOnClickListener {
// mute/unmute voice instructions
isVoiceInstructionsMuted = !isVoiceInstructionsMuted
}
}
/**
* Stores and updates the state of whether the voice instructions should be played as they come or muted.
*/
private var isVoiceInstructionsMuted = false
set(value) {
field = value
if (value) {
ui.soundButton.muteAndExtend(UI.BUTTON_ANIMATION_DURATION)
voiceInstructionsPlayer.volume(SpeechVolume(0f))
} else {
ui.soundButton.unmuteAndExtend(UI.BUTTON_ANIMATION_DURATION)
voiceInstructionsPlayer.volume(SpeechVolume(1f))
}
}
/**
* Observes when a new voice instruction should be played.
*/
val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions ->
speechApi.generate(voiceInstructions, speechCallback)
}
/**
* Based on whether the synthesized audio file is available, the callback plays the file
* or uses the fall back which is played back using the on-device Text-To-Speech engine.
*/
private val speechCallback =
MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>> { expected ->
expected.fold(
{ error ->
// play the instruction via fallback text-to-speech engine
voiceInstructionsPlayer.play(
error.fallback,
voiceInstructionsPlayerCallback
)
},
{ value ->
// play the sound file from the external generator
voiceInstructionsPlayer.play(
value.announcement,
voiceInstructionsPlayerCallback
)
}
)
}
/**
* When a synthesized audio file was downloaded, this callback cleans up the disk after it was played.
*/
private val voiceInstructionsPlayerCallback =
MapboxNavigationConsumer<SpeechAnnouncement> { value ->
// remove already consumed file to free-up space
speechApi.clean(value)
}
fun cancel() {
speechApi.cancel()
voiceInstructionsPlayer.shutdown()
}
}