Skip to content

Commit

Permalink
Merge pull request #56 from chenxiaolong/sample-rate
Browse files Browse the repository at this point in the history
Add support for configurable sample rate
  • Loading branch information
chenxiaolong authored May 31, 2022
2 parents ef6ccb5 + 057a5fe commit 23d4dbc
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 62 deletions.
1 change: 1 addition & 0 deletions app/magisk/updates/release/changelog.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### Unreleased

* Change output format button group to material chips to prevent text from being cut off with narrower screen widths (Issue: #52, PR: #55, @chenxiaolong)
* Add support for configuring the capture sample rate (PR: #56, @chenxiaolong)

### Version 1.6

Expand Down
82 changes: 63 additions & 19 deletions app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
@file:OptIn(ExperimentalUnsignedTypes::class)

package com.chiller3.bcr

import android.os.Bundle
Expand All @@ -10,12 +13,10 @@ import com.chiller3.bcr.databinding.FormatBottomSheetChipBinding
import com.chiller3.bcr.format.*
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.chip.ChipGroup
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider

class FormatBottomSheetFragment : BottomSheetDialogFragment(),
ChipGroup.OnCheckedStateChangeListener, LabelFormatter, Slider.OnChangeListener,
View.OnClickListener {
ChipGroup.OnCheckedStateChangeListener, Slider.OnChangeListener, View.OnClickListener {
private var _binding: FormatBottomSheetBinding? = null
private val binding
get() = _binding!!
Expand All @@ -24,14 +25,19 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
private val formatToChipId = HashMap<Format, Int>()
private lateinit var formatParamInfo: FormatParamInfo

private val chipIdToSampleRate = HashMap<Int, UInt?>()
private val sampleRateToChipId = HashMap<UInt?, Int>()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FormatBottomSheetBinding.inflate(inflater, container, false)

binding.paramSlider.setLabelFormatter(this)
binding.paramSlider.setLabelFormatter {
formatParamInfo.format(it.toUInt())
}
binding.paramSlider.addOnChangeListener(this)

binding.reset.setOnClickListener(this)
Expand All @@ -41,20 +47,20 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
continue
}

val chipBinding = FormatBottomSheetChipBinding.inflate(
inflater, binding.nameGroup, false)
val id = View.generateViewId()
chipBinding.root.id = id
chipBinding.root.text = format.name
chipBinding.root.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
binding.nameGroup.addView(chipBinding.root)
chipIdToFormat[id] = format
formatToChipId[format] = id
addFormatChip(inflater, format)
}

binding.nameGroup.setOnCheckedStateChangeListener(this)

addSampleRateChip(inflater, null)
for (sampleRate in SampleRates.all) {
addSampleRateChip(inflater, sampleRate)
}

binding.sampleRateGroup.setOnCheckedStateChangeListener(this)

refreshFormat()
refreshSampleRate()

return binding.root
}
Expand All @@ -64,6 +70,30 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
_binding = null
}

private fun addFormatChip(inflater: LayoutInflater, format: Format) {
val chipBinding = FormatBottomSheetChipBinding.inflate(
inflater, binding.nameGroup, false)
val id = View.generateViewId()
chipBinding.root.id = id
chipBinding.root.text = format.name
chipBinding.root.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
binding.nameGroup.addView(chipBinding.root)
chipIdToFormat[id] = format
formatToChipId[format] = id
}

private fun addSampleRateChip(inflater: LayoutInflater, sampleRate: UInt?) {
val chipBinding = FormatBottomSheetChipBinding.inflate(
inflater, binding.sampleRateGroup, false)
val id = View.generateViewId()
chipBinding.root.id = id
chipBinding.root.text = SampleRates.format(requireContext(), sampleRate)
chipBinding.root.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
binding.sampleRateGroup.addView(chipBinding.root)
chipIdToSampleRate[id] = sampleRate
sampleRateToChipId[sampleRate] = id
}

/**
* Update UI based on currently selected format in the preferences.
*
Expand All @@ -74,6 +104,11 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
binding.nameGroup.check(formatToChipId[format]!!)
}

private fun refreshSampleRate() {
val sampleRate = SampleRates.fromPreferences(requireContext())
binding.sampleRateGroup.check(sampleRateToChipId[sampleRate]!!)
}

/**
* Update parameter title and slider to match format parameter specifications.
*/
Expand Down Expand Up @@ -110,12 +145,18 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
}

override fun onCheckedChanged(group: ChipGroup, checkedIds: MutableList<Int>) {
Preferences.setFormatName(requireContext(), chipIdToFormat[checkedIds.first()]!!.name)
refreshParam()
}
val context = requireContext()

override fun getFormattedValue(value: Float): String =
formatParamInfo.format(value.toUInt())
when (group) {
binding.nameGroup -> {
Preferences.setFormatName(context, chipIdToFormat[checkedIds.first()]!!.name)
refreshParam()
}
binding.sampleRateGroup -> {
Preferences.setSampleRate(context, chipIdToSampleRate[checkedIds.first()])
}
}
}

