/* * 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 . */ 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 -> 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 { value -> // remove already consumed file to free-up space speechApi.clean(value) } fun cancel() { speechApi.cancel() voiceInstructionsPlayer.shutdown() } }