Skip to content

Instantly share code, notes, and snippets.

@LouisCAD
Last active October 12, 2021 17:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LouisCAD/4ca4bd6cb85e3d8b2509492d67b282f7 to your computer and use it in GitHub Desktop.
Save LouisCAD/4ca4bd6cb85e3d8b2509492d67b282f7 to your computer and use it in GitHub Desktop.
import android.media.AudioAttributes
import android.os.Build.VERSION.SDK_INT
import androidx.annotation.RequiresApi
import com.beepiz.extensions.android.media.AudioAttrs.Usage.Voice
import splitties.bitflags.withFlag
/**
* @property allowedCapturePolicy is only supported starting with API 29
* @property muteHapticChannels defaults to true (disabled), and is only supported starting with API 29
*/
data class AudioAttrs(
val usage: Usage,
val contentType: ContentType,
val allowedCapturePolicy: CapturePolicy = CapturePolicy.AllowCaptureByAll,
val muteHapticChannels: Boolean = true,
val flags: Int
) {
/**
* [legacyAudioTrackLowLatency] works starting from API 24, and is deprecated in API 26.
*/
@Suppress("InlinedApi")
constructor(
usage: Usage,
contentType: ContentType,
allowedCapturePolicy: CapturePolicy = CapturePolicy.AllowCaptureByAll,
muteHapticChannels: Boolean = true,
enforceAudibility: Boolean = false,
requestHardwareAvSync: Boolean = false,
legacyAudioTrackLowLatency: Boolean = false
) : this(
usage = usage,
contentType = contentType,
allowedCapturePolicy = allowedCapturePolicy,
muteHapticChannels = muteHapticChannels,
flags = 0
.withFlag(if (enforceAudibility) AudioAttributes.FLAG_AUDIBILITY_ENFORCED else 0)
.withFlag(if (requestHardwareAvSync) AudioAttributes.FLAG_HW_AV_SYNC else 0)
.withFlag(
if (legacyAudioTrackLowLatency) {
@Suppress("deprecation") AudioAttributes.FLAG_LOW_LATENCY
} else 0
)
)
sealed interface Usage {
/** E.g.: wake-up alarm. */
object Alarm : Usage
enum class Assistance : Usage {
/** Accessibility, such as with a screen reader. */
Accessibility,
/** Driving or navigation directions. */
NavigationGuidance,
/** Sonifications, such as with interface sounds. */
Sonification
}
/** Audio responses to user queries, audio instructions or help utterances. */
@RequiresApi(26)
object Assistant : Usage
/** Game audio. */
object Game : Usage
/** Media, such as music, or movie soundtracks */
object Media : Usage
sealed interface Notification : Usage {
/**
* Generic notification. See other notification usages for more specialized uses.
*/
companion object : Notification
sealed interface Communication : Notification {
/** Non-immediate type of communication such as e-mail. */
object Delayed : Communication
/** "Instant" communication such as chat, or SMS. */
object Instant : Communication
/**
* Request to enter/end a communication, such as a VoIP communication or
* video-conference.
*/
object Request : Communication
}
/**
* Designed to attract the user's attention, such as a reminder or low battery warning.
*/
object Event : Notification
/** Telephony ringtone. */
object Ringtone : Notification
}
class Other(val value: Int) : Usage
object Unknown : Usage
sealed interface Voice : Usage {
sealed interface Communication : Voice {
companion object : Communication
object Signalling : Communication
}
}
}
enum class ContentType {
Movie, Music, Sonification, Speech, Unknown
}
enum class CapturePolicy {
AllowCaptureByAll,
AllowCaptureBySystem,
AllowCaptureByNone
}
@RequiresApi(21)
fun toAudioAttributes(): AudioAttributes {
return AudioAttributes.Builder().also {
it.setUsage(
when (usage) {
Usage.Alarm -> AudioAttributes.USAGE_ALARM
is Assistance -> when (usage) {
Assistance.Accessibility -> AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
Assistance.NavigationGuidance -> {
AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE
}
Assistance.Sonification -> AudioAttributes.USAGE_ASSISTANCE_SONIFICATION
}
Usage.Assistant -> @Suppress("InlinedApi") AudioAttributes.USAGE_ASSISTANT
Usage.Game -> AudioAttributes.USAGE_GAME
Usage.Media -> AudioAttributes.USAGE_MEDIA
is Usage.Notification -> when (usage) {
is Communication -> when (usage) {
Delayed -> AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED
Instant -> AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT
Request -> AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST
}
Usage.Notification -> AudioAttributes.USAGE_NOTIFICATION
Usage.Notification.Event -> AudioAttributes.USAGE_NOTIFICATION_EVENT
Usage.Notification.Ringtone -> AudioAttributes.USAGE_NOTIFICATION_RINGTONE
}
Usage.Unknown -> AudioAttributes.USAGE_UNKNOWN
is Usage.Other -> usage.value
is Voice.Communication -> when (usage) {
Voice.Communication -> AudioAttributes.USAGE_VOICE_COMMUNICATION
Voice.Communication.Signalling -> {
AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING
}
}
}
)
it.setContentType(
when (contentType) {
ContentType.Movie -> AudioAttributes.CONTENT_TYPE_MOVIE
ContentType.Music -> AudioAttributes.CONTENT_TYPE_MUSIC
ContentType.Sonification -> AudioAttributes.CONTENT_TYPE_SONIFICATION
ContentType.Speech -> AudioAttributes.CONTENT_TYPE_SPEECH
ContentType.Unknown -> AudioAttributes.CONTENT_TYPE_UNKNOWN
}
)
if (SDK_INT >= 29) {
it.setHapticChannelsMuted(muteHapticChannels)
it.setAllowedCapturePolicy(
when (allowedCapturePolicy) {
CapturePolicy.AllowCaptureByAll -> AudioAttributes.ALLOW_CAPTURE_BY_ALL
CapturePolicy.AllowCaptureBySystem -> AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM
CapturePolicy.AllowCaptureByNone -> AudioAttributes.ALLOW_CAPTURE_BY_NONE
}
)
}
it.setFlags(flags)
}.build()
}
}
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Build.VERSION.SDK_INT
import androidx.annotation.RequiresApi
import kotlinx.coroutines.flow.*
import splitties.mainhandler.mainHandler
import splitties.systemservices.audioManager
sealed interface AudioFocus {
sealed interface Gain : AudioFocus {
companion object : Gain
sealed interface Transient : Gain {
companion object : Transient
@RequiresApi(19)
object Exclusive : Transient
object MayDuck : Transient
}
}
sealed interface Loss : AudioFocus {
companion object : Loss
sealed interface Transient : Loss {
companion object : Transient
object CanDuck : Transient
}
}
}
/**
* The [Flow] of [AudioFocus] passed to [block] can receive any [AudioFocus.Loss] subclass,
* and [AudioFocus.Gain] (but not its subclasses).
*/
inline fun withAudioFocus(
gain: AudioFocus.Gain,
audioAttrs: AudioAttrs,
acceptDelayedFocusGain: Boolean,
willPauseWhenDucked: Boolean = false,
block: (audioFocusFlow: Flow<AudioFocus>) -> Unit
) {
when {
SDK_INT >= 26 -> withAudioFocus(
gain = gain,
audioAttributes = audioAttrs.toAudioAttributes(),
acceptDelayedFocusGain = acceptDelayedFocusGain,
willPauseWhenDucked = willPauseWhenDucked,
block = block
)
else -> withAudioFocusLegacy(
gain = gain,
streamType = audioAttrs.legacyStreamType(),
block = block
)
}
}
@PublishedApi
internal inline fun withAudioFocusLegacy(
gain: AudioFocus.Gain,
streamType: Int,
block: (audioFocusFlow: Flow<AudioFocus>) -> Unit
) {
val focusChangeFlow: MutableStateFlow<AudioFocus> = MutableStateFlow(AudioFocus.Loss)
val listener = AudioManager.OnAudioFocusChangeListener { focusChange ->
focusChangeFlow.value = audioFocus(focusChange)
}
@Suppress("deprecation")
try {
val result = audioManager.requestAudioFocus(listener, streamType, gain.value())
focusChangeFlow.value = audioFocusRequestResult(result)
block(focusChangeFlow)
} finally {
audioManager.abandonAudioFocus(listener)
}
}
/**
* The [Flow] of [AudioFocus] passed to [block] can receive any [AudioFocus.Loss] subclass,
* and [AudioFocus.Gain] (but not its subclasses).
*/
@PublishedApi
@RequiresApi(26)
internal inline fun withAudioFocus(
gain: AudioFocus.Gain,
audioAttributes: AudioAttributes,
acceptDelayedFocusGain: Boolean,
willPauseWhenDucked: Boolean,
block: (audioFocusFlow: Flow<AudioFocus>) -> Unit
) {
val focusChangeFlow: MutableStateFlow<AudioFocus> = MutableStateFlow(AudioFocus.Loss)
val audioFocusRequest = AudioFocusRequest.Builder(gain.value()).also {
it.setAudioAttributes(audioAttributes)
it.setWillPauseWhenDucked(willPauseWhenDucked)
it.setAcceptsDelayedFocusGain(acceptDelayedFocusGain)
it.setOnAudioFocusChangeListener({ focusChange ->
focusChangeFlow.value = audioFocus(focusChange)
}, mainHandler)
}.build()
try {
val result = audioManager.requestAudioFocus(audioFocusRequest)
focusChangeFlow.value = audioFocusRequestResult(result)
block(focusChangeFlow)
} finally {
audioManager.abandonAudioFocusRequest(audioFocusRequest)
}
}
@PublishedApi
internal fun audioFocusRequestResult(result: Int): AudioFocus = when (result) {
AudioManager.AUDIOFOCUS_REQUEST_FAILED -> AudioFocus.Loss
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> AudioFocus.Loss.Transient
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> AudioFocus.Gain
else -> error("Unexpected audio focus request result: $result")
}
@PublishedApi
internal fun audioFocus(androidValue: Int): AudioFocus = when (androidValue) {
AudioManager.AUDIOFOCUS_GAIN -> AudioFocus.Gain
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> AudioFocus.Gain.Transient
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> AudioFocus.Gain.Transient.Exclusive
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> AudioFocus.Gain.Transient.MayDuck
AudioManager.AUDIOFOCUS_LOSS -> AudioFocus.Loss
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> AudioFocus.Loss.Transient
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> AudioFocus.Loss.Transient.CanDuck
else -> throw IllegalArgumentException("Unexpected audio focus value: $androidValue")
}
@PublishedApi
internal fun AudioFocus.Gain.value(): Int {
return when (this) {
AudioFocus.Gain -> AudioManager.AUDIOFOCUS_GAIN
AudioFocus.Gain.Transient -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
AudioFocus.Gain.Transient.Exclusive -> when {
SDK_INT >= 19 -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
else -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
}
AudioFocus.Gain.Transient.MayDuck -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment