mirror of
https://github.com/home-assistant/android
synced 2024-10-01 13:53:53 +00:00
Request audio focus for Assist voice input + output (#4308)
* Request audio focus for Assist voice input + output - Request exclusive focus for Assist voice input (no other applications allowed to playback) - Request focus with ducking allowed for Assist voice output * Remove useless @Inject
This commit is contained in:
parent
314a60bd2e
commit
b0dd673cb9
|
@ -62,6 +62,7 @@ dependencies {
|
|||
implementation(libs.appcompat)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.media)
|
||||
|
||||
api(libs.androidx.room.runtime)
|
||||
api(libs.androidx.room.ktx)
|
||||
|
|
|
@ -2,8 +2,13 @@ package io.homeassistant.companion.android.common.util
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||
import android.media.AudioRecord
|
||||
import android.media.MediaRecorder.AudioSource
|
||||
import androidx.media.AudioAttributesCompat
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import androidx.media.AudioManagerCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -16,7 +21,7 @@ import kotlinx.coroutines.launch
|
|||
/**
|
||||
* Wrapper around [AudioRecord] providing pre-configured audio recording functionality.
|
||||
*/
|
||||
class AudioRecorder {
|
||||
class AudioRecorder(private val audioManager: AudioManager?) {
|
||||
|
||||
companion object {
|
||||
// Docs: 'currently the only rate that is guaranteed to work on all devices'
|
||||
|
@ -42,6 +47,9 @@ class AudioRecorder {
|
|||
/** Flow emitting audio recording bytes as they come in */
|
||||
val audioBytes = _audioBytes.asSharedFlow()
|
||||
|
||||
private var focusRequest: AudioFocusRequestCompat? = null
|
||||
private val focusListener = OnAudioFocusChangeListener { /* Not used */ }
|
||||
|
||||
/**
|
||||
* Start the recorder. After calling this function, data will be available via [audioBytes].
|
||||
* @throws SecurityException when missing permission to record audio
|
||||
|
@ -55,6 +63,7 @@ class AudioRecorder {
|
|||
if (!ready) return false
|
||||
|
||||
if (recorderJob == null || recorderJob?.isActive == false) {
|
||||
requestFocus()
|
||||
recorder?.startRecording()
|
||||
recorderJob = ioScope.launch {
|
||||
val dataSize = minBufferSize()
|
||||
|
@ -84,6 +93,7 @@ class AudioRecorder {
|
|||
recorder?.stop()
|
||||
recorderJob?.cancel()
|
||||
recorderJob = null
|
||||
abandonFocus()
|
||||
releaseRecorder()
|
||||
}
|
||||
|
||||
|
@ -101,4 +111,34 @@ class AudioRecorder {
|
|||
}
|
||||
|
||||
private fun minBufferSize() = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT)
|
||||
|
||||
private fun requestFocus() {
|
||||
if (audioManager == null) return
|
||||
if (focusRequest == null) {
|
||||
focusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE).run {
|
||||
setAudioAttributes(
|
||||
AudioAttributesCompat.Builder().run {
|
||||
setUsage(AudioAttributesCompat.USAGE_ASSISTANT)
|
||||
setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
|
||||
build()
|
||||
}
|
||||
)
|
||||
setOnAudioFocusChangeListener(focusListener)
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
focusRequest?.let {
|
||||
try {
|
||||
AudioManagerCompat.requestAudioFocus(audioManager, it)
|
||||
} catch (e: Exception) {
|
||||
// We don't use the result / focus if available but if not still continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun abandonFocus() {
|
||||
if (audioManager == null || focusRequest == null) return
|
||||
AudioManagerCompat.abandonAudioFocusRequest(audioManager, focusRequest!!)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
package io.homeassistant.companion.android.common.util
|
||||
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.media.AudioAttributesCompat
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import androidx.media.AudioManagerCompat
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -13,7 +18,7 @@ import kotlinx.coroutines.withContext
|
|||
/**
|
||||
* Simple interface for playing short streaming audio (from URLs).
|
||||
*/
|
||||
class AudioUrlPlayer {
|
||||
class AudioUrlPlayer(private val audioManager: AudioManager?) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AudioUrlPlayer"
|
||||
|
@ -21,6 +26,9 @@ class AudioUrlPlayer {
|
|||
|
||||
private var player: MediaPlayer? = null
|
||||
|
||||
private var focusRequest: AudioFocusRequestCompat? = null
|
||||
private val focusListener = OnAudioFocusChangeListener { /* Not used */ }
|
||||
|
||||
/**
|
||||
* Stream and play audio from the provided [url]. Any currently playing audio will be stopped.
|
||||
* This function will suspend until playback has started.
|
||||
|
@ -50,6 +58,7 @@ class AudioUrlPlayer {
|
|||
)
|
||||
setOnPreparedListener {
|
||||
if (isActive) {
|
||||
requestFocus(isAssistant)
|
||||
it.start()
|
||||
cont.resume(true)
|
||||
} else {
|
||||
|
@ -89,5 +98,41 @@ class AudioUrlPlayer {
|
|||
private fun releasePlayer() {
|
||||
player?.release()
|
||||
player = null
|
||||
abandonFocus()
|
||||
}
|
||||
|
||||
private fun requestFocus(isAssistant: Boolean) {
|
||||
if (audioManager == null) return
|
||||
if (focusRequest == null) {
|
||||
focusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK).run {
|
||||
setAudioAttributes(
|
||||
AudioAttributesCompat.Builder().run {
|
||||
if (isAssistant) {
|
||||
setUsage(AudioAttributesCompat.USAGE_ASSISTANT)
|
||||
setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
|
||||
} else {
|
||||
setUsage(AudioAttributesCompat.USAGE_MEDIA)
|
||||
setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
|
||||
}
|
||||
build()
|
||||
}
|
||||
)
|
||||
setOnAudioFocusChangeListener(focusListener)
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
focusRequest?.let {
|
||||
try {
|
||||
AudioManagerCompat.requestAudioFocus(audioManager, it)
|
||||
} catch (e: Exception) {
|
||||
// We don't use the result / focus if available but if not still continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun abandonFocus() {
|
||||
if (audioManager == null || focusRequest == null) return
|
||||
AudioManagerCompat.abandonAudioFocusRequest(audioManager, focusRequest!!)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package io.homeassistant.companion.android.common.util
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import androidx.core.content.getSystemService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -12,9 +16,11 @@ object UtilModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAudioRecorder(): AudioRecorder = AudioRecorder()
|
||||
fun provideAudioRecorder(@ApplicationContext appContext: Context): AudioRecorder =
|
||||
AudioRecorder(appContext.getSystemService<AudioManager>())
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAudioUrlPlayer(): AudioUrlPlayer = AudioUrlPlayer()
|
||||
fun provideAudioUrlPlayer(@ApplicationContext appContext: Context): AudioUrlPlayer =
|
||||
AudioUrlPlayer(appContext.getSystemService<AudioManager>())
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ ksp = "1.9.23-1.0.19"
|
|||
ktlint = "12.1.0"
|
||||
lifecycle = "2.7.0"
|
||||
material = "1.11.0"
|
||||
media = "1.7.0"
|
||||
media3 = "1.3.0"
|
||||
navigation-compose = "2.7.7"
|
||||
okhttp = "4.12.0"
|
||||
|
@ -86,6 +87,7 @@ activity-compose = { module = "androidx.activity:activity-compose", version.ref
|
|||
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity-compose" }
|
||||
android-beacon-library = { module = "org.altbeacon:android-beacon-library", version.ref = "androidBeaconLibrary" }
|
||||
androidx-health-services-client = { module = "androidx.health:health-services-client", version.ref = "healthServicesClient" }
|
||||
androidx-media = { module = "androidx.media:media", version.ref = "media" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
|
||||
|
|
Loading…
Reference in a new issue