override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
when (slider) {
Expand All @@ -129,10 +170,13 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
override fun onClick(v: View?) {
when (v) {
binding.reset -> {
Preferences.resetAllFormats(requireContext())
val context = requireContext()
Preferences.resetAllFormats(context)
Preferences.setSampleRate(context, null)
refreshFormat()
// Need to explicitly refresh the parameter when the default format is already chosen
refreshParam()
refreshSampleRate()
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions app/src/main/java/com/chiller3/bcr/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ object Preferences {
// Not associated with a UI preference
private const val PREF_FORMAT_NAME = "codec_name"
private const val PREF_FORMAT_PARAM_PREFIX = "codec_param_"
const val PREF_SAMPLE_RATE = "sample_rate"

/**
* Get the default output directory. The directory should always be writable and is suitable for
Expand Down Expand Up @@ -184,4 +185,44 @@ object Preferences {

editor.apply()
}

/**
* Get the saved sample rate.
*/
fun getSampleRate(context: Context): UInt? {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Use a sentinel value because doing contains + getInt results in TOCTOU issues
val value = prefs.getInt(PREF_SAMPLE_RATE, -1)

return if (value == -1) {
null
} else {
value.toUInt()
}
}

/**
* Set the sample rate.
*
* @param sampleRate Must not be [UInt.MAX_VALUE]
*
* @throws IllegalArgumentException if [sampleRate] is [UInt.MAX_VALUE]
*/
fun setSampleRate(context: Context, sampleRate: UInt?) {
// -1 (when casted to int) is used as a sentinel value
if (sampleRate == UInt.MAX_VALUE) {
throw IllegalArgumentException("Sample rate cannot be ${UInt.MAX_VALUE}")
}

val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val editor = prefs.edit()

if (sampleRate == null) {
editor.remove(PREF_SAMPLE_RATE)
} else {
editor.putInt(PREF_SAMPLE_RATE, sampleRate.toInt())
}

editor.apply()
}
}
14 changes: 10 additions & 4 deletions app/src/main/java/com/chiller3/bcr/RecorderThread.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.documentfile.provider.DocumentFile
import com.chiller3.bcr.format.Container
import com.chiller3.bcr.format.Format
import com.chiller3.bcr.format.Formats
import com.chiller3.bcr.format.SampleRates
import java.io.IOException
import java.lang.Integer.min
import java.nio.ByteBuffer
Expand Down Expand Up @@ -54,6 +55,7 @@ class RecorderThread(
// Format
private val format: Format
private val formatParam: UInt?
private val sampleRate = SampleRates.fromPreferences(context)

init {
logI("Created thread for call: $call")
Expand Down Expand Up @@ -261,10 +263,14 @@ class RecorderThread(
private fun recordUntilCancelled(pfd: ParcelFileDescriptor) {
AndroidProcess.setThreadPriority(AndroidProcess.THREAD_PRIORITY_AUDIO)

val audioFormat = AudioFormat.Builder()
.setEncoding(ENCODING)
.setChannelMask(CHANNEL_CONFIG)
.build()
val audioFormat = AudioFormat.Builder().run {
setEncoding(ENCODING)
setChannelMask(CHANNEL_CONFIG)
if (sampleRate != null) {
setSampleRate(sampleRate.toInt())
}
build()
}
val audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL)
.setAudioFormat(audioFormat)
Expand Down
13 changes: 8 additions & 5 deletions app/src/main/java/com/chiller3/bcr/SettingsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.preference.SwitchPreferenceCompat
import com.chiller3.bcr.format.Formats
import com.chiller3.bcr.format.NoParamInfo
import com.chiller3.bcr.format.RangedParamInfo
import com.chiller3.bcr.format.SampleRates

class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -109,15 +110,17 @@ class SettingsActivity : AppCompatActivity() {
}

private fun refreshOutputFormat() {
val (format, formatParamSaved) = Formats.fromPreferences(requireContext())
val context = requireContext()
val (format, formatParamSaved) = Formats.fromPreferences(context)
val formatParam = formatParamSaved ?: format.paramInfo.default
val summary = getString(R.string.pref_output_format_desc)
val suffix = when (val info = format.paramInfo) {
is RangedParamInfo -> " (${info.format(formatParam)})"
val prefix = when (val info = format.paramInfo) {
is RangedParamInfo -> "${info.format(formatParam)}, "
NoParamInfo -> ""
}
val sampleRate = SampleRates.format(context, SampleRates.fromPreferences(context))

prefOutputFormat.summary = "${summary}\n\n${format.name}${suffix}"
prefOutputFormat.summary = "${summary}\n\n${format.name} (${prefix}${sampleRate})"
}

private fun refreshInhibitBatteryOptState() {
Expand Down Expand Up @@ -193,7 +196,7 @@ class SettingsActivity : AppCompatActivity() {
}
}
// Update the output format state when it's changed by the bottom sheet
Preferences.isFormatKey(key) -> {
Preferences.isFormatKey(key) || key == Preferences.PREF_SAMPLE_RATE -> {
refreshOutputFormat()
}
}
Expand Down
47 changes: 47 additions & 0 deletions app/src/main/java/com/chiller3/bcr/format/SampleRates.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
@file:OptIn(ExperimentalUnsignedTypes::class)

package com.chiller3.bcr.format

import android.content.Context
import com.chiller3.bcr.Preferences
import com.chiller3.bcr.R

object SampleRates {
/**
* Hardcoded list of sample rates supported by every [Format].
*
* Ideally, there would be a way to query what sample rates are supported for a given audio
* source and then filter that list based on what the [Format] supports. Unfortunately, no such
* API exists.
*/
val all = uintArrayOf(
8_000u,
12_000u,
16_000u,
24_000u,
48_000u,
)

/**
* Get the saved sample rate from the preferences.
*
* If the saved sample rate is no longer valid, then null is returned.
*/
fun fromPreferences(context: Context): UInt? {
val savedSampleRate = Preferences.getSampleRate(context)

if (savedSampleRate != null && all.contains(savedSampleRate)) {
return savedSampleRate
}

return null
}

fun format(context: Context, sampleRate: UInt?): String =
if (sampleRate == null) {
context.getString(R.string.bottom_sheet_sample_rate_native)
} else {
"$sampleRate Hz"
}
}
Loading

0 comments on commit 23d4dbc

Please sign in to comment.