mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
voice reply improcement
This commit is contained in:
1 parent
01a7ae1930
commit
07d164ac74
3 files changed
+85
-6
No files matched your search
@@ -15,6 +15,7 @@ import androidx.core.content.ContextCompat
|
||||
import app.seb3thehacker.gearslip.call.CallUiState
|
||||
import app.seb3thehacker.gearslip.call.CarCalls
|
||||
import app.seb3thehacker.gearslip.speech.SpeechEngine
|
||||
import app.seb3thehacker.gearslip.speech.VoiceFocus
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -119,15 +120,19 @@ object CarAssistant {
|
||||
_state.value = _state.value.copy(phase = Phase.IDLE)
|
||||
return
|
||||
}
|
||||
appContext?.let(VoiceFocus::acquire)
|
||||
engine.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {}
|
||||
override fun onDone(utteranceId: String?) {
|
||||
VoiceFocus.release()
|
||||
_state.value = _state.value.copy(phase = Phase.IDLE)
|
||||
}
|
||||
@Deprecated("Deprecated in Java, no replacement on this API level")
|
||||
override fun onError(utteranceId: String?) {
|
||||
VoiceFocus.release()
|
||||
_state.value = _state.value.copy(phase = Phase.IDLE)
|
||||
}
|
||||
override fun onStop(utteranceId: String?, interrupted: Boolean) { VoiceFocus.release() }
|
||||
})
|
||||
engine.speak(text, TextToSpeech.QUEUE_FLUSH, null, "gearslip-assistant")
|
||||
}
|
||||
@@ -138,17 +143,19 @@ object CarAssistant {
|
||||
*/
|
||||
fun say(text: String, onDone: () -> Unit) {
|
||||
val engine = tts
|
||||
if (engine == null || !ttsReady || text.isBlank()) {
|
||||
val context = appContext
|
||||
if (engine == null || context == null || !ttsReady || text.isBlank()) {
|
||||
mainHandler.post(onDone)
|
||||
return
|
||||
}
|
||||
VoiceFocus.acquire(context)
|
||||
engine.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {}
|
||||
override fun onDone(utteranceId: String?) { mainHandler.post(onDone) }
|
||||
override fun onDone(utteranceId: String?) { VoiceFocus.release(); mainHandler.post(onDone) }
|
||||
@Deprecated("Deprecated in Java, no replacement on this API level")
|
||||
override fun onError(utteranceId: String?) { mainHandler.post(onDone) }
|
||||
override fun onError(utteranceId: String?) { VoiceFocus.release(); mainHandler.post(onDone) }
|
||||
// Stopped part way: whoever stopped it has already moved on.
|
||||
override fun onStop(utteranceId: String?, interrupted: Boolean) {}
|
||||
override fun onStop(utteranceId: String?, interrupted: Boolean) { VoiceFocus.release() }
|
||||
})
|
||||
engine.speak(text, TextToSpeech.QUEUE_FLUSH, null, "gearslip-say")
|
||||
}
|
||||
|
||||
@@ -85,13 +85,16 @@ object SpeechEngine {
|
||||
worker.execute {
|
||||
if (id != run) return@execute
|
||||
if (!ensureLoaded(app)) return@execute post { onError(NOT_LOADED) }
|
||||
if (cue) cue()
|
||||
if (id != run) return@execute
|
||||
VoiceFocus.acquire(app)
|
||||
val audio = try {
|
||||
if (cue) cue()
|
||||
if (id != run) return@execute
|
||||
record(id) { post(onSpeaking) }
|
||||
} catch (t: Throwable) {
|
||||
GearslipLog.e("speech: recording failed", t)
|
||||
return@execute post { onError("The microphone isn't available right now.") }
|
||||
} finally {
|
||||
VoiceFocus.release()
|
||||
}
|
||||
if (id != run) return@execute
|
||||
if (audio == null) return@execute post { onError("Didn't catch that.") }
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package app.seb3thehacker.gearslip.speech
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import app.seb3thehacker.gearslip.GearslipLog
|
||||
|
||||
/**
|
||||
* Pauses the music while Gearslip talks or listens. Without it the microphone heard the song and
|
||||
* Whisper wrote down lyrics, and a message read aloud had to compete with the music.
|
||||
*
|
||||
* Transient exclusive focus makes music apps pause rather than duck, and they resume on their
|
||||
* own once it's given back. The release waits a moment, so a read-aloud message followed by
|
||||
* "Do you want to reply?" and the microphone is one pause, not three.
|
||||
*/
|
||||
object VoiceFocus {
|
||||
|
||||
private const val RELEASE_DELAY_MS = 1_500L
|
||||
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
private var request: AudioFocusRequest? = null
|
||||
private var audio: AudioManager? = null
|
||||
private var holders = 0
|
||||
|
||||
private val releaseNow = Runnable {
|
||||
val r = request ?: return@Runnable
|
||||
audio?.abandonAudioFocusRequest(r)
|
||||
request = null
|
||||
GearslipLog.i("voice: gave audio focus back")
|
||||
}
|
||||
|
||||
/** Pauses the music until a matching [release]. Safe from any thread. */
|
||||
fun acquire(context: Context) {
|
||||
val app = context.applicationContext
|
||||
main.post {
|
||||
holders++
|
||||
main.removeCallbacks(releaseNow)
|
||||
if (request != null) return@post
|
||||
val manager = app.getSystemService(AudioManager::class.java) ?: return@post
|
||||
val r = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANT)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build(),
|
||||
)
|
||||
.setOnAudioFocusChangeListener { }
|
||||
.build()
|
||||
val granted = manager.requestAudioFocus(r) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
||||
GearslipLog.i("voice: audio focus ${if (granted) "granted, music paused" else "refused"}")
|
||||
if (granted) {
|
||||
audio = manager
|
||||
request = r
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Lets the music come back shortly, unless something else takes focus again first. */
|
||||
fun release() {
|
||||
main.post {
|
||||
if (holders == 0) return@post
|
||||
holders--
|
||||
if (holders == 0) main.postDelayed(releaseNow, RELEASE_DELAY_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user