From bcaea201690da7e7439f52781da04b888b80f9c5 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson <chillermillerlong@hotmail.com> Date: Mon, 30 May 2022 19:48:11 -0400 Subject: [PATCH 1/3] Add support for configurable sample rate Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com> --- .../chiller3/bcr/FormatBottomSheetFragment.kt | 51 ++++++++++++++++--- .../main/java/com/chiller3/bcr/Preferences.kt | 41 +++++++++++++++ .../java/com/chiller3/bcr/RecorderThread.kt | 14 +++-- .../java/com/chiller3/bcr/SettingsActivity.kt | 13 +++-- .../com/chiller3/bcr/format/SampleRates.kt | 46 +++++++++++++++++ .../main/res/layout/format_bottom_sheet.xml | 14 +++++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/chiller3/bcr/format/SampleRates.kt diff --git a/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt b/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt index a89de929d..854e181e2 100644 --- a/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt +++ b/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt @@ -1,3 +1,6 @@ +@file:Suppress("OPT_IN_IS_NOT_ENABLED") +@file:OptIn(ExperimentalUnsignedTypes::class) + package com.chiller3.bcr import android.os.Bundle @@ -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!! @@ -31,9 +32,16 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(), ): View { _binding = FormatBottomSheetBinding.inflate(inflater, container, false) - binding.paramSlider.setLabelFormatter(this) + binding.paramSlider.setLabelFormatter { + formatParamInfo.format(it.toUInt()) + } binding.paramSlider.addOnChangeListener(this) + binding.sampleRateSlider.setLabelFormatter { + SampleRates.format(sampleRateFromIndex(it.toInt())) + } + binding.sampleRateSlider.addOnChangeListener(this) + binding.reset.setOnClickListener(this) for (format in Formats.all) { @@ -55,6 +63,7 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(), binding.nameGroup.setOnCheckedStateChangeListener(this) refreshFormat() + refreshSampleRate() return binding.root } @@ -109,35 +118,61 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(), } } + private fun refreshSampleRate() { + val sampleRate = SampleRates.fromPreferences(requireContext()) + + // Index == SampleRates.all.size is used to represent the native sample rate option + binding.sampleRateSlider.valueFrom = 0f + binding.sampleRateSlider.valueTo = SampleRates.all.size.toFloat() + binding.sampleRateSlider.stepSize = 1f + + if (sampleRate == null) { + binding.sampleRateSlider.value = SampleRates.all.size.toFloat() + } else { + binding.sampleRateSlider.value = SampleRates.all.indexOf(sampleRate).toFloat() + } + } + override fun onCheckedChanged(group: ChipGroup, checkedIds: MutableList<Int>) { Preferences.setFormatName(requireContext(), chipIdToFormat[checkedIds.first()]!!.name) refreshParam() } - override fun getFormattedValue(value: Float): String = - formatParamInfo.format(value.toUInt()) - override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { when (slider) { binding.paramSlider -> { val format = chipIdToFormat[binding.nameGroup.checkedChipId]!! Preferences.setFormatParam(requireContext(), format.name, value.toUInt()) } + binding.sampleRateSlider -> { + val sampleRate = sampleRateFromIndex(value.toInt()) + Preferences.setSampleRate(requireContext(), sampleRate) + } } } 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() } } } companion object { val TAG: String = FormatBottomSheetFragment::class.java.simpleName + + private fun sampleRateFromIndex(index: Int) = + if (index == SampleRates.all.size) { + null + } else { + SampleRates.all[index] + } } } \ No newline at end of file diff --git a/app/src/main/java/com/chiller3/bcr/Preferences.kt b/app/src/main/java/com/chiller3/bcr/Preferences.kt index f27100ccf..c1274b32f 100644 --- a/app/src/main/java/com/chiller3/bcr/Preferences.kt +++ b/app/src/main/java/com/chiller3/bcr/Preferences.kt @@ -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 @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/chiller3/bcr/RecorderThread.kt b/app/src/main/java/com/chiller3/bcr/RecorderThread.kt index e5e22b9b6..8b1e86cb1 100644 --- a/app/src/main/java/com/chiller3/bcr/RecorderThread.kt +++ b/app/src/main/java/com/chiller3/bcr/RecorderThread.kt @@ -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 @@ -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") @@ -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) diff --git a/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt b/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt index edd6b41e9..281a83ce2 100644 --- a/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt +++ b/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt @@ -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?) { @@ -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(SampleRates.fromPreferences(context)) - prefOutputFormat.summary = "${summary}\n\n${format.name}${suffix}" + prefOutputFormat.summary = "${summary}\n\n${format.name} (${prefix}${sampleRate})" } private fun refreshInhibitBatteryOptState() { @@ -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() } } diff --git a/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt b/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt new file mode 100644 index 000000000..3c7af22ae --- /dev/null +++ b/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt @@ -0,0 +1,46 @@ +@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 + +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(sampleRate: UInt?): String = + if (sampleRate == null) { + "native" + } else { + "$sampleRate Hz" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/format_bottom_sheet.xml b/app/src/main/res/layout/format_bottom_sheet.xml index f28479448..81e11e636 100644 --- a/app/src/main/res/layout/format_bottom_sheet.xml +++ b/app/src/main/res/layout/format_bottom_sheet.xml @@ -42,6 +42,20 @@ app:labelBehavior="visible" /> </LinearLayout> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/bottom_sheet_section_separation" + android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom" + android:text="@string/bottom_sheet_sample_rate" + android:textAppearance="?attr/textAppearanceHeadline6" /> + + <com.google.android.material.slider.Slider + android:id="@+id/sample_rate_slider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:labelBehavior="visible" /> + <com.google.android.material.button.MaterialButton android:id="@+id/reset" android:layout_width="wrap_content" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cda8c0d3c..6502b06a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ <string name="bottom_sheet_output_format">Output format</string> <string name="bottom_sheet_compression_level">Compression level</string> <string name="bottom_sheet_bitrate">Bitrate</string> + <string name="bottom_sheet_sample_rate">Sample rate</string> <string name="bottom_sheet_reset">Reset to defaults</string> <!-- Notifications --> From b65727cf562debc8a1ce4095f125ada1b265cf9f Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson <chillermillerlong@hotmail.com> Date: Mon, 30 May 2022 20:13:54 -0400 Subject: [PATCH 2/3] Use chips for sample rate instead of a second slider Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com> --- .../chiller3/bcr/FormatBottomSheetFragment.kt | 91 +++++++++------- .../java/com/chiller3/bcr/SettingsActivity.kt | 2 +- .../com/chiller3/bcr/format/SampleRates.kt | 5 +- .../main/res/layout/format_bottom_sheet.xml | 100 ++++++++++-------- app/src/main/res/values/strings.xml | 1 + 5 files changed, 108 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt b/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt index 854e181e2..ac92cc331 100644 --- a/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt +++ b/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt @@ -25,6 +25,9 @@ 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?, @@ -37,11 +40,6 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(), } binding.paramSlider.addOnChangeListener(this) - binding.sampleRateSlider.setLabelFormatter { - SampleRates.format(sampleRateFromIndex(it.toInt())) - } - binding.sampleRateSlider.addOnChangeListener(this) - binding.reset.setOnClickListener(this) for (format in Formats.all) { @@ -49,19 +47,18 @@ 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() @@ -73,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. * @@ -83,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. */ @@ -118,36 +144,26 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(), } } - private fun refreshSampleRate() { - val sampleRate = SampleRates.fromPreferences(requireContext()) - - // Index == SampleRates.all.size is used to represent the native sample rate option - binding.sampleRateSlider.valueFrom = 0f - binding.sampleRateSlider.valueTo = SampleRates.all.size.toFloat() - binding.sampleRateSlider.stepSize = 1f + override fun onCheckedChanged(group: ChipGroup, checkedIds: MutableList<Int>) { + val context = requireContext() - if (sampleRate == null) { - binding.sampleRateSlider.value = SampleRates.all.size.toFloat() - } else { - binding.sampleRateSlider.value = SampleRates.all.indexOf(sampleRate).toFloat() + when (group) { + binding.nameGroup -> { + Preferences.setFormatName(context, chipIdToFormat[checkedIds.first()]!!.name) + refreshParam() + } + binding.sampleRateGroup -> { + Preferences.setSampleRate(context, chipIdToSampleRate[checkedIds.first()]) + } } } - override fun onCheckedChanged(group: ChipGroup, checkedIds: MutableList<Int>) { - Preferences.setFormatName(requireContext(), chipIdToFormat[checkedIds.first()]!!.name) - refreshParam() - } - override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { when (slider) { binding.paramSlider -> { val format = chipIdToFormat[binding.nameGroup.checkedChipId]!! Preferences.setFormatParam(requireContext(), format.name, value.toUInt()) } - binding.sampleRateSlider -> { - val sampleRate = sampleRateFromIndex(value.toInt()) - Preferences.setSampleRate(requireContext(), sampleRate) - } } } @@ -167,12 +183,5 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(), companion object { val TAG: String = FormatBottomSheetFragment::class.java.simpleName - - private fun sampleRateFromIndex(index: Int) = - if (index == SampleRates.all.size) { - null - } else { - SampleRates.all[index] - } } } \ No newline at end of file diff --git a/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt b/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt index 281a83ce2..08871a895 100644 --- a/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt +++ b/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt @@ -118,7 +118,7 @@ class SettingsActivity : AppCompatActivity() { is RangedParamInfo -> "${info.format(formatParam)}, " NoParamInfo -> "" } - val sampleRate = SampleRates.format(SampleRates.fromPreferences(context)) + val sampleRate = SampleRates.format(context, SampleRates.fromPreferences(context)) prefOutputFormat.summary = "${summary}\n\n${format.name} (${prefix}${sampleRate})" } diff --git a/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt b/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt index 3c7af22ae..2ff4a8251 100644 --- a/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt +++ b/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt @@ -5,6 +5,7 @@ package com.chiller3.bcr.format import android.content.Context import com.chiller3.bcr.Preferences +import com.chiller3.bcr.R object SampleRates { /** @@ -37,9 +38,9 @@ object SampleRates { return null } - fun format(sampleRate: UInt?): String = + fun format(context: Context, sampleRate: UInt?): String = if (sampleRate == null) { - "native" + context.getString(R.string.bottom_sheet_sample_rate_native) } else { "$sampleRate Hz" } diff --git a/app/src/main/res/layout/format_bottom_sheet.xml b/app/src/main/res/layout/format_bottom_sheet.xml index 81e11e636..dd476a745 100644 --- a/app/src/main/res/layout/format_bottom_sheet.xml +++ b/app/src/main/res/layout/format_bottom_sheet.xml @@ -1,66 +1,72 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:gravity="center_horizontal" - android:padding="@dimen/bottom_sheet_overall_padding"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom" - android:text="@string/bottom_sheet_output_format" - android:textAppearance="?attr/textAppearanceHeadline6" /> - - <com.chiller3.bcr.ChipGroupCentered - android:id="@+id/name_group" - android:layout_width="match_parent" - android:layout_height="wrap_content" - app:selectionRequired="true" - app:singleSelection="true" /> + android:layout_height="wrap_content"> <LinearLayout - android:id="@+id/param_group" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:gravity="center_horizontal"> + android:gravity="center_horizontal" + android:padding="@dimen/bottom_sheet_overall_padding"> + <TextView - android:id="@+id/param_title" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/bottom_sheet_section_separation" android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom" + android:text="@string/bottom_sheet_output_format" android:textAppearance="?attr/textAppearanceHeadline6" /> - <com.google.android.material.slider.Slider - android:id="@+id/param_slider" + <com.chiller3.bcr.ChipGroupCentered + android:id="@+id/name_group" android:layout_width="match_parent" android:layout_height="wrap_content" - app:labelBehavior="visible" /> - </LinearLayout> + app:selectionRequired="true" + app:singleSelection="true" /> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/bottom_sheet_section_separation" - android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom" - android:text="@string/bottom_sheet_sample_rate" - android:textAppearance="?attr/textAppearanceHeadline6" /> + <LinearLayout + android:id="@+id/param_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal"> + <TextView + android:id="@+id/param_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/bottom_sheet_section_separation" + android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom" + android:textAppearance="?attr/textAppearanceHeadline6" /> - <com.google.android.material.slider.Slider - android:id="@+id/sample_rate_slider" - android:layout_width="match_parent" - android:layout_height="wrap_content" - app:labelBehavior="visible" /> + <com.google.android.material.slider.Slider + android:id="@+id/param_slider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:labelBehavior="visible" /> + </LinearLayout> - <com.google.android.material.button.MaterialButton - android:id="@+id/reset" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/bottom_sheet_section_separation" - android:text="@string/bottom_sheet_reset" - style="?attr/materialButtonOutlinedStyle" /> -</LinearLayout> \ No newline at end of file + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/bottom_sheet_section_separation" + android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom" + android:text="@string/bottom_sheet_sample_rate" + android:textAppearance="?attr/textAppearanceHeadline6" /> + + <com.chiller3.bcr.ChipGroupCentered + android:id="@+id/sample_rate_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:selectionRequired="true" + app:singleSelection="true" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/reset" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/bottom_sheet_section_separation" + android:text="@string/bottom_sheet_reset" + style="?attr/materialButtonOutlinedStyle" /> + </LinearLayout> +</androidx.core.widget.NestedScrollView> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6502b06a4..d9278a5db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ <string name="bottom_sheet_compression_level">Compression level</string> <string name="bottom_sheet_bitrate">Bitrate</string> <string name="bottom_sheet_sample_rate">Sample rate</string> + <string name="bottom_sheet_sample_rate_native">Native sample rate</string> <string name="bottom_sheet_reset">Reset to defaults</string> <!-- Notifications --> From 057a5fef49a8f4f447ba1f4ee767878287d8ce36 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson <chillermillerlong@hotmail.com> Date: Mon, 30 May 2022 20:15:36 -0400 Subject: [PATCH 3/3] changelog.txt: Add entry for PR #56 Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com> --- app/magisk/updates/release/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/magisk/updates/release/changelog.txt b/app/magisk/updates/release/changelog.txt index 9bef52e8c..3cd648379 100644 --- a/app/magisk/updates/release/changelog.txt +++ b/app/magisk/updates/release/changelog.txt @@ -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