Sound meter preview dialog (hold on 'На связи'): S-meter, clipping, jitter/loss demo
This commit is contained in:
@@ -35,6 +35,7 @@ import ru.ua1zbe.svxremote.prefs.Prefs
|
|||||||
import ru.ua1zbe.svxremote.proto.MsgParser
|
import ru.ua1zbe.svxremote.proto.MsgParser
|
||||||
import ru.ua1zbe.svxremote.proto.ReflectorClient
|
import ru.ua1zbe.svxremote.proto.ReflectorClient
|
||||||
import ru.ua1zbe.svxremote.ui.PttButton
|
import ru.ua1zbe.svxremote.ui.PttButton
|
||||||
|
import ru.ua1zbe.svxremote.ui.SoundMeterPreviewView
|
||||||
|
|
||||||
private const val REQ_MIC = 1001
|
private const val REQ_MIC = 1001
|
||||||
private const val REQ_NOTIF = 1002
|
private const val REQ_NOTIF = 1002
|
||||||
@@ -166,6 +167,12 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
|
|
||||||
lblTalker.setOnClickListener { showTalkLogDialog() }
|
lblTalker.setOnClickListener { showTalkLogDialog() }
|
||||||
|
|
||||||
|
// Удержание на надписи «На связи» — демо-окно индикаторов звука
|
||||||
|
lblTxState.setOnLongClickListener {
|
||||||
|
showSoundMetersPreview()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
// Удержание на названии программы (> 2 с) — выбор профиля настроек;
|
// Удержание на названии программы (> 2 с) — выбор профиля настроек;
|
||||||
// короткие тапы считаем: тройной тап — настройки
|
// короткие тапы считаем: тройной тап — настройки
|
||||||
lblAppTitle.setOnTouchListener { _, event ->
|
lblAppTitle.setOnTouchListener { _, event ->
|
||||||
@@ -659,6 +666,16 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
.show()
|
.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() {
|
private fun showGroupsDialog() {
|
||||||
val groups = (settings.groups + settings.talkGroup).distinct()
|
val groups = (settings.groups + settings.talkGroup).distinct()
|
||||||
if (groups.isEmpty()) {
|
if (groups.isEmpty()) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,8 @@
|
|||||||
<string name="meter_mic">MIC</string>
|
<string name="meter_mic">MIC</string>
|
||||||
<string name="meter_idle">---</string>
|
<string name="meter_idle">---</string>
|
||||||
<string name="meter_db">+%1$d dB</string>
|
<string name="meter_db">+%1$d dB</string>
|
||||||
|
<string name="meter_preview_title">Примеры индикаторов звука</string>
|
||||||
|
<string name="meter_preview_hint">Временное демо (уровни симулируются). Скажи, что реализовать по-настоящему.</string>
|
||||||
<string name="tg_badge">TG%1$d ▾</string>
|
<string name="tg_badge">TG%1$d ▾</string>
|
||||||
<string name="talker_last_none">Последний: —</string>
|
<string name="talker_last_none">Последний: —</string>
|
||||||
<string name="status_connecting">Подключение…</string>
|
<string name="status_connecting">Подключение…</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user