Separate hidden audio settings screen (mic gain, RX buffer, jitter) via triple-tap; remove from main settings
This commit is contained in:
@@ -25,6 +25,12 @@
|
||||
android:exported="false"
|
||||
android:label="@string/settings_title" />
|
||||
|
||||
<activity
|
||||
android:name=".AudioSettingsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/audio_settings_title"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package ru.ua1zbe.svxremote
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import ru.ua1zbe.svxremote.prefs.Prefs
|
||||
|
||||
/**
|
||||
* Скрытые настройки звука (тройной тап по названию программы на главном
|
||||
* экране): усиление микрофона, буфер воспроизведения (джиттер), накопление
|
||||
* перед стартом. Применяются при следующем подключении/передаче.
|
||||
*/
|
||||
class AudioSettingsActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_audio_settings)
|
||||
|
||||
val seekMicGain = findViewById<SeekBar>(R.id.seek_mic_gain)
|
||||
val lblMicGain = findViewById<TextView>(R.id.lbl_mic_gain_value)
|
||||
val seekRxBuffer = findViewById<SeekBar>(R.id.seek_rx_buffer)
|
||||
val lblRxBuffer = findViewById<TextView>(R.id.lbl_rx_buffer_value)
|
||||
val seekJitter = findViewById<SeekBar>(R.id.seek_jitter)
|
||||
val lblJitter = findViewById<TextView>(R.id.lbl_jitter_value)
|
||||
|
||||
fun updateMicGainLabel() {
|
||||
lblMicGain.text = "${100 + seekMicGain.progress * 10}%"
|
||||
}
|
||||
|
||||
fun updateRxBufferLabel() {
|
||||
lblRxBuffer.text = "${100 + seekRxBuffer.progress * 20} мс"
|
||||
}
|
||||
|
||||
fun updateJitterLabel() {
|
||||
lblJitter.text = "${seekJitter.progress * 20} мс"
|
||||
}
|
||||
|
||||
seekMicGain.progress = ((Prefs.micGain(this) - 1.0f) * 10).toInt().coerceIn(0, 40)
|
||||
seekRxBuffer.progress = ((Prefs.rxBufferMs(this) - 100) / 20).coerceIn(0, 25)
|
||||
seekJitter.progress = (Prefs.rxJitterMs(this) / 20).coerceIn(0, 20)
|
||||
updateMicGainLabel()
|
||||
updateRxBufferLabel()
|
||||
updateJitterLabel()
|
||||
|
||||
val listener = object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
updateMicGainLabel()
|
||||
updateRxBufferLabel()
|
||||
updateJitterLabel()
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
}
|
||||
seekMicGain.setOnSeekBarChangeListener(listener)
|
||||
seekRxBuffer.setOnSeekBarChangeListener(listener)
|
||||
seekJitter.setOnSeekBarChangeListener(listener)
|
||||
|
||||
findViewById<Button>(R.id.btn_audio_save).setOnClickListener {
|
||||
Prefs.setMicGain(this, 1.0f + seekMicGain.progress * 0.1f)
|
||||
Prefs.setRxBufferMs(this, 100 + seekRxBuffer.progress * 20)
|
||||
Prefs.setRxJitterMs(this, seekJitter.progress * 20)
|
||||
Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +193,7 @@ class MainActivity : Activity(), SvxService.Listener {
|
||||
if (titleTaps >= 3) {
|
||||
titleTaps = 0
|
||||
titleResetRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
startActivity(Intent(this, AudioSettingsActivity::class.java))
|
||||
}
|
||||
}
|
||||
true
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import ru.ua1zbe.svxremote.prefs.ConnectionSettings
|
||||
@@ -46,23 +45,6 @@ class SettingsActivity : Activity() {
|
||||
findViewById<TextView>(R.id.lbl_version).text =
|
||||
getString(R.string.app_version_fmt, BuildConfig.VERSION_NAME)
|
||||
|
||||
val seekMicGain = findViewById<SeekBar>(R.id.seek_mic_gain)
|
||||
val lblMicGain = findViewById<TextView>(R.id.lbl_mic_gain_value)
|
||||
fun updateMicGainLabel() {
|
||||
lblMicGain.text = "${100 + seekMicGain.progress * 10}%"
|
||||
}
|
||||
seekMicGain.progress =
|
||||
((Prefs.micGain(this) - 1.0f) * 10).toInt().coerceIn(0, 40)
|
||||
updateMicGainLabel()
|
||||
seekMicGain.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
updateMicGainLabel()
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
|
||||
val profileNames = (1..Prefs.PROFILE_COUNT)
|
||||
.map { getString(R.string.profile_name, it) }
|
||||
spinnerProfile.adapter = ArrayAdapter(
|
||||
@@ -113,7 +95,6 @@ class SettingsActivity : Activity() {
|
||||
Prefs.setActiveProfile(this, currentProfile)
|
||||
Prefs.setNodeToastsEnabled(this, chkNodeToasts.isChecked)
|
||||
Prefs.setAprsCoords(this, inputAprsCoords.text.toString())
|
||||
Prefs.setMicGain(this, 1.0f + seekMicGain.progress * 0.1f)
|
||||
toast(getString(R.string.saved))
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -119,6 +119,8 @@ class SvxService : Service(), ReflectorClient.Callbacks {
|
||||
when (state) {
|
||||
ReflectorClient.STATE_READY -> {
|
||||
updateNotification(getString(R.string.notif_connected))
|
||||
engine.rxBufferMs = Prefs.rxBufferMs(this)
|
||||
engine.jitterFrames = (Prefs.rxJitterMs(this) / 20).coerceAtLeast(1)
|
||||
engine.startDecoder()
|
||||
}
|
||||
ReflectorClient.STATE_DISCONNECTED -> {
|
||||
|
||||
@@ -58,13 +58,14 @@ class OpusEngine(
|
||||
private var dbgOut = 0
|
||||
private var playStarted = false
|
||||
private var gateStart = 0L
|
||||
private val jitterTarget = 10 // ≈ 200 мс накапливаем до первого слива
|
||||
// Настраивается из скрытых настроек звука (AudioSettingsActivity)
|
||||
@Volatile
|
||||
var rxBufferMs = 417 // буфер AudioTrack, мс (40000 байт ≈ 417 мс при 48 кГц)
|
||||
@Volatile
|
||||
var jitterFrames = 10 // сколько кадров накопить до первого слива (кадр = 20 мс)
|
||||
private val configBuffers = ArrayList<ByteArray>()
|
||||
private var configIdx = 0
|
||||
|
||||
/** Минимальный размер буфера AudioTrack (байт). 40000 ≈ 417 мс при 48 кГц. */
|
||||
private val rxBufferBytes = 40_000
|
||||
|
||||
fun startDecoder() {
|
||||
if (decoder != null) return
|
||||
val format = MediaFormat()
|
||||
@@ -91,7 +92,7 @@ class OpusEngine(
|
||||
OUT_SAMPLE_RATE,
|
||||
AudioFormat.CHANNEL_OUT_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
maxOf(minBuf, rxBufferBytes), // ~417 мс — джиттер-буфер AudioTrack
|
||||
maxOf(minBuf, rxBufferMs * 96), // 96 байт/мс при 48 кГц моно 16 бит
|
||||
AudioTrack.MODE_STREAM
|
||||
) // play() включаем на первом выходном кадре — буфер успевает накопиться
|
||||
} catch (e: Exception) {
|
||||
@@ -288,7 +289,7 @@ class OpusEngine(
|
||||
/** Джиттер-гейт открыт: накопили кадры или вышло 150 мс с первого пакета. */
|
||||
private fun gateOpen(): Boolean {
|
||||
if (playStarted) return true
|
||||
if (rxQueue.size >= jitterTarget) return true
|
||||
if (rxQueue.size >= jitterFrames) return true
|
||||
return gateStart != 0L && System.currentTimeMillis() - gateStart >= 150
|
||||
}
|
||||
|
||||
|
||||
@@ -144,4 +144,22 @@ object Prefs {
|
||||
fun setMicGain(context: Context, gain: Float) {
|
||||
sp(context).edit().putFloat("mic_gain", gain.coerceIn(0.5f, 5.0f)).apply()
|
||||
}
|
||||
|
||||
// ---------- Скрытые настройки звука (AudioSettingsActivity) ----------
|
||||
|
||||
/** Буфер воспроизведения RX, мс. */
|
||||
fun rxBufferMs(context: Context): Int =
|
||||
sp(context).getInt("rx_buffer_ms", 417)
|
||||
|
||||
fun setRxBufferMs(context: Context, ms: Int) {
|
||||
sp(context).edit().putInt("rx_buffer_ms", ms.coerceIn(100, 600)).apply()
|
||||
}
|
||||
|
||||
/** Накопление перед стартом воспроизведения, мс (кратно 20). */
|
||||
fun rxJitterMs(context: Context): Int =
|
||||
sp(context).getInt("rx_jitter_ms", 200)
|
||||
|
||||
fun setRxJitterMs(context: Context, ms: Int) {
|
||||
sp(context).edit().putInt("rx_jitter_ms", ms.coerceIn(0, 400)).apply()
|
||||
}
|
||||
}
|
||||
|
||||
127
app/src/main/res/layout/activity_audio_settings.xml
Normal file
127
app/src/main/res/layout/activity_audio_settings.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bg">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/audio_settings_title"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/audio_settings_hint"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp"
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/audio_mic_gain"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lbl_mic_gain_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek_mic_gain"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="40"
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/audio_rx_buffer"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lbl_rx_buffer_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek_rx_buffer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="25"
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/audio_jitter"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lbl_jitter_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek_jitter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="20"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_audio_save"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:text="@string/btn_save"
|
||||
android:textColor="#0D1117"
|
||||
android:background="@drawable/bg_primary_btn"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -202,46 +202,6 @@
|
||||
android:padding="12dp"
|
||||
android:hint="@string/pref_aprs_coords_hint" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pref_audio_label"
|
||||
android:textColor="@color/accent"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/pref_mic_gain"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lbl_mic_gain_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100%"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek_mic_gain"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="40"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_save"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
<string name="pref_aprs_hint">Экспериментально: принимает метеостанции в радиусе 250 км. Последняя станция показывается вместо строки сервера на главном экране.</string>
|
||||
<string name="pref_aprs_coords">Координаты (широта, долгота)</string>
|
||||
<string name="pref_aprs_coords_hint">пусто = центр Санкт-Петербурга, например 59.95, 30.30</string>
|
||||
<string name="pref_audio_label">Звук</string>
|
||||
<string name="pref_mic_gain">Усиление микрофона</string>
|
||||
<string name="audio_settings_title">Настройки звука</string>
|
||||
<string name="audio_settings_hint">Скрытые настройки: применяются при следующем подключении/передаче.</string>
|
||||
<string name="audio_mic_gain">Усиление микрофона</string>
|
||||
<string name="audio_rx_buffer">Буфер воспроизведения</string>
|
||||
<string name="audio_jitter">Накопление перед стартом</string>
|
||||
<string name="toast_node_joined">Подключился: %1$s%2$s</string>
|
||||
<string name="toast_node_left">Отключился: %1$s%2$s</string>
|
||||
<string name="node_tg_suffix"> (группа %1$d)</string>
|
||||
|
||||
Reference in New Issue
Block a user