Skip to content

Commit

Permalink
Request audio focus for Assist voice input + output (#4308)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jpelgrom authored Apr 5, 2024
1 parent 314a60b commit b0dd673
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 4 deletions.
1 change: 1 addition & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -84,6 +93,7 @@ class AudioRecorder {
recorder?.stop()
recorderJob?.cancel()
recorderJob = null
abandonFocus()
releaseRecorder()
}

Expand All @@ -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!!)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,14 +18,17 @@ 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"
}

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.
Expand Down Expand Up @@ -50,6 +58,7 @@ class AudioUrlPlayer {
)
setOnPreparedListener {
if (isActive) {
requestFocus(isAssistant)
it.start()
cont.resume(true)
} else {
Expand Down Expand Up @@ -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!!)
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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>())
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down

0 comments on commit b0dd673

Please sign in to comment.