129 lines
4.9 KiB
Kotlin
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()
|
|
|
|
}
|
|
|
|
}
|