Add session-level audio focus to pause stereo during conversations
- Add AudioFocusManager: holds audio focus for entire conversation with 4-second grace period after TTS finishes - Default to MODE_PAUSE (AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) so stereo fully stops instead of just ducking - Wire into VoiceLoop: acquire on PTT, schedule release on TTS done, immediate release on wrap-up and destroy - Remove per-utterance focus from TTSManager (was causing stereo to bounce on/off between each exchange) - Add musicBehavior property to AppConfig (pause/duck/none)
This commit is contained in:
@@ -10,6 +10,7 @@ import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
|||||||
import farm.hobbs.clippyauto.push.ClippyResponseBus
|
import farm.hobbs.clippyauto.push.ClippyResponseBus
|
||||||
import farm.hobbs.clippyauto.push.ResponseDedup
|
import farm.hobbs.clippyauto.push.ResponseDedup
|
||||||
import farm.hobbs.clippyauto.session.SessionManager
|
import farm.hobbs.clippyauto.session.SessionManager
|
||||||
|
import farm.hobbs.clippyauto.audio.AudioFocusManager
|
||||||
import farm.hobbs.clippyauto.voice.*
|
import farm.hobbs.clippyauto.voice.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -55,6 +56,7 @@ class VoiceLoop(
|
|||||||
private lateinit var speechManager: SpeechManager
|
private lateinit var speechManager: SpeechManager
|
||||||
private lateinit var ttsManager: TTSManager
|
private lateinit var ttsManager: TTSManager
|
||||||
private lateinit var audioCues: AudioCueManager
|
private lateinit var audioCues: AudioCueManager
|
||||||
|
private lateinit var audioFocusManager: AudioFocusManager
|
||||||
|
|
||||||
private var syncJob: Job? = null
|
private var syncJob: Job? = null
|
||||||
private var thinkingTimerRunnable: Runnable? = null
|
private var thinkingTimerRunnable: Runnable? = null
|
||||||
@@ -63,6 +65,9 @@ class VoiceLoop(
|
|||||||
var onQuietModeResponse: ((String) -> Unit)? = null
|
var onQuietModeResponse: ((String) -> Unit)? = null
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
|
// Initialize session-level audio focus (pauses stereo during conversation)
|
||||||
|
audioFocusManager = AudioFocusManager(context)
|
||||||
|
|
||||||
// Initialize audio cues
|
// Initialize audio cues
|
||||||
audioCues = AudioCueManager(context)
|
audioCues = AudioCueManager(context)
|
||||||
|
|
||||||
@@ -77,6 +82,9 @@ class VoiceLoop(
|
|||||||
onSpeakingDone = {
|
onSpeakingDone = {
|
||||||
stateMachine.transition(VoiceState.IDLE)
|
stateMachine.transition(VoiceState.IDLE)
|
||||||
onStateChange(VoiceState.IDLE, "Tap to talk to Clippy")
|
onStateChange(VoiceState.IDLE, "Tap to talk to Clippy")
|
||||||
|
// Schedule delayed focus release (4s grace period)
|
||||||
|
// If Jeremy taps talk again, acquireSessionFocus() cancels this
|
||||||
|
audioFocusManager.scheduleRelease()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
ttsManager.initialize()
|
ttsManager.initialize()
|
||||||
@@ -134,20 +142,23 @@ class VoiceLoop(
|
|||||||
when (stateMachine.state.value) {
|
when (stateMachine.state.value) {
|
||||||
VoiceState.IDLE -> {
|
VoiceState.IDLE -> {
|
||||||
LatencyTracker.markT0(sessionManager.sessionId ?: "") // t0: PTT press
|
LatencyTracker.markT0(sessionManager.sessionId ?: "") // t0: PTT press
|
||||||
|
audioFocusManager.acquireSessionFocus() // Pause stereo for conversation
|
||||||
stateMachine.transition(VoiceState.LISTENING)
|
stateMachine.transition(VoiceState.LISTENING)
|
||||||
speechManager.startListening()
|
speechManager.startListening()
|
||||||
onStateChange(VoiceState.LISTENING, "🎙️ Listening...")
|
onStateChange(VoiceState.LISTENING, "🎙️ Listening...")
|
||||||
}
|
}
|
||||||
VoiceState.SPEAKING -> {
|
VoiceState.SPEAKING -> {
|
||||||
// Interruption — stop TTS, start listening
|
// Interruption — stop TTS, start listening (keep audio focus)
|
||||||
ttsManager.interrupt()
|
ttsManager.interrupt()
|
||||||
audioCues.stopThinking()
|
audioCues.stopThinking()
|
||||||
|
audioFocusManager.acquireSessionFocus() // Re-acquire/cancel pending release
|
||||||
stateMachine.transition(VoiceState.LISTENING)
|
stateMachine.transition(VoiceState.LISTENING)
|
||||||
speechManager.startListening()
|
speechManager.startListening()
|
||||||
onStateChange(VoiceState.LISTENING, "🎙️ Listening...")
|
onStateChange(VoiceState.LISTENING, "🎙️ Listening...")
|
||||||
}
|
}
|
||||||
VoiceState.QUIET -> {
|
VoiceState.QUIET -> {
|
||||||
// In quiet mode, talk button still works for input
|
// In quiet mode, talk button still works for input
|
||||||
|
audioFocusManager.acquireSessionFocus()
|
||||||
stateMachine.transition(VoiceState.LISTENING)
|
stateMachine.transition(VoiceState.LISTENING)
|
||||||
speechManager.startListening()
|
speechManager.startListening()
|
||||||
onStateChange(VoiceState.LISTENING, "🎙️ Listening (quiet mode)...")
|
onStateChange(VoiceState.LISTENING, "🎙️ Listening (quiet mode)...")
|
||||||
@@ -188,6 +199,7 @@ class VoiceLoop(
|
|||||||
|
|
||||||
is VoiceCommandProcessor.CommandResult.WrapUp -> {
|
is VoiceCommandProcessor.CommandResult.WrapUp -> {
|
||||||
onStateChange(VoiceState.PROCESSING, "📋 Wrapping up...")
|
onStateChange(VoiceState.PROCESSING, "📋 Wrapping up...")
|
||||||
|
audioFocusManager.releaseSessionFocus() // Let stereo resume
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// Send wrap-up signal to Matrix — Clippy will post summary
|
// Send wrap-up signal to Matrix — Clippy will post summary
|
||||||
sendToMatrix("[wrap up] Jeremy requested session close and summary.")
|
sendToMatrix("[wrap up] Jeremy requested session close and summary.")
|
||||||
@@ -364,6 +376,7 @@ class VoiceLoop(
|
|||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
cancelThinkingTimer()
|
cancelThinkingTimer()
|
||||||
|
audioFocusManager.releaseSessionFocus()
|
||||||
syncJob?.cancel()
|
syncJob?.cancel()
|
||||||
speechManager.destroy()
|
speechManager.destroy()
|
||||||
ttsManager.shutdown()
|
ttsManager.shutdown()
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package farm.hobbs.clippyauto.audio
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.AudioFocusRequest
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import farm.hobbs.clippyauto.config.AppConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages audio focus for the entire ClippyAuto session.
|
||||||
|
*
|
||||||
|
* Instead of grabbing/releasing focus per STT/TTS cycle (which causes
|
||||||
|
* the car stereo to bounce on and off), this holds focus for the whole
|
||||||
|
* conversation and releases after a configurable grace period.
|
||||||
|
*
|
||||||
|
* Supports three music behavior modes:
|
||||||
|
* - PAUSE: AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE — stereo fully stops
|
||||||
|
* - DUCK: AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK — stereo lowers volume
|
||||||
|
* - NONE: No audio focus changes — stereo keeps playing normally
|
||||||
|
*/
|
||||||
|
class AudioFocusManager(context: Context) {
|
||||||
|
|
||||||
|
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
private var focusRequest: AudioFocusRequest? = null
|
||||||
|
private var hasFocus = false
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private var releaseRunnable: Runnable? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "AudioFocusManager"
|
||||||
|
|
||||||
|
/** Grace period after TTS finishes before releasing focus */
|
||||||
|
private const val RELEASE_GRACE_MS = 4000L // 4 seconds
|
||||||
|
|
||||||
|
// Music behavior modes (stored in AppConfig)
|
||||||
|
const val MODE_PAUSE = 0 // Full exclusive — stereo stops
|
||||||
|
const val MODE_DUCK = 1 // Duck — stereo lowers volume
|
||||||
|
const val MODE_NONE = 2 // No change — stereo keeps playing
|
||||||
|
}
|
||||||
|
|
||||||
|
private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
||||||
|
when (focusChange) {
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||||
|
Log.d(TAG, "Audio focus gained")
|
||||||
|
hasFocus = true
|
||||||
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||||
|
Log.d(TAG, "Audio focus lost permanently")
|
||||||
|
hasFocus = false
|
||||||
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||||
|
Log.d(TAG, "Audio focus lost transiently")
|
||||||
|
// Keep our state — we'll re-request if needed
|
||||||
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||||
|
Log.d(TAG, "Audio focus ducked by another app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grab audio focus for the session.
|
||||||
|
* Call once when the user taps the talk button.
|
||||||
|
* Cancels any pending release from a previous grace period.
|
||||||
|
*/
|
||||||
|
fun acquireSessionFocus(): Boolean {
|
||||||
|
val mode = AppConfig.musicBehavior
|
||||||
|
|
||||||
|
// If user chose "no change", don't touch audio focus at all
|
||||||
|
if (mode == MODE_NONE) {
|
||||||
|
Log.d(TAG, "Music behavior = NONE, skipping audio focus")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any pending delayed release
|
||||||
|
cancelPendingRelease()
|
||||||
|
|
||||||
|
if (hasFocus) {
|
||||||
|
Log.d(TAG, "Already have focus, skipping acquire")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val focusGain = when (mode) {
|
||||||
|
MODE_PAUSE -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
|
||||||
|
MODE_DUCK -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
||||||
|
else -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val request = AudioFocusRequest.Builder(focusGain)
|
||||||
|
.setAudioAttributes(
|
||||||
|
AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_ASSISTANT)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.setOnAudioFocusChangeListener(focusChangeListener)
|
||||||
|
.setAcceptsDelayedFocusGain(true)
|
||||||
|
.setWillPauseWhenDucked(mode == MODE_PAUSE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
focusRequest = request
|
||||||
|
val result = audioManager.requestAudioFocus(request)
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
|
||||||
|
Log.d(TAG, "Session audio focus acquired (mode=$mode)")
|
||||||
|
hasFocus = true
|
||||||
|
true
|
||||||
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
|
||||||
|
Log.d(TAG, "Audio focus delayed")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Audio focus request failed: $result")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val result = audioManager.requestAudioFocus(
|
||||||
|
focusChangeListener,
|
||||||
|
AudioManager.STREAM_MUSIC,
|
||||||
|
focusGain
|
||||||
|
)
|
||||||
|
hasFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
||||||
|
hasFocus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a delayed release of audio focus.
|
||||||
|
* Call when TTS finishes playing. If the user taps talk again
|
||||||
|
* within the grace period, the release is cancelled.
|
||||||
|
*/
|
||||||
|
fun scheduleRelease() {
|
||||||
|
if (AppConfig.musicBehavior == MODE_NONE) return
|
||||||
|
|
||||||
|
cancelPendingRelease()
|
||||||
|
|
||||||
|
releaseRunnable = Runnable {
|
||||||
|
releaseSessionFocus()
|
||||||
|
Log.d(TAG, "Audio focus released after grace period")
|
||||||
|
}
|
||||||
|
handler.postDelayed(releaseRunnable!!, RELEASE_GRACE_MS)
|
||||||
|
Log.d(TAG, "Scheduled focus release in ${RELEASE_GRACE_MS}ms")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a pending delayed release.
|
||||||
|
* Call when the user starts talking again before the grace period expires.
|
||||||
|
*/
|
||||||
|
private fun cancelPendingRelease() {
|
||||||
|
releaseRunnable?.let {
|
||||||
|
handler.removeCallbacks(it)
|
||||||
|
releaseRunnable = null
|
||||||
|
Log.d(TAG, "Cancelled pending focus release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately release audio focus.
|
||||||
|
* Call on session end, "wrap it up", or app close.
|
||||||
|
*/
|
||||||
|
fun releaseSessionFocus() {
|
||||||
|
cancelPendingRelease()
|
||||||
|
|
||||||
|
if (!hasFocus && focusRequest == null) return
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
focusRequest?.let {
|
||||||
|
audioManager.abandonAudioFocusRequest(it)
|
||||||
|
}
|
||||||
|
focusRequest = null
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
audioManager.abandonAudioFocus(focusChangeListener)
|
||||||
|
}
|
||||||
|
hasFocus = false
|
||||||
|
Log.d(TAG, "Session audio focus released")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasFocus(): Boolean = hasFocus
|
||||||
|
}
|
||||||
@@ -24,6 +24,12 @@ object AppConfig {
|
|||||||
private const val KEY_MATRIX_PASS = "matrix_password"
|
private const val KEY_MATRIX_PASS = "matrix_password"
|
||||||
private const val KEY_MATRIX_TOKEN = "matrix_access_token"
|
private const val KEY_MATRIX_TOKEN = "matrix_access_token"
|
||||||
private const val KEY_INITIALIZED = "config_initialized"
|
private const val KEY_INITIALIZED = "config_initialized"
|
||||||
|
private const val KEY_MUSIC_BEHAVIOR = "music_behavior"
|
||||||
|
|
||||||
|
// Music behavior modes
|
||||||
|
const val MUSIC_PAUSE = 0 // Fully stop stereo during conversation
|
||||||
|
const val MUSIC_DUCK = 1 // Lower stereo volume
|
||||||
|
const val MUSIC_NONE = 2 // Leave stereo alone
|
||||||
|
|
||||||
// Defaults (v1 — will be replaced by SetupActivity input)
|
// Defaults (v1 — will be replaced by SetupActivity input)
|
||||||
private const val DEFAULT_HOMESERVER = "https://mess.hobbs.farm"
|
private const val DEFAULT_HOMESERVER = "https://mess.hobbs.farm"
|
||||||
@@ -89,6 +95,16 @@ object AppConfig {
|
|||||||
prefs?.edit()?.putString(KEY_MATRIX_TOKEN, value)?.apply()
|
prefs?.edit()?.putString(KEY_MATRIX_TOKEN, value)?.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Music behavior during Clippy conversations.
|
||||||
|
* Default: MUSIC_PAUSE (fully stop stereo so mic gets clean audio)
|
||||||
|
*/
|
||||||
|
var musicBehavior: Int
|
||||||
|
get() = prefs?.getInt(KEY_MUSIC_BEHAVIOR, MUSIC_PAUSE) ?: MUSIC_PAUSE
|
||||||
|
set(value) {
|
||||||
|
prefs?.edit()?.putInt(KEY_MUSIC_BEHAVIOR, value)?.apply()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update credentials (for future SetupActivity use).
|
* Update credentials (for future SetupActivity use).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package farm.hobbs.clippyauto.voice
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioFocusRequest
|
|
||||||
import android.media.AudioManager
|
|
||||||
import android.speech.tts.TextToSpeech
|
import android.speech.tts.TextToSpeech
|
||||||
import android.speech.tts.UtteranceProgressListener
|
import android.speech.tts.UtteranceProgressListener
|
||||||
import farm.hobbs.clippyauto.util.LatencyTracker
|
import farm.hobbs.clippyauto.util.LatencyTracker
|
||||||
@@ -12,14 +10,15 @@ import java.util.Locale
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages text-to-speech output with queuing, chunking, and audio focus.
|
* Manages text-to-speech output with queuing and chunking.
|
||||||
*
|
*
|
||||||
* Key behaviors:
|
* Key behaviors:
|
||||||
* - Plays through car audio via USAGE_ASSISTANT
|
* - Plays through car audio via USAGE_ASSISTANT AudioAttributes
|
||||||
* - Ducks music during playback (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
|
||||||
* - Queues chunked responses and plays sequentially
|
* - Queues chunked responses and plays sequentially
|
||||||
* - Splits responses > MAX_CHUNK_CHARS into sentence-level chunks
|
* - Splits responses > MAX_CHUNK_CHARS into sentence-level chunks
|
||||||
* - Supports 200ms fade on interruption
|
*
|
||||||
|
* Audio focus is managed at the session level by AudioFocusManager
|
||||||
|
* (not per-utterance here) to avoid the stereo bouncing on/off.
|
||||||
*/
|
*/
|
||||||
class TTSManager(
|
class TTSManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -37,21 +36,19 @@ class TTSManager(
|
|||||||
private var tts: TextToSpeech? = null
|
private var tts: TextToSpeech? = null
|
||||||
private var isReady = false
|
private var isReady = false
|
||||||
private val queue = LinkedList<String>()
|
private val queue = LinkedList<String>()
|
||||||
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
||||||
|
|
||||||
private val audioFocusRequest = AudioFocusRequest.Builder(
|
/** AudioAttributes for routing TTS through Android Auto head unit */
|
||||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
private val ttsAudioAttributes = AudioAttributes.Builder()
|
||||||
).setAudioAttributes(
|
|
||||||
AudioAttributes.Builder()
|
|
||||||
.setUsage(AudioAttributes.USAGE_ASSISTANT)
|
.setUsage(AudioAttributes.USAGE_ASSISTANT)
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
.build()
|
.build()
|
||||||
).build()
|
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
tts = TextToSpeech(context) { status ->
|
tts = TextToSpeech(context) { status ->
|
||||||
if (status == TextToSpeech.SUCCESS) {
|
if (status == TextToSpeech.SUCCESS) {
|
||||||
tts?.language = Locale.US
|
tts?.language = Locale.US
|
||||||
|
// Route TTS audio through Android Auto head unit
|
||||||
|
tts?.setAudioAttributes(ttsAudioAttributes)
|
||||||
tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||||
override fun onStart(utteranceId: String?) {
|
override fun onStart(utteranceId: String?) {
|
||||||
LatencyTracker.markT5() // t5: First audio frame
|
LatencyTracker.markT5() // t5: First audio frame
|
||||||
@@ -62,7 +59,7 @@ class TTSManager(
|
|||||||
if (queue.isNotEmpty()) {
|
if (queue.isNotEmpty()) {
|
||||||
speakNext()
|
speakNext()
|
||||||
} else {
|
} else {
|
||||||
audioManager.abandonAudioFocusRequest(audioFocusRequest)
|
// Audio focus release is handled by AudioFocusManager
|
||||||
onSpeakingDone()
|
onSpeakingDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +77,7 @@ class TTSManager(
|
|||||||
/**
|
/**
|
||||||
* Queue text for TTS playback. Automatically chunks long text.
|
* Queue text for TTS playback. Automatically chunks long text.
|
||||||
* If nothing is currently playing, starts immediately.
|
* If nothing is currently playing, starts immediately.
|
||||||
|
* Note: Audio focus should be acquired by AudioFocusManager before calling this.
|
||||||
*/
|
*/
|
||||||
fun speak(text: String) {
|
fun speak(text: String) {
|
||||||
if (!isReady) return
|
if (!isReady) return
|
||||||
@@ -91,7 +89,6 @@ class TTSManager(
|
|||||||
|
|
||||||
if (queue.size == chunks.size) {
|
if (queue.size == chunks.size) {
|
||||||
// Nothing was playing before — start now
|
// Nothing was playing before — start now
|
||||||
audioManager.requestAudioFocus(audioFocusRequest)
|
|
||||||
speakNext()
|
speakNext()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,11 +128,11 @@ class TTSManager(
|
|||||||
/**
|
/**
|
||||||
* Interrupt current speech (for when Jeremy starts talking).
|
* Interrupt current speech (for when Jeremy starts talking).
|
||||||
* Clears the queue so unplayed chunks are dropped.
|
* Clears the queue so unplayed chunks are dropped.
|
||||||
|
* Note: Audio focus is NOT released here — AudioFocusManager handles that.
|
||||||
*/
|
*/
|
||||||
fun interrupt() {
|
fun interrupt() {
|
||||||
queue.clear()
|
queue.clear()
|
||||||
tts?.stop()
|
tts?.stop()
|
||||||
audioManager.abandonAudioFocusRequest(audioFocusRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
|
|||||||
Reference in New Issue
Block a user