Audio feedback update
Added the following files : - AudioCue.kt - AudioFeedbackEngine - AudioFeedbackSettings Edited files : - BowlingCameraActivity (added the triggerAudiofeedback function)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @file AudioFeedbackEngine.kt
|
||||
* @brief TextToSpeech-backed engine for delivering spoken coaching cues.
|
||||
*/
|
||||
package com.example.jnicpp.bowling
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.util.Log
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* @brief Delivers spoken [AudioCue]s to the user during a live bowling session.
|
||||
*
|
||||
* Wraps Android's [TextToSpeech] and adds:
|
||||
* - A [QueuePolicy]-driven contention model (interrupt / queue / discard).
|
||||
* - Per-call respect for [AudioFeedbackSettings.isMuted] and
|
||||
* [AudioFeedbackSettings.volume], so a mute or volume change anywhere in
|
||||
* the app takes effect on the very next cue without restarting the engine.
|
||||
*
|
||||
* **Lifecycle**: TTS initialisation is asynchronous. Any [speak] calls
|
||||
* arriving before initialisation completes are silently dropped -- the first
|
||||
* few frames of a session are not worth queuing since the bowler is still in
|
||||
* their starting stance. Call [release] from `Activity.onDestroy` to stop
|
||||
* in-progress speech and shut TTS down cleanly.
|
||||
*
|
||||
* **Threading**: [speak] and [release] must be called on the **main thread**,
|
||||
* matching how ML Kit delivers [PoseAnalyzer] results (see [PoseAnalyzer]'s
|
||||
* class doc). TTS internally dispatches synthesis to its own thread.
|
||||
*
|
||||
* @param context Application context used to construct [TextToSpeech].
|
||||
* @param settings Live audio preferences; read on every [speak] call.
|
||||
*/
|
||||
class AudioFeedbackEngine(
|
||||
context: Context,
|
||||
private val settings: AudioFeedbackSettings
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AudioFeedbackEngine"
|
||||
}
|
||||
|
||||
// Null until init { ... } assigns it; never null after that.
|
||||
private var tts: TextToSpeech? = null
|
||||
|
||||
// Set true only once TTS signals SUCCESS -- guards against speak() calls
|
||||
// arriving before the engine is usable.
|
||||
@Volatile
|
||||
private var ready = false
|
||||
|
||||
init {
|
||||
tts = TextToSpeech(context.applicationContext) { status ->
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
// setLanguage is safe here: the callback is delivered on the
|
||||
// main thread, and tts is already assigned by this point.
|
||||
val result = tts?.setLanguage(Locale.getDefault())
|
||||
if (result == TextToSpeech.LANG_MISSING_DATA ||
|
||||
result == TextToSpeech.LANG_NOT_SUPPORTED
|
||||
) {
|
||||
Log.w(TAG, "TTS locale not supported: ${Locale.getDefault()}, falling back to ENGLISH")
|
||||
tts?.setLanguage(Locale.ENGLISH)
|
||||
}
|
||||
ready = true
|
||||
Log.d(TAG, "TTS initialized")
|
||||
} else {
|
||||
Log.w(TAG, "TTS initialization failed (status=$status); audio feedback disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Speaks [cue] according to [policy].
|
||||
*
|
||||
* Silently no-ops when:
|
||||
* - TTS has not finished initialising yet.
|
||||
* - [AudioFeedbackSettings.isMuted] is true.
|
||||
* - [policy] is [QueuePolicy.DISCARD_IF_BUSY] and TTS is already speaking.
|
||||
*
|
||||
* [AudioFeedbackSettings.volume] is applied as TTS's `KEY_PARAM_VOLUME`
|
||||
* parameter so it scales within the system stream volume rather than
|
||||
* replacing it.
|
||||
*
|
||||
* @param cue The coaching cue to speak.
|
||||
* @param policy How to handle contention with an already-playing cue.
|
||||
* Defaults to [QueuePolicy.DISCARD_IF_BUSY] to prevent cue
|
||||
* spam across many consecutive frames with the same issue.
|
||||
*/
|
||||
fun speak(cue: AudioCue, policy: QueuePolicy = QueuePolicy.DISCARD_IF_BUSY) {
|
||||
if (!ready) return
|
||||
if (settings.isMuted) return
|
||||
|
||||
val engine = tts ?: return
|
||||
|
||||
val queueMode: Int = when (policy) {
|
||||
QueuePolicy.INTERRUPT -> TextToSpeech.QUEUE_FLUSH
|
||||
QueuePolicy.QUEUE -> TextToSpeech.QUEUE_ADD
|
||||
QueuePolicy.DISCARD_IF_BUSY -> {
|
||||
if (engine.isSpeaking) return
|
||||
TextToSpeech.QUEUE_ADD
|
||||
}
|
||||
}
|
||||
|
||||
val params = Bundle().apply {
|
||||
putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, settings.volume)
|
||||
}
|
||||
|
||||
// utteranceId lets TTS callbacks (if added later) identify which cue
|
||||
// finished. Using the simple class name avoids UUID allocation per frame.
|
||||
engine.speak(cue.text, queueMode, params, cue::class.java.simpleName)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stops in-progress speech, flushes the TTS queue, and shuts the
|
||||
* engine down. Must be called from `Activity.onDestroy`.
|
||||
*
|
||||
* After this call, [speak] will silently no-op (because [ready] is false
|
||||
* again), so it is safe to call [release] before all references to this
|
||||
* engine are dropped.
|
||||
*/
|
||||
fun release() {
|
||||
ready = false
|
||||
tts?.stop()
|
||||
tts?.shutdown()
|
||||
tts = null
|
||||
Log.d(TAG, "TTS released")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user