diff --git a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt
index d165d38..463ee1a 100644
--- a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt
+++ b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt
@@ -35,6 +35,7 @@ import ru.ua1zbe.svxremote.prefs.Prefs
import ru.ua1zbe.svxremote.proto.MsgParser
import ru.ua1zbe.svxremote.proto.ReflectorClient
import ru.ua1zbe.svxremote.ui.PttButton
+import ru.ua1zbe.svxremote.ui.SoundMeterPreviewView
private const val REQ_MIC = 1001
private const val REQ_NOTIF = 1002
@@ -166,6 +167,12 @@ class MainActivity : Activity(), SvxService.Listener {
lblTalker.setOnClickListener { showTalkLogDialog() }
+ // Удержание на надписи «На связи» — демо-окно индикаторов звука
+ lblTxState.setOnLongClickListener {
+ showSoundMetersPreview()
+ true
+ }
+
// Удержание на названии программы (> 2 с) — выбор профиля настроек;
// короткие тапы считаем: тройной тап — настройки
lblAppTitle.setOnTouchListener { _, event ->
@@ -659,6 +666,16 @@ class MainActivity : Activity(), SvxService.Listener {
.show()
}
+ /** Демо-окно с примерами индикаторов звука (удержание на «На связи»). */
+ private fun showSoundMetersPreview() {
+ AlertDialog.Builder(this)
+ .setTitle(R.string.meter_preview_title)
+ .setMessage(R.string.meter_preview_hint)
+ .setView(SoundMeterPreviewView(this))
+ .setPositiveButton(R.string.close, null)
+ .show()
+ }
+
private fun showGroupsDialog() {
val groups = (settings.groups + settings.talkGroup).distinct()
if (groups.isEmpty()) {
diff --git a/app/src/main/java/ru/ua1zbe/svxremote/ui/SoundMeterPreviewView.kt b/app/src/main/java/ru/ua1zbe/svxremote/ui/SoundMeterPreviewView.kt
new file mode 100644
index 0000000..9bfd1ae
--- /dev/null
+++ b/app/src/main/java/ru/ua1zbe/svxremote/ui/SoundMeterPreviewView.kt
@@ -0,0 +1,156 @@
+package ru.ua1zbe.svxremote.ui
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.os.Handler
+import android.os.Looper
+import android.util.AttributeSet
+import android.view.View
+import kotlin.random.Random
+
+/**
+ * Временное демо-окно с примерами индикаторов звука (S-метр, клиппинг,
+ * джиттер/потери). Открывается удержанием на надписи «На связи».
+ * Уровни симулируются, чтобы было видно, как индикаторы работают.
+ */
+class SoundMeterPreviewView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : View(context, attrs) {
+
+ private val bg = Paint().apply { color = 0xFF0D1117.toInt() }
+ private val track = Paint().apply { color = 0xFF21262D.toInt() }
+ private val fill = Paint().apply { color = 0xFF58A6FF.toInt() }
+ private val fillHot = Paint().apply { color = 0xFFF85149.toInt() }
+ private val peakPaint = Paint().apply { color = 0xFFFFFFFF.toInt(); strokeWidth = dp(2) }
+ private val tick = Paint().apply { color = 0xFF8B949E.toInt(); strokeWidth = dp(1) }
+ private val label = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = 0xFF8B949E.toInt()
+ textSize = sp(11)
+ }
+ private val labelSmall = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = 0xFF8B949E.toInt()
+ textSize = sp(9)
+ }
+ private val clipDot = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = 0xFFF85149.toInt()
+ }
+ private val clipDotOff = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = 0xFF30363D.toInt()
+ }
+
+ private val handler = Handler(Looper.getMainLooper())
+ private var simLevel = 0.0f
+ private var simPeak = 0.0f
+ private var simJitter = 30.0f
+ private var simLoss = 0.1f
+ private var t = 0.0
+
+ private val anim = object : Runnable {
+ override fun run() {
+ t += 0.08
+ val wave = ((Math.sin(t) + Math.sin(t * 2.3)) / 2 + 1) / 2
+ simLevel = (wave * 0.62 + Random.nextFloat() * 0.12).toFloat()
+ // Периодически «перегруз» — клиппинг и пики
+ if (Random.nextFloat() > 0.92f) simLevel = 0.98f
+ simPeak = maxOf(simLevel, simPeak - 0.02f)
+ simJitter = 25 + Random.nextFloat() * 120
+ simLoss = (Random.nextFloat() * 0.5f).coerceAtLeast(0.03f)
+ invalidate()
+ handler.postDelayed(this, 50)
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ handler.post(anim)
+ }
+
+ override fun onDetachedFromWindow() {
+ handler.removeCallbacksAndMessages(null)
+ super.onDetachedFromWindow()
+ }
+
+ private fun dp(v: Int): Float = v * resources.displayMetrics.density
+ private fun sp(v: Int): Float = v * resources.displayMetrics.scaledDensity
+
+ override fun onDraw(canvas: Canvas) {
+ val w = width.toFloat()
+ val h = height.toFloat()
+ canvas.drawRect(0f, 0f, w, h, bg)
+
+ // --- S-метр ---
+ var y = dp(18)
+ canvas.drawText("S-метр (peak-hold)", dp(16), y, label)
+ y += dp(8)
+ drawSMeter(canvas, w, y)
+
+ // --- Уровень + клиппинг ---
+ y += dp(58)
+ canvas.drawText("Уровень + клиппинг", dp(16), y, label)
+ y += dp(8)
+ drawVUMeter(canvas, w, y)
+
+ // --- Джиттер / потери ---
+ y += dp(58)
+ canvas.drawText("Джиттер и потери пакетов", dp(16), y, label)
+ y += dp(18)
+ canvas.drawText(
+ "джиттер %d мс · потери %.1f%%".format(simJitter.toInt(), simLoss),
+ dp(16), y, label
+ )
+ val bar = RectF(dp(16), y + dp(6), w - dp(16), y + dp(16))
+ canvas.drawRect(bar, track)
+ val jw = (simJitter / 250f).coerceIn(0f, 1f) * bar.width()
+ canvas.drawRect(bar.left, bar.top, bar.left + jw, bar.bottom, fill)
+ }
+
+ /** S-метр: полоса со шкалой S1..S9, +20..+60 и пик-меткой. */
+ private fun drawSMeter(canvas: Canvas, w: Float, top: Float) {
+ val left = dp(16)
+ val right = w - dp(16)
+ val bandTop = top + dp(2)
+ val bandH = dp(14)
+ canvas.drawRect(left, bandTop, right, bandTop + bandH, track)
+ val level = (simLevel).coerceIn(0f, 1f)
+ val fw = level * (right - left)
+ canvas.drawRect(left, bandTop, left + fw, bandTop + bandH, if (level > 0.85f) fillHot else fill)
+ // пик-метка
+ val px = left + simPeak.coerceIn(0f, 1f) * (right - left)
+ canvas.drawLine(px, bandTop - dp(3), px, bandTop + bandH + dp(3), peakPaint)
+ // деления и шкала
+ val marks = listOf("S1" to 0.11f, "S3" to 0.22f, "S5" to 0.33f, "S7" to 0.44f,
+ "S9" to 0.55f, "+20" to 0.66f, "+40" to 0.77f, "+60" to 0.88f)
+ for ((text, pos) in marks) {
+ val mx = left + pos * (right - left)
+ canvas.drawLine(mx, bandTop, mx, bandTop + bandH, tick)
+ canvas.drawText(text, mx - dp(10), bandTop + bandH + dp(12), labelSmall)
+ }
+ canvas.drawText("+%.0f".format(level * 60), left, bandTop - dp(4), labelSmall)
+ }
+
+ /** VU-полоса с зоной перегрузки и красной точкой клиппинга. */
+ private fun drawVUMeter(canvas: Canvas, w: Float, top: Float) {
+ val left = dp(16)
+ val right = w - dp(52)
+ val bandTop = top + dp(2)
+ val bandH = dp(14)
+ canvas.drawRect(left, bandTop, right, bandTop + bandH, track)
+ val level = simLevel.coerceIn(0f, 1f)
+ val fw = level * (right - left)
+ canvas.drawRect(left, bandTop, left + fw, bandTop + bandH, fill)
+ // красная зона последних 15%
+ canvas.drawRect(
+ right - (right - left) * 0.15f, bandTop, right, bandTop + bandH,
+ if (level > 0.85f) fillHot else track
+ )
+ // клиппинг-точка
+ val cxp = w - dp(24)
+ val cyp = bandTop + bandH / 2
+ canvas.drawCircle(cxp, cyp, dp(7), if (level > 0.92f) clipDot else clipDotOff)
+ canvas.drawText("CLIP", cxp - dp(20), cyp + dp(10), labelSmall)
+ }
+}
\ 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 9d0e346..bdc676c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -53,6 +53,8 @@
MIC
---
+%1$d dB
+ Примеры индикаторов звука
+ Временное демо (уровни симулируются). Скажи, что реализовать по-настоящему.
TG%1$d ▾
Последний: —
Подключение…