Triple-tap on app title opens settings; add mic gain slider (100-500%)
This commit is contained in:
@@ -67,6 +67,8 @@ class MainActivity : Activity(), SvxService.Listener {
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var titleHoldRunnable: Runnable? = null
|
||||
private var titleResetRunnable: Runnable? = null
|
||||
private var titleTaps = 0
|
||||
|
||||
private data class TalkLogEntry(val time: Long, val callsign: String, val tg: Long)
|
||||
|
||||
@@ -164,18 +166,36 @@ class MainActivity : Activity(), SvxService.Listener {
|
||||
|
||||
lblTalker.setOnClickListener { showTalkLogDialog() }
|
||||
|
||||
// Удержание на названии программы (> 2 с) — выбор профиля настроек
|
||||
// Удержание на названии программы (> 2 с) — выбор профиля настроек;
|
||||
// короткие тапы считаем: тройной тап — настройки
|
||||
lblAppTitle.setOnTouchListener { _, event ->
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
val r = Runnable { showProfileDialog() }
|
||||
val r = Runnable {
|
||||
titleHoldRunnable = null
|
||||
showProfileDialog()
|
||||
}
|
||||
titleHoldRunnable = r
|
||||
mainHandler.postDelayed(r, 2000)
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
val wasHold = titleHoldRunnable == null
|
||||
titleHoldRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||
titleHoldRunnable = null
|
||||
if (!wasHold) {
|
||||
titleTaps++
|
||||
// Скользящее окно: сброс от последнего тапа
|
||||
titleResetRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||
val reset = Runnable { titleTaps = 0 }
|
||||
titleResetRunnable = reset
|
||||
mainHandler.postDelayed(reset, 1200)
|
||||
if (titleTaps >= 3) {
|
||||
titleTaps = 0
|
||||
titleResetRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> true
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -45,6 +46,23 @@ 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(
|
||||
@@ -95,6 +113,7 @@ 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()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import ru.ua1zbe.svxremote.audio.OpusEngine
|
||||
import ru.ua1zbe.svxremote.prefs.Prefs
|
||||
import ru.ua1zbe.svxremote.proto.MsgParser
|
||||
import ru.ua1zbe.svxremote.proto.ReflectorClient
|
||||
|
||||
@@ -99,7 +100,7 @@ class SvxService : Service(), ReflectorClient.Callbacks {
|
||||
}
|
||||
|
||||
fun startTx() {
|
||||
engine.startCapture { packet -> client.sendAudio(packet) }
|
||||
engine.startCapture(Prefs.micGain(this)) { packet -> client.sendAudio(packet) }
|
||||
}
|
||||
|
||||
fun stopTx() {
|
||||
|
||||
@@ -58,10 +58,13 @@ class OpusEngine(
|
||||
private var dbgOut = 0
|
||||
private var playStarted = false
|
||||
private var gateStart = 0L
|
||||
private val jitterTarget = 6 // ≈ 120 мс накапливаем до первого слива
|
||||
private val jitterTarget = 10 // ≈ 200 мс накапливаем до первого слива
|
||||
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()
|
||||
@@ -88,7 +91,7 @@ class OpusEngine(
|
||||
OUT_SAMPLE_RATE,
|
||||
AudioFormat.CHANNEL_OUT_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
maxOf(minBuf, 20000), // ~208 мс — буфер AudioTrack и есть джиттер-буфер
|
||||
maxOf(minBuf, rxBufferBytes), // ~417 мс — джиттер-буфер AudioTrack
|
||||
AudioTrack.MODE_STREAM
|
||||
) // play() включаем на первом выходном кадре — буфер успевает накопиться
|
||||
} catch (e: Exception) {
|
||||
@@ -282,11 +285,11 @@ class OpusEngine(
|
||||
decoder?.let { feedDecoder(it, idx, rxQueue.poll() ?: packet) }
|
||||
}
|
||||
|
||||
/** Джиттер-гейт открыт: накопили кадры или вышло 80 мс с первого пакета. */
|
||||
/** Джиттер-гейт открыт: накопили кадры или вышло 150 мс с первого пакета. */
|
||||
private fun gateOpen(): Boolean {
|
||||
if (playStarted) return true
|
||||
if (rxQueue.size >= jitterTarget) return true
|
||||
return gateStart != 0L && System.currentTimeMillis() - gateStart >= 80
|
||||
return gateStart != 0L && System.currentTimeMillis() - gateStart >= 150
|
||||
}
|
||||
|
||||
fun onAudioFlush() {
|
||||
@@ -394,7 +397,7 @@ class OpusEngine(
|
||||
// ---------- Захват (TX) ----------
|
||||
|
||||
/** Начать PTT: запустить запись и кодирование. */
|
||||
fun startCapture(onPacket: (ByteArray) -> Unit) {
|
||||
fun startCapture(micGain: Float = 1.0f, onPacket: (ByteArray) -> Unit) {
|
||||
if (encoding) return
|
||||
val enc = createEncoder()
|
||||
encoder = enc
|
||||
@@ -417,7 +420,7 @@ class OpusEngine(
|
||||
rec.startRecording()
|
||||
encoding = true
|
||||
val t = Thread({
|
||||
captureLoop(enc, rec, onPacket)
|
||||
captureLoop(enc, rec, micGain, onPacket)
|
||||
}, "svx-capture")
|
||||
t.isDaemon = true
|
||||
recThread = t
|
||||
@@ -441,7 +444,7 @@ class OpusEngine(
|
||||
}
|
||||
}
|
||||
|
||||
private fun captureLoop(enc: MediaCodec, rec: AudioRecord, onPacket: (ByteArray) -> Unit) {
|
||||
private fun captureLoop(enc: MediaCodec, rec: AudioRecord, micGain: Float, onPacket: (ByteArray) -> Unit) {
|
||||
val pcmBuf = ByteBuffer.allocateDirect(FRAME_SIZE * 2).order(ByteOrder.LITTLE_ENDIAN)
|
||||
val micLevel = FloatArray(1)
|
||||
while (encoding) {
|
||||
@@ -451,6 +454,14 @@ class OpusEngine(
|
||||
pcmBuf.limit(n)
|
||||
val pcm = ByteArray(n)
|
||||
pcmBuf.get(pcm)
|
||||
if (micGain != 1.0f) {
|
||||
for (i in 0 until n step 2) {
|
||||
val s = ((pcm[i + 1].toInt() shl 8) or (pcm[i].toInt() and 0xFF)).toShort()
|
||||
val g = (s * micGain).toInt().coerceIn(-32768, 32767)
|
||||
pcm[i] = (g and 0xFF).toByte()
|
||||
pcm[i + 1] = ((g shr 8) and 0xFF).toByte()
|
||||
}
|
||||
}
|
||||
micLevel[0] = pcmLevel(pcm)
|
||||
onLevel(SRC_MIC, micLevel[0])
|
||||
try {
|
||||
|
||||
@@ -136,4 +136,12 @@ object Prefs {
|
||||
fun setAprsCoords(context: Context, coords: String) {
|
||||
sp(context).edit().putString("aprs_coords", coords.trim()).apply()
|
||||
}
|
||||
|
||||
/** Усиление микрофона (1.0 = без усиления). */
|
||||
fun micGain(context: Context): Float =
|
||||
sp(context).getFloat("mic_gain", 1.0f)
|
||||
|
||||
fun setMicGain(context: Context, gain: Float) {
|
||||
sp(context).edit().putFloat("mic_gain", gain.coerceIn(0.5f, 5.0f)).apply()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,46 @@
|
||||
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,6 +19,8 @@
|
||||
<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="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