Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically detect supported sample rates for each codec #508

Merged
merged 1 commit into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,22 @@ android.applicationVariants.all {
}
}

val configXml = tasks.register("configXml${capitalized}") {
inputs.property("variant.applicationId", variant.applicationId)

val outputFile = variantDir.map { it.file("config-${variant.applicationId}.xml") }
outputs.file(outputFile)

doLast {
outputFile.get().asFile.writeText("""
<?xml version="1.0" encoding="utf-8"?>
<config>
<hidden-api-whitelisted-app package="${variant.applicationId}" />
</config>
""".trimIndent())
}
}

val addonD = tasks.register("addonD${capitalized}") {
inputs.property("variant.applicationId", variant.applicationId)

Expand Down Expand Up @@ -358,6 +374,9 @@ android.applicationVariants.all {
from(permissionsXml.map { it.outputs }) {
into("system/etc/permissions")
}
from(configXml.map { it.outputs }) {
into("system/etc/sysconfig")
}
from(variant.outputs.map { it.outputFile }) {
into("system/priv-app/${variant.applicationId}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.InputType
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.BulletSpan
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
Expand Down Expand Up @@ -42,11 +45,35 @@ class FormatSampleRateDialogFragment : DialogFragment() {

binding = DialogTextInputBinding.inflate(layoutInflater)

binding.message.text = getString(
R.string.format_sample_rate_dialog_message,
sampleRateInfo.format(context, sampleRateInfo.range.first),
sampleRateInfo.format(context, sampleRateInfo.range.last),
)
binding.message.text = SpannableStringBuilder().apply {
append(getString(R.string.format_sample_rate_dialog_message_desc))

// BulletSpan operates on unscaled pixels for some reason.
val density = resources.displayMetrics.density
val gapPx = (density * 4).toInt()
val radiusPx = (density * 2).toInt()

for (range in sampleRateInfo.ranges) {
append('\n')

val start = length

append(getString(
R.string.format_sample_rate_dialog_message_range,
sampleRateInfo.format(context, range.first),
sampleRateInfo.format(context, range.last),
))

val end = length

setSpan(
BulletSpan(gapPx, binding.message.currentTextColor, radiusPx),
start,
end,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE,
)
}
}

// Try to detect if the displayed format is a prefix or suffix since it may not be the same
// in every language.
Expand All @@ -72,12 +99,13 @@ class FormatSampleRateDialogFragment : DialogFragment() {

if (it!!.isNotEmpty()) {
try {
val newValue = it.toString().toUInt()
if (newValue in sampleRateInfo.range) {
value = newValue
value = it.toString().toUInt().apply {
sampleRateInfo.validate(this)
}
} catch (e: NumberFormatException) {
// Ignore
} catch (e: IllegalArgumentException) {
// Ignore
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.chiller3.bcr.extension

import android.annotation.SuppressLint
import android.media.AudioFormat
import android.os.Build

Expand All @@ -11,3 +12,14 @@ val AudioFormat.frameSizeInBytesCompat: Int
assert(encoding == AudioFormat.ENCODING_PCM_16BIT)
2 * channelCount
}

// Static extension functions are currently not supported in Kotlin. Also, we install a sysconfig
Copy link
Contributor

@quyenvsp quyenvsp May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, what mean "we install a sysconfig"? This line seem lead to crash on my Pixel 5a API 34.

Caused by: java.lang.NoSuchFieldException: No field SAMPLE_RATE_HZ_MIN in class Landroid/media/AudioFormat; (declaration of 'android.media.AudioFormat' appears in /system/framework/framework.jar!classes2.dex)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's referring to the /system/etc/sysconfig/config-com.chiller3.bcr.xml file, which is included in BCR's Magisk module. That's supposed to allow BCR to access hidden/system APIs.

// file to allow access to these hidden fields.

@SuppressLint("SoonBlockedPrivateApi")
val SAMPLE_RATE_HZ_MIN_COMPAT: Int =
AudioFormat::class.java.getDeclaredField("SAMPLE_RATE_HZ_MIN").getInt(null)

@SuppressLint("SoonBlockedPrivateApi")
val SAMPLE_RATE_HZ_MAX_COMPAT: Int =
AudioFormat::class.java.getDeclaredField("SAMPLE_RATE_HZ_MAX").getInt(null)
18 changes: 7 additions & 11 deletions app/src/main/java/com/chiller3/bcr/format/AacFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import android.media.MediaFormat
import android.media.MediaMuxer
import java.io.FileDescriptor

object AacFormat : Format() {
class AacFormat : Format() {
override val name: String = "M4A/AAC"
// https://datatracker.ietf.org/doc/html/rfc6381#section-3.1
override val mimeTypeContainer: String = "audio/mp4"
override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_AAC
override val passthrough: Boolean = false
override val paramInfo: FormatParamInfo = RangedParamInfo(
RangedParamType.Bitrate,
// The format has no hard limits, so the lower bound is ffmpeg's recommended minimum bitrate
Expand All @@ -24,16 +28,8 @@ object AacFormat : Format() {
128_000u,
),
)
override val sampleRateInfo: SampleRateInfo = DiscreteSampleRateInfo(
// This what Android's C2 software encoder (C2SoftAacEnc.cpp) supports.
uintArrayOf(8_000u, 11_025u, 12_000u, 16_000u, 22_050u, 24_000u, 32_000u, 44_100u, 48_000u),
16_000u,
)
// https://datatracker.ietf.org/doc/html/rfc6381#section-3.1
override val mimeTypeContainer: String = "audio/mp4"
override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_AAC
override val passthrough: Boolean = false
override val supported: Boolean = true
override val sampleRateInfo: SampleRateInfo =
SampleRateInfo.fromCodec(baseMediaFormat, 16_000u)

override fun updateMediaFormat(mediaFormat: MediaFormat, param: UInt) {
mediaFormat.apply {
Expand Down
15 changes: 6 additions & 9 deletions app/src/main/java/com/chiller3/bcr/format/AmrNbFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ package com.chiller3.bcr.format
import android.media.MediaFormat
import java.io.FileDescriptor

data object AmrNbFormat : Format() {
class AmrNbFormat : Format() {
override val name: String = "AMR-NB"
override val mimeTypeContainer: String = "audio/amr"
override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_AMR_NB
override val passthrough: Boolean = false
override val paramInfo: FormatParamInfo = RangedParamInfo(
RangedParamType.Bitrate,
4_750u..12_200u,
Expand All @@ -19,14 +22,8 @@ data object AmrNbFormat : Format() {
12_200u,
),
)
override val sampleRateInfo: SampleRateInfo = DiscreteSampleRateInfo(
uintArrayOf(8_000u),
8_000u,
)
override val mimeTypeContainer: String = "audio/amr"
override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_AMR_NB
override val passthrough: Boolean = false
override val supported: Boolean = true
override val sampleRateInfo: SampleRateInfo =
SampleRateInfo.fromCodec(baseMediaFormat, 8_000u)

override fun updateMediaFormat(mediaFormat: MediaFormat, param: UInt) {
mediaFormat.apply {
Expand Down
15 changes: 6 additions & 9 deletions app/src/main/java/com/chiller3/bcr/format/AmrWbFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ package com.chiller3.bcr.format
import android.media.MediaFormat
import java.io.FileDescriptor

data object AmrWbFormat : Format() {
class AmrWbFormat : Format() {
override val name: String = "AMR-WB"
override val mimeTypeContainer: String = MediaFormat.MIMETYPE_AUDIO_AMR_WB
override val mimeTypeAudio: String = mimeTypeContainer
override val passthrough: Boolean = false
override val paramInfo: FormatParamInfo = RangedParamInfo(
RangedParamType.Bitrate,
6_600u..23_850u,
Expand All @@ -19,14 +22,8 @@ data object AmrWbFormat : Format() {
23_850u,
),
)
override val sampleRateInfo: SampleRateInfo = DiscreteSampleRateInfo(
uintArrayOf(16_000u),
16_000u,
)
override val mimeTypeContainer: String = MediaFormat.MIMETYPE_AUDIO_AMR_WB
override val mimeTypeAudio: String = mimeTypeContainer
override val passthrough: Boolean = false
override val supported: Boolean = true
override val sampleRateInfo: SampleRateInfo =
SampleRateInfo.fromCodec(baseMediaFormat, 16_000u)

override fun updateMediaFormat(mediaFormat: MediaFormat, param: UInt) {
mediaFormat.apply {
Expand Down
16 changes: 6 additions & 10 deletions app/src/main/java/com/chiller3/bcr/format/FlacFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,20 @@ package com.chiller3.bcr.format
import android.media.MediaFormat
import java.io.FileDescriptor

object FlacFormat : Format() {
class FlacFormat : Format() {
override val name: String = "FLAC"
override val mimeTypeContainer: String = MediaFormat.MIMETYPE_AUDIO_FLAC
override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_FLAC
override val passthrough: Boolean = false
override val paramInfo: FormatParamInfo = RangedParamInfo(
RangedParamType.CompressionLevel,
0u..8u,
// Devices are fast enough nowadays to use the highest compression for realtime recording
8u,
uintArrayOf(0u, 5u, 8u),
)
override val sampleRateInfo: SampleRateInfo = RangedSampleRateInfo(
1u..655_350u,
16_000u,
uintArrayOf(8_000u, 16_000u, 48_000u),
)
override val mimeTypeContainer: String = MediaFormat.MIMETYPE_AUDIO_FLAC
override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_FLAC
override val passthrough: Boolean = false
override val supported: Boolean = true
override val sampleRateInfo: SampleRateInfo =
SampleRateInfo.fromCodec(baseMediaFormat, 16_000u)

override fun updateMediaFormat(mediaFormat: MediaFormat, param: UInt) {
mediaFormat.apply {
Expand Down
62 changes: 38 additions & 24 deletions app/src/main/java/com/chiller3/bcr/format/Format.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.chiller3.bcr.format

import android.media.AudioFormat
import android.media.MediaFormat
import android.util.Log
import com.chiller3.bcr.Preferences
import com.chiller3.bcr.extension.frameSizeInBytesCompat
import java.io.FileDescriptor
Expand All @@ -10,12 +11,6 @@ sealed class Format {
/** User-facing name of the format. */
abstract val name: String

/** Details about the format parameter range and default value. */
abstract val paramInfo: FormatParamInfo

/** Defaults about the supported sample rates. */
abstract val sampleRateInfo: SampleRateInfo

/** The MIME type of the container storing the encoded audio stream. */
abstract val mimeTypeContainer: String

Expand All @@ -29,8 +24,17 @@ sealed class Format {
/** Whether the format takes the PCM samples as is without encoding. */
abstract val passthrough: Boolean

/** Whether the format is supported on the current device. */
abstract val supported: Boolean
/** Details about the format parameter range and default value. */
abstract val paramInfo: FormatParamInfo

/** Defaults about the supported sample rates. */
abstract val sampleRateInfo: SampleRateInfo

/** Bare minimum [MediaFormat] containing only [MediaFormat.KEY_MIME]. */
protected val baseMediaFormat: MediaFormat
get() = MediaFormat().apply {
setString(MediaFormat.KEY_MIME, mimeTypeAudio)
}

/**
* Create a [MediaFormat] representing the encoded audio with parameters matching the specified
Expand All @@ -47,8 +51,7 @@ sealed class Format {
paramInfo.validate(param)
}

val format = MediaFormat().apply {
setString(MediaFormat.KEY_MIME, mimeTypeAudio)
val format = baseMediaFormat.apply {
setInteger(MediaFormat.KEY_CHANNEL_COUNT, audioFormat.channelCount)
setInteger(MediaFormat.KEY_SAMPLE_RATE, audioFormat.sampleRate)
setInteger(KEY_X_FRAME_SIZE_IN_BYTES, audioFormat.frameSizeInBytesCompat)
Expand Down Expand Up @@ -90,17 +93,31 @@ sealed class Format {
abstract fun getContainer(fd: FileDescriptor): Container

companion object {
private val TAG = Format::class.java.simpleName

const val KEY_X_FRAME_SIZE_IN_BYTES = "x-frame-size-in-bytes"

val all: Array<Format> = arrayOf(
OpusFormat,
AacFormat,
FlacFormat,
WaveFormat,
AmrWbFormat,
AmrNbFormat,
)
private val default: Format = all.first { it.supported }
val all: List<Format> by lazy {
val formats = mutableListOf<Format>()

for (constructor in arrayOf(
::OpusFormat,
::AacFormat,
::FlacFormat,
{ WaveFormat },
::AmrWbFormat,
::AmrNbFormat,
)) {
try {
formats.add(constructor())
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Failed to initialize with $constructor", e)
}
}

formats
}
private val default: Format = all.first()

/** Find output format by name. */
fun getByName(name: String): Format? = all.find { it.name == name }
Expand All @@ -112,11 +129,8 @@ sealed class Format {
* sample rate, if set, is set to the nearest valid value.
*/
fun fromPreferences(prefs: Preferences): Triple<Format, UInt?, UInt?> {
// Use the saved format if it is valid and supported on the current device. Otherwise,
// fall back to the default.
val format = prefs.format
?.let { if (it.supported) { it } else { null } }
?: default
// Use the saved format if it is (still) valid.
val format = prefs.format ?: default

// Convert the saved value to the nearest valid value (eg. in case the bitrate range is
// changed in a future version).
Expand Down
24 changes: 13 additions & 11 deletions app/src/main/java/com/chiller3/bcr/format/OpusFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ import android.os.Build
import androidx.annotation.RequiresApi
import java.io.FileDescriptor

object OpusFormat : Format() {
class OpusFormat : Format() {
init {
require(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
"Only supported on Android 10 and newer"
}
}

override val name: String = "OGG/Opus"
// https://datatracker.ietf.org/doc/html/rfc7845#section-9
override val mimeTypeContainer: String = "audio/ogg"
override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_OPUS
override val passthrough: Boolean = false
override val paramInfo: FormatParamInfo = RangedParamInfo(
RangedParamType.Bitrate,
6_000u..510_000u,
Expand All @@ -24,16 +34,8 @@ object OpusFormat : Format() {
48_000u,
),
)
override val sampleRateInfo: SampleRateInfo = DiscreteSampleRateInfo(
// This what Android's C2 software encoder (C2SoftOpusEnc.cpp) supports.
uintArrayOf(8_000u, 12_000u, 16_000u, 24_000u, 48_000u),
16_000u,
)
// https://datatracker.ietf.org/doc/html/rfc7845#section-9
override val mimeTypeContainer: String = "audio/ogg"
override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_OPUS
override val passthrough: Boolean = false
override val supported: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
override val sampleRateInfo: SampleRateInfo =
SampleRateInfo.fromCodec(baseMediaFormat, 16_000u)

override fun updateMediaFormat(mediaFormat: MediaFormat, param: UInt) {
mediaFormat.apply {
Expand Down
Loading
Loading