mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
fix audio web
This commit is contained in:
+95
-84
@@ -6,11 +6,15 @@ unit WebPageHtml;
|
||||
Содержит единственную функцию GetIndexHtml, которая возвращает полную
|
||||
HTML-страницу (одним файлом) для отправки клиенту по HTTP GET /.
|
||||
|
||||
Страница включает:
|
||||
- CSS: тёмная тема, VFO, спектр, водопад, S-метр
|
||||
- HTML: разметка панелей управления
|
||||
- JS: WebSocket клиент, отрисовка спектра/водопада/S-метра,
|
||||
VFO отображение и управление, Opus декодер (CDN)
|
||||
ИСПРАВЛЕНИЯ:
|
||||
- Кнопка START/STOP добавлена в тулбар 1 (шлёт set_run, зелёная/красная)
|
||||
- Кнопка RX/TX удалена из тулбара 2
|
||||
- NR/NB/ANF: cur обновляется локально сразу при клике (иначе повторный
|
||||
клик инвертировал уже отправленное значение)
|
||||
- Opus: decodeFrame() правильный метод, async/await
|
||||
- Opus: opQueue буферизует пакеты пока WASM не готов
|
||||
- Opus: AudioContext создаётся только по user gesture (оверлей)
|
||||
- SPANS: только 48K..1536K
|
||||
}
|
||||
|
||||
{$IFDEF FPC}
|
||||
@@ -27,11 +31,10 @@ implementation
|
||||
function GetIndexHtml: string;
|
||||
begin
|
||||
Result :=
|
||||
// ── <head>: мета, шрифты, CSS ──────────────────────────────────────────
|
||||
'<!DOCTYPE html><html lang="en"><head>' +
|
||||
'<meta charset="UTF-8">' +
|
||||
'<meta name="viewport" content="width=device-width,initial-scale=1">' +
|
||||
'<title>HPSDR Web Remote</title>' +
|
||||
'<title>HPSDR Web Remote v2</title>' +
|
||||
'<link rel="preconnect" href="https://fonts.googleapis.com">' +
|
||||
'<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400&family=Share+Tech+Mono&display=swap" rel="stylesheet">' +
|
||||
'<style>' +
|
||||
@@ -39,7 +42,6 @@ begin
|
||||
'body{background:#141414;color:#ccc;font-family:"Segoe UI",Tahoma,sans-serif;font-size:12px;overflow:hidden;height:100vh}' +
|
||||
'.wrap{display:flex;flex-direction:column;height:100vh;gap:2px;padding:2px}' +
|
||||
'@import url(''https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500&display=swap'');' +
|
||||
// ── Тулбар ──
|
||||
'.tbar{display:flex;align-items:center;gap:5px;background:#1e1e1e;border:1px solid #2e2e2e;border-radius:3px;padding:2px 6px;flex-shrink:0;height:32px}' +
|
||||
'.tsep{flex:1}' +
|
||||
'button{background:linear-gradient(180deg,#404040,#2c2c2c);color:#ccc;border:1px solid #505050;border-radius:3px;padding:0 9px;font-size:11px;cursor:pointer;height:22px;white-space:nowrap;line-height:1;display:inline-flex;align-items:center;justify-content:center}' +
|
||||
@@ -55,7 +57,6 @@ begin
|
||||
'input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:11px;height:11px;border-radius:50%;background:#5a9ab0;border:1px solid #7ac;cursor:pointer}' +
|
||||
'input[type=range]::-moz-range-thumb{width:11px;height:11px;border-radius:50%;background:#5a9ab0;border:1px solid #7ac;cursor:pointer}' +
|
||||
'.vol-ic{font-size:13px;cursor:pointer;color:#7ab;user-select:none;line-height:1}' +
|
||||
// ── VFO-строка ──
|
||||
'.row2{height:74px;display:flex;align-items:center;gap:6px;background:#0c0c0c}' +
|
||||
'#smeter{height:64px;width:340px;flex-shrink:0;border:1px solid #333;border-radius:2px;display:block}' +
|
||||
'.vfo-mid{flex:1;display:flex;align-items:center;justify-content:center;gap:5px}' +
|
||||
@@ -75,7 +76,6 @@ begin
|
||||
'.swap-g button{padding:0 7px;height:22px;font-size:10px}' +
|
||||
'.right-g{display:flex;align-items:center;gap:4px;margin-left:auto}' +
|
||||
'.right-g button,.right-g select{height:62px;font-size:12px}' +
|
||||
// ── Спектр / линейка / водопад ──
|
||||
'.spec-area{flex:1 1 auto;min-height:80px;position:relative;background:#050a05;border:1px solid #252525;border-radius:3px;overflow:hidden}' +
|
||||
'.spec-area canvas{position:absolute;top:0;left:0;width:100%;height:100%}' +
|
||||
'.span-ov{position:absolute;top:4px;left:4px;display:flex;gap:3px;z-index:5}' +
|
||||
@@ -90,10 +90,9 @@ begin
|
||||
'#latEl{margin-left:auto}' +
|
||||
'</style></head>' +
|
||||
|
||||
// ── <body>: разметка панелей ────────────────────────────────────────────
|
||||
'<body><div class="wrap">' +
|
||||
|
||||
// Тулбар 1: громкость, AGC, режимы, DSP кнопки
|
||||
// Тулбар 1: громкость, AGC, режимы, DSP кнопки, START/STOP
|
||||
'<div class="tbar">' +
|
||||
'<span class="vol-ic" id="muteIc" title="Mute">🔊</span>' +
|
||||
'<div class="sl-g"><input type="range" id="volSl" min="0" max="100" value="70"><span class="sl-val" id="volVal">70</span></div>' +
|
||||
@@ -106,24 +105,26 @@ begin
|
||||
'<span class="sl-sep">|</span>' +
|
||||
'<div class="tsep"></div>' +
|
||||
'<select id="agcSel">' +
|
||||
'<option value="0">AGC-F</option><option value="1" selected>AGC-M</option>' +
|
||||
'<option value="2">AGC-S</option><option value="3">AGC-L</option><option value="4">AGC-OFF</option>' +
|
||||
'<option value="0">AGC-F</option><option value="1" selected>AGC-M</option>' +
|
||||
'<option value="2">AGC-S</option><option value="3">AGC-L</option><option value="4">AGC-OFF</option>' +
|
||||
'</select>' +
|
||||
'<select id="modGrpSel">' +
|
||||
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
|
||||
'<option value="fm">FM</option><option value="am">AM</option>' +
|
||||
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
|
||||
'<option value="fm">FM</option><option value="am">AM</option>' +
|
||||
'</select>' +
|
||||
'<select id="stepSel">' +
|
||||
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' +
|
||||
'<option value="1000">1 kHz</option><option value="10000">10 kHz</option>' +
|
||||
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' +
|
||||
'<option value="1000">1 kHz</option><option value="10000">10 kHz</option>' +
|
||||
'</select>' +
|
||||
'<select id="attSel"><option value="0" selected>0 dB</option><option value="1">-10 dB</option><option value="2">-20 dB</option></select>' +
|
||||
'<button id="nbBtn">NB</button>' +
|
||||
'<button id="anfBtn">ANF</button>' +
|
||||
'<button id="nrBtn">NR</button>' +
|
||||
'<span class="sl-sep">|</span>' +
|
||||
'<button id="startStopBtn" class="on">START</button>' +
|
||||
'</div>' +
|
||||
|
||||
// Тулбар 2: S-метр, VFO A/B, RX/TX, диапазон, CW
|
||||
// Тулбар 2: S-метр, VFO A/B, диапазон, CW (кнопка TX/RX убрана)
|
||||
'<div class="tbar row2">' +
|
||||
'<canvas id="smeter" width="340" height="64"></canvas>' +
|
||||
'<div class="vfo-mid">' +
|
||||
@@ -139,7 +140,6 @@ begin
|
||||
'<div id="vDispB" class="vfo-ro dim"></div>' +
|
||||
'</div>' +
|
||||
'<div class="right-g">' +
|
||||
'<button id="rxTxBtn">RX</button>' +
|
||||
'<button id="cwBtn">CW</button>' +
|
||||
'<button id="modTglBtn">LSB</button>' +
|
||||
'<select id="bandSel"></select>' +
|
||||
@@ -147,17 +147,14 @@ begin
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Спектр / частотная линейка / водопад / статусбар
|
||||
'<div class="spec-area"><canvas id="spec"></canvas><div class="span-ov" id="spanOv"></div></div>' +
|
||||
'<div class="ruler-area"><canvas id="ruler"></canvas></div>' +
|
||||
'<div class="wf-area"><canvas id="wf"></canvas></div>' +
|
||||
'<div class="sbar"><span id="stEl">connecting...</span><span id="latEl"></span></div>' +
|
||||
'</div>' +
|
||||
|
||||
// ── <script>: весь клиентский JS ───────────────────────────────────────
|
||||
'<script>' +
|
||||
|
||||
// Ссылки на canvas и контексты
|
||||
'const specCv=document.getElementById("spec"),wfCv=document.getElementById("wf"),' +
|
||||
'rulerCv=document.getElementById("ruler"),smCv=document.getElementById("smeter");' +
|
||||
'const sctx=specCv.getContext("2d"),wctx=wfCv.getContext("2d"),' +
|
||||
@@ -165,29 +162,23 @@ begin
|
||||
'const wfBuf=document.createElement("canvas"),wfBCtx=wfBuf.getContext("2d");' +
|
||||
'const stEl=document.getElementById("stEl"),latEl=document.getElementById("latEl");' +
|
||||
|
||||
// Состояние
|
||||
'let cur={},vfoA=14200000,vfoB=14200000,activeVfo="A";' +
|
||||
'let dragging=false,dragX0=0,dragCen0=0,dragVfo0=0,dragMs=0;' +
|
||||
'let hoverX=-1,wTarget="",markerOn=false,markerX=0;' +
|
||||
'let specSm=[],wfAvg=[],wfHi=-50,wfLo=-120;' +
|
||||
'let audioCtx=null,audioT=0;' +
|
||||
|
||||
// AudioContext
|
||||
'function audioKick(){if(!audioCtx){audioCtx=new(window.AudioContext||window.webkitAudioContext)({sampleRate:48000});audioT=audioCtx.currentTime+0.05;}if(audioCtx.state!=="running")audioCtx.resume();}' +
|
||||
|
||||
// Цветовая палитра водопада (256 цветов)
|
||||
'const PAL=[0x000000,0x000003,0x000006,0x000009,0x00000C,0x00000F,0x000012,0x000015,0x000019,0x00001C,0x00001F,0x000022,0x000025,0x000028,0x00002B,0x00002F,0x000032,0x000035,0x000038,0x00003B,0x00003E,0x000041,0x000045,0x000048,0x00004B,0x00004E,0x000051,0x000054,0x000057,0x00005A,0x00005E,0x000061,0x000064,0x000067,0x00006A,0x00006D,0x000070,0x000074,0x000077,0x00007A,0x00007D,0x000080,0x000083,0x000086,0x00008A,0x00008D,0x000090,0x000093,0x000096,0x000099,0x00009C,0x0000A0,0x0002A2,0x0004A4,0x0006A7,0x0008A9,0x000AAC,0x000CAE,0x000EB1,0x0010B3,0x0012B6,0x0014B8,0x0017BB,0x0019BD,0x001BC0,0x001DC2,0x001FC5,0x0021C7,0x0023CA,0x0025CC,0x0027CF,0x0029D1,0x002BD4,0x002ED6,0x0030D9,0x0032DB,0x0034DE,0x0036E0,0x0038E3,0x003AE5,0x003CE8,0x003EEA,0x0040EC,0x0042EF,0x0045F1,0x0047F4,0x0049F6,0x004BF9,0x004DFB,0x004FFE,0x0052FE,0x0056FD,0x005AFC,0x005DFB,0x0061FA,0x0065F9,0x0068F8,0x006CF7,0x0070F6,0x0073F6,0x0077F5,0x007BF4,0x007EF3,0x0082F2,0x0085F1,0x0089F0,0x008DEF,0x0090EE,0x0094ED,0x0098EC,0x009BEC,0x009FEB,0x00A3EA,0x00A6E9,0x00AAE8,0x00AEE7,0x00B1E6,0x00B5E5,0x00B9E4,0x00BCE3,0x00C0E2,0x00C4E1,0x00C7E1,0x00CBE0,0x00CFDF,0x00D2DE,0x00D6DD,0x00DADC,0x00DCD9,0x00DDD3,0x00DECD,0x00DFC7,0x00E0C2,0x00E1BC,0x00E1B6,0x00E2B0,0x00E3AB,0x00E4A5,0x00E59F,0x00E699,0x00E794,0x00E88E,0x00E98';
|
||||
Result := Result +
|
||||
'const PAL=[0x000000,0x000003,0x000006,0x000009,0x00000C,0x00000F,0x000012,0x000015,0x000019,0x00001C,0x00001F,0x000022,0x000025,0x000028,0x00002B,0x00002F,0x000032,0x000035,0x000038,0x00003B,0x00003E,0x000041,0x000045,0x000048,0x00004B,0x00004E,0x000051,0x000054,0x000057,0x00005A,0x00005E,0x000061,0x000064,0x000067,0x00006A,0x00006D,0x000070,0x000074,0x000077,0x00007A,0x00007D,0x000080,0x000083,0x000086,0x00008A,0x00008D,0x000090,0x000093,0x000096,0x000099,0x00009C,0x0000A0,0x0002A2,0x0004A4,0x0006A7,0x0008A9,0x000AAC,0x000CAE,0x000EB1,0x0010B3,0x0012B6,0x0014B8,0x0017BB,0x0019BD,0x001BC0,0x001DC2,0x001FC5,0x0021C7,0x0023CA,0x0025CC,0x0027CF,0x0029D1,0x002BD4,0x002ED6,0x0030D9,0x0032DB,0x0034DE,0x0036E0,0x0038E3,0x003AE5,0x003CE8,0x003EEA,0x0040EC,0x0042EF,0x0045F1,0x0047F4,0x0049F6,0x004BF9,0x004DFB,0x004FFE,0x0052FE,0x0056FD,0x005AFC,0x005DFB,0x0061FA,0x0065F9,0x0068F8,0x006CF7,0x0070F6,0x0073F6,0x0077F5,0x007BF4,0x007EF3,0x0082F2,0x0085F1,0x0089F0,0x008DEF,0x0090EE,0x0094ED,0x0098EC,0x009BEC,0x009FEB,0x00A3EA,0x00A6E9,0x00AAE8,0x00AEE7,0x00B1E6,0x00B5E5,0x00B9E4,0x00BCE3,0x00C0E2,0x00C4E1,0x00C7E1,0x00CBE0,0x00CFDF,0x00D2DE,0x00D6DD,0x00DADC,0x00DCD9,0x00DDD3,0x00DECD,0x00DFC7,0x00E0C2,0x00E1BC,0x00E1B6,0x00E2B0,0x00E3AB,0x00E4A5,0x00E59F,0x00E699,0x00E794,0x00E88E,0x00E98' +
|
||||
'8,0x00EA82,0x00EB7D,0x00EC77,0x00EC71,0x00ED6B,0x00EE66,0x00EF60,0x00F05A,0x00F154,0x00F24F,0x00F349,0x00F443,0x00F53D,0x00F638,0x00F632,0x00F72C,0x00F826,0x00F921,0x00FA1B,0x00FB15,0x00FC0F,0x00FD0A,0x00FE04,0x01FF00,0x08FF00,0x0EFF00,0x15FF00,0x1CFF00,0x22FF00,0x29FF00,0x30FF00,0x36FF00,0x3DFF00,0x44FF00,0x4AFF00,0x51FF00,0x58FF00,0x5FFF00,0x65FF00,0x6CFF00,0x72FF00,0x79FF00,0x80FF00,0x86FF00,0x8DFF00,0x94FF00,0x9BFF00,0xA1FF00,0xA8FF00,0xAEFF00,0xB5FF00,0xBCFF00,0xC2FF00,0xC9FF00,0xD0FF00,0xD6FF00,0xDDFF00,0xE4FF00,0xEAFF00,0xF1FF00,0xF8FF00,0xFFFF00,0xFFF900,0xFFF300,0xFFED00,0xFFE800,0xFFE200,0xFFDC00,0xFFD600,0xFFD100,0xFFCB00,0xFFC500,0xFFC000,0xFFBA00,0xFFB400,0xFFAE00,0xFFA900,0xFFA300,0xFF9D00,0xFF9800,0xFF9200,0xFF8C00,0xFF8600,0xFF8100,0xFF7B00,0xFF7500,0xFF7000,0xFF6A00,0xFF6400,0xFF5E00,0xFF5900,0xFF5300,0xFF5304,0xFF5C11,0xFF641D,0xFF6D2A,0xFF7536,0xFF7E43,0xFF864F,0xFF8F5C,0xFF9868,0xFFA075,0xFFA982,0xFFB18E,0xFFBA9A,0xFFC2A7,0xFFCBB3,0xFFD4C0,0xFFDCCC,0xFFE5D9,0xFFEDE6,0xFFF6F2,0xFFFFFF];' +
|
||||
|
||||
// Утилиты
|
||||
'const cl=(v,a,b)=>Math.max(a,Math.min(b,v));' +
|
||||
'const q=id=>document.getElementById(id);' +
|
||||
'function sw(id,on){const b=q(id);if(b)b.classList.toggle("on",!!on);}' +
|
||||
'function ws2(obj){if(ws&&ws.readyState===1)ws.send(JSON.stringify(obj));}' +
|
||||
'function cmd(c,v){ws2({cmd:c,on:v});}' +
|
||||
|
||||
// Режимы и группы
|
||||
'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM"];' +
|
||||
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,5],am:[6,7]};' +
|
||||
'function modGrp(){return q("modGrpSel").value||"ssb";}' +
|
||||
@@ -195,21 +186,25 @@ begin
|
||||
'function updModTglLabel(){const b=q("modTglBtn");if(!b)return;b.textContent=MODE_N[modTglTarget()];}' +
|
||||
'function syncModGrp(m){const g=m<=1?"ssb":m<=4?"cw":m===5?"fm":"am";const s=q("modGrpSel");if(s&&s.value!==g)s.value=g;}' +
|
||||
'q("modGrpSel").onchange=function(){updModTglLabel();};' +
|
||||
'q("modTglBtn").onclick=function(){audioKick();ws2({cmd:"mode",mode:modTglTarget()});};' +
|
||||
'q("cwBtn").onclick=function(){audioKick();ws2({cmd:"mode",mode:cur.mode===3?4:3});};' +
|
||||
'q("agcSel").onchange=function(e){ws2({cmd:"agc",mode:+e.target.value});};' +
|
||||
'q("modTglBtn").onclick=function(){audioKick();pend("mode");ws2({cmd:"mode",mode:modTglTarget()});};' +
|
||||
'q("cwBtn").onclick=function(){audioKick();pend("mode");ws2({cmd:"mode",mode:cur.mode===3?4:3});};' +
|
||||
'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' +
|
||||
|
||||
// Слайдеры
|
||||
'q("volSl").oninput=function(e){ws2({cmd:"volume",v:+e.target.value});var v=q("volVal");if(v)v.textContent=e.target.value;};' +
|
||||
'q("rfSl").oninput=function(e){ws2({cmd:"agctop",db:+e.target.value});var v=q("rfVal");if(v)v.textContent=e.target.value+"dB";};' +
|
||||
'q("txSl").oninput=function(e){var v=q("txVal");if(v)v.textContent=e.target.value;};' +
|
||||
|
||||
// DSP кнопки
|
||||
'q("nrBtn").onclick=function(){cmd("set_nr",!cur.nr);};' +
|
||||
'q("nbBtn").onclick=function(){cmd("set_nb",!cur.nb);};' +
|
||||
'q("anfBtn").onclick=function(){cmd("set_anf",!cur.anf);};' +
|
||||
// DSP кнопки — cur обновляется локально сразу, не ждём ответа сервера
|
||||
'q("nrBtn").onclick=function(){cur.nr=!cur.nr;sw("nrBtn",cur.nr);pend("nr");cmd("set_nr",cur.nr);};' +
|
||||
'q("nbBtn").onclick=function(){cur.nb=!cur.nb;sw("nbBtn",cur.nb);pend("nb");cmd("set_nb",cur.nb);};' +
|
||||
'q("anfBtn").onclick=function(){cur.anf=!cur.anf;sw("anfBtn",cur.anf);pend("anf");cmd("set_anf",cur.anf);};' +
|
||||
|
||||
// START/STOP
|
||||
'function updRunBtn(r){var b=q("startStopBtn");if(!b)return;b.textContent=r?"STOP":"START";b.classList.toggle("rx-tx",!!r);b.classList.toggle("on",!r);}' +
|
||||
'q("startStopBtn").onclick=function(){audioKick();cur.running=!cur.running;pend("run");updRunBtn(cur.running);cmd("set_run",cur.running);};' +
|
||||
|
||||
'q("muteIc").onclick=()=>{cur.mute=!cur.mute;q("muteIc").textContent=cur.mute?"\uD83D\uDD07":"\uD83D\uDD0A";cmd("set_mute",cur.mute);};' +
|
||||
|
||||
// VFO отображение
|
||||
'let stA=0,stAmem=100,stB=0,stBmem=100;' +
|
||||
'function fmtVfo(hz,step){' +
|
||||
'const s=String(Math.max(0,Math.round(hz||0))).padStart(9,"0");' +
|
||||
@@ -229,7 +224,6 @@ begin
|
||||
'return h;}' +
|
||||
'function renderVfos(){q("vDispA").innerHTML=fmtVfo(vfoA,stA);q("vDispB").innerHTML=fmtVfo(vfoB,stB);}' +
|
||||
|
||||
// Привязка событий VFO (колесо, клик по цифре, dblclick)
|
||||
'function bindVfo(dispId,getHz,setHz,getSt,setSt,getMem,setMem){' +
|
||||
'const el=q(dispId);' +
|
||||
'el.addEventListener("mousemove",e=>{const t=e.target,s=Number(t?.dataset?.s||0);if(s>0){setSt(s);setMem(s);renderVfos();}});' +
|
||||
@@ -238,7 +232,7 @@ begin
|
||||
'el.addEventListener("wheel",e=>{e.preventDefault();e.stopPropagation();audioKick();' +
|
||||
'const dir=e.deltaY<0?1:-1,st=getSt()||getMem()||100;' +
|
||||
'const nf=cl(Math.round(getHz())+st*dir,30000,60000000);' +
|
||||
'setHz(nf);renderVfos();ws2({cmd:"freq",hz:nf});},{passive:false});' +
|
||||
'setHz(nf);renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});},{passive:false});' +
|
||||
'el.addEventListener("dblclick",()=>{' +
|
||||
'const s=prompt("Set Hz",String(Math.round(getHz())));if(!s)return;' +
|
||||
'const f=parseInt(s.replace(/[^0-9]/g,""),10);if(!isFinite(f))return;' +
|
||||
@@ -247,7 +241,6 @@ begin
|
||||
'bindVfo("vDispA",()=>vfoA,v=>{vfoA=v;},()=>stA,v=>{stA=v;},()=>stAmem,v=>{stAmem=v;});' +
|
||||
'bindVfo("vDispB",()=>vfoB,v=>{vfoB=v;},()=>stB,v=>{stB=v;},()=>stBmem,v=>{stBmem=v;});' +
|
||||
|
||||
// Переключение активного VFO, кнопки A=B/B=A/SWAP
|
||||
'function selVfo(w){activeVfo=w;q("vBtnA").classList.toggle("active",w==="A");q("vBtnB").classList.toggle("active",w==="B");q("vDispA").classList.toggle("dim",w!="A");q("vDispB").classList.toggle("dim",w!="B");ws2({cmd:"freq",hz:Math.round(w==="A"?vfoA:vfoB)});}' +
|
||||
'q("vBtnA").onclick=()=>selVfo("A");' +
|
||||
'q("vBtnB").onclick=()=>selVfo("B");' +
|
||||
@@ -255,24 +248,17 @@ begin
|
||||
'q("btnAB").onclick=()=>{vfoB=vfoA;renderVfos();};' +
|
||||
'q("btnSwap").onclick=()=>{const t=vfoA;vfoA=vfoB;vfoB=t;renderVfos();ws2({cmd:"freq",hz:Math.round(activeVfo==="A"?vfoA:vfoB)});};' +
|
||||
|
||||
// RX/TX и Mute
|
||||
'q("rxTxBtn").onclick=()=>{audioKick();cmd("set_run",!cur.running);};' +
|
||||
'function updRunBtn(r){const b=q("rxTxBtn");b.textContent=r?"TX":"RX";b.classList.toggle("rx-tx",!!r);}' +
|
||||
'q("muteIc").onclick=()=>{cur.mute=!cur.mute;q("muteIc").textContent=cur.mute?"\uD83D\uDD07":"\uD83D\uDD0A";cmd("set_mute",cur.mute);};' +
|
||||
|
||||
// Кнопки выбора полосы обзора (SPAN)
|
||||
'const SPANS=[3000,6000,12000,24000,48000,96000,192000,384000,480000,960000];' +
|
||||
'const SPAN_L=["3K","6K","12K","24K","48K","96K","192K","384K","480K","960K"];' +
|
||||
// SPAN кнопки: только 48K..1536K
|
||||
'const SPANS=[48000,96000,192000,384000,768000,1536000];' +
|
||||
'const SPAN_L=["48K","96K","192K","384K","768K","1536K"];' +
|
||||
'(function(){const c=q("spanOv");SPANS.forEach((hz,i)=>{const b=document.createElement("button");b.className="sp-btn";b.id="sb"+i;b.textContent=SPAN_L[i];b.onclick=()=>ws2({cmd:"span",hz});c.appendChild(b);});})();' +
|
||||
'function updSpan(s){SPANS.forEach((hz,i)=>{const b=q("sb"+i);if(b)b.classList.toggle("on",hz===s);});}' +
|
||||
|
||||
// Список диапазонов
|
||||
'const BAND_N=["160m","80m","60m","40m","30m","20m","17m","15m","12m","10m","6m"];' +
|
||||
'const BAND_F=[1900000,3750000,5357000,7100000,10125000,14200000,18120000,21200000,24940000,28500000,50150000];' +
|
||||
'(function(){const s=q("bandSel");BAND_N.forEach((n,i)=>{const o=document.createElement("option");o.value=i;o.textContent=n;s.appendChild(o);});s.onchange=()=>ws2({cmd:"band",idx:+s.value});})();' +
|
||||
'function updBand(vfo){let bi=0,bd=1e15;BAND_F.forEach((f,i)=>{const d=Math.abs((vfo||0)-f);if(d<bd){bd=d;bi=i;}});q("bandSel").value=bi;}' +
|
||||
|
||||
// S-метр
|
||||
'let smAvg=-130,smPeak=-130,smMin=-130;' +
|
||||
'function dbToX(db,bx,bw){return Math.max(bx,Math.min(bx+bw,Math.round(bx+(db+127)/127*bw)));}' +
|
||||
'function paintSM(dbm){' +
|
||||
@@ -322,36 +308,48 @@ begin
|
||||
'else sLbl="S9+"+Math.round(dbm+93);' +
|
||||
'mctx.fillStyle="#F0F0F0";mctx.font="bold 12px Courier New";mctx.fillText(sLbl,1,YB+TICK_LONG+10);}' +
|
||||
|
||||
// Применение state от сервера
|
||||
'function applyState(msg){' +
|
||||
'cur=msg;vfoA=msg.vfo_a_hz||vfoA;renderVfos();' +
|
||||
'const as=q("agcSel");if(as&&msg.agc_mode!=null)as.value=msg.agc_mode;' +
|
||||
'const vs=q("volSl");if(vs&&!vs.matches(":active")){vs.value=msg.volume||70;var vv=q("volVal");if(vv)vv.textContent=msg.volume||70;}' +
|
||||
'const rs=q("rfSl");if(rs&&!rs.matches(":active")){rs.value=msg.agc_top||90;var rv=q("rfVal");if(rv)rv.textContent=(msg.agc_top||90)+"dB";}' +
|
||||
'sw("nrBtn",!!msg.nr);sw("nbBtn",!!msg.nb);sw("anfBtn",!!msg.anf);' +
|
||||
'updRunBtn(!!msg.running);syncModGrp(msg.mode||0);updModTglLabel();' +
|
||||
'updBand(msg.vfo_a_hz||0);updSpan(msg.span_hz||192000);' +
|
||||
'paintSM(Number(msg.smeter_dbm||-130));' +
|
||||
'const mn=msg.mode_name||(MODE_N[msg.mode||0]||"?");' +
|
||||
'stEl.textContent=(msg.connected?"OK":"OFF")+" | "+(msg.running?"RUN":"STOP")+" | "+mn+" | "+((vfoA/1e6).toFixed(3))+" MHz";}' +
|
||||
// pendingCmds: команды в пути — не перезаписываем эти поля из server state
|
||||
'var pendingCmds={};' +
|
||||
'function pend(field){pendingCmds[field]=Date.now()+600;}' +
|
||||
'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' +
|
||||
|
||||
// Маппинг: server JSON field → pend key
|
||||
'var PEND_MAP={vfo_a_hz:"freq",nr:"nr",nb:"nb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc"};' +
|
||||
|
||||
'function applyState(msg){' +
|
||||
// Мержим только поля которые не pending
|
||||
'Object.keys(msg).forEach(function(k){' +
|
||||
'var pk=PEND_MAP[k];' +
|
||||
'if(!pk||!isPending(pk))cur[k]=msg[k];' +
|
||||
'});' +
|
||||
// VFO
|
||||
'if(!isPending("freq")){vfoA=cur.vfo_a_hz||vfoA;renderVfos();}' +
|
||||
'const as=q("agcSel");if(as&&cur.agc_mode!=null&&!isPending("agc"))as.value=cur.agc_mode;' +
|
||||
'const vs=q("volSl");if(vs&&!vs.matches(":active")){vs.value=cur.volume||70;var vv=q("volVal");if(vv)vv.textContent=cur.volume||70;}' +
|
||||
'const rs=q("rfSl");if(rs&&!rs.matches(":active")){rs.value=cur.agc_top||90;var rv=q("rfVal");if(rv)rv.textContent=(cur.agc_top||90)+"dB";}' +
|
||||
'if(!isPending("nr"))sw("nrBtn",!!cur.nr);' +
|
||||
'if(!isPending("nb"))sw("nbBtn",!!cur.nb);' +
|
||||
'if(!isPending("anf"))sw("anfBtn",!!cur.anf);' +
|
||||
'if(!isPending("run"))updRunBtn(!!cur.running);' +
|
||||
'if(!isPending("mode")){syncModGrp(cur.mode||0);updModTglLabel();}' +
|
||||
'updBand(cur.vfo_a_hz||0);updSpan(cur.span_hz||192000);' +
|
||||
'paintSM(Number(cur.smeter_dbm||-130));' +
|
||||
'const mn=cur.mode_name||(MODE_N[cur.mode||0]||"?");' +
|
||||
'stEl.textContent=(cur.connected?"OK":"OFF")+" | "+(cur.running?"RUN":"STOP")+" | "+mn+" | "+((vfoA/1e6).toFixed(3))+" MHz";}' +
|
||||
|
||||
// Waterfall palette lookup и фильтр
|
||||
'function pal(db){const v=Math.round(cl((db-wfLo)/Math.max(1,wfHi-wfLo),0,1)*255);const c=PAL[v]|0;return[(c>>16)&255,(c>>8)&255,c&255];}' +
|
||||
'function fltLH(){const bw=cur.filter_bw||2700;const m=cur.mode|0;if(m===0)return[-bw,-100];if(m===1)return[100,bw];return[-bw/2,bw/2];}' +
|
||||
'function xToHz(x,w){return(cur.center_hz||0)+((x/Math.max(1,w))-.5)*(cur.span_hz||0);}' +
|
||||
'function hzToX(f,w){return((f-(cur.center_hz||0))/(cur.span_hz||1)+.5)*w;}' +
|
||||
|
||||
// Маркер частоты
|
||||
'function drawMk(ctx,w,h){if(!markerOn)return;ctx.save();ctx.strokeStyle="rgba(255,80,80,.85)";ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(markerX,0);ctx.lineTo(markerX,h);ctx.stroke();' +
|
||||
'const lbl=((xToHz(markerX,w))/1e6).toFixed(4)+" M";ctx.fillStyle="rgba(35,5,5,.82)";ctx.fillRect(cl(markerX+5,0,w-88),h-16,84,13);ctx.fillStyle="#f88";ctx.font="10px monospace";ctx.fillText(lbl,cl(markerX+8,2,w-86),h-6);ctx.restore();}' +
|
||||
|
||||
// Частотная линейка
|
||||
'function drawRuler(){const w=rulerCv.clientWidth,h=rulerCv.clientHeight;rctx.fillStyle="#0c1210";rctx.fillRect(0,0,w,h);' +
|
||||
'const st=(cur.center_hz||0)-(cur.span_hz||0)/2;' +
|
||||
'for(let i=0;i<=8;i++){const x=i*w/8,f=st+i*(cur.span_hz||0)/8;rctx.strokeStyle="#2a3a2e";rctx.lineWidth=1;rctx.beginPath();rctx.moveTo(x+.5,0);rctx.lineTo(x+.5,7);rctx.stroke();rctx.fillStyle="#7a9a80";rctx.font="10px monospace";const t=(f/1e6).toFixed(3);rctx.fillText(t,x-14,21);}' +
|
||||
'const vx=hzToX(vfoA,w);rctx.strokeStyle="#60d070";rctx.lineWidth=1;rctx.beginPath();rctx.moveTo(vx+.5,0);rctx.lineTo(vx+.5,h);rctx.stroke();}' +
|
||||
|
||||
// Спектр
|
||||
'function drawSpec(bins){const w=specCv.clientWidth,h=specCv.clientHeight;sctx.fillStyle="#050a05";sctx.fillRect(0,0,w,h);' +
|
||||
'for(let i=1;i<5;i++){const y=i*h/5;sctx.strokeStyle="#121e12";sctx.lineWidth=1;sctx.beginPath();sctx.moveTo(0,y);sctx.lineTo(w,y);sctx.stroke();}' +
|
||||
'if(!bins?.length){drawMk(sctx,w,h);return;}' +
|
||||
@@ -365,7 +363,6 @@ begin
|
||||
'sctx.strokeStyle="#3ddd3d";sctx.lineWidth=1.2;sctx.stroke();' +
|
||||
'const vx=hzToX(vfoA,w);sctx.strokeStyle="#60ff80";sctx.lineWidth=1;sctx.beginPath();sctx.moveTo(vx,0);sctx.lineTo(vx,h);sctx.stroke();drawMk(sctx,w,h);}' +
|
||||
|
||||
// Водопад
|
||||
'function drawWf(bins){const w=wfCv.clientWidth,h=wfCv.clientHeight;' +
|
||||
'if(!bins?.length){drawMk(wctx,w,h);return;}' +
|
||||
'if(wfBuf.width!==w||wfBuf.height!==h){wfBuf.width=w;wfBuf.height=h;wfBCtx.fillStyle="#000";wfBCtx.fillRect(0,0,w,h);}' +
|
||||
@@ -386,32 +383,36 @@ begin
|
||||
'wctx.fillStyle="rgba(220,230,230,.10)";wctx.fillRect(Math.min(x1,x2),0,Math.abs(x2-x1),h);' +
|
||||
'const vx=hzToX(vfoA,w);wctx.strokeStyle="#60ff80";wctx.lineWidth=1;wctx.beginPath();wctx.moveTo(vx,0);wctx.lineTo(vx,h);wctx.stroke();drawMk(wctx,w,h);}' +
|
||||
|
||||
// Resize с учётом devicePixelRatio
|
||||
'function rcv(cv){const dpr=Math.max(1,devicePixelRatio||1),w=cv.clientWidth|0,h=cv.clientHeight|0;if(cv.width!==(w*dpr|0)||cv.height!==(h*dpr|0)){cv.width=w*dpr;cv.height=h*dpr;cv.getContext("2d").setTransform(dpr,0,0,dpr,0,0);}}' +
|
||||
'function resize(){rcv(specCv);rcv(wfCv);rcv(rulerCv);const sw=smCv.clientWidth|0,sh=smCv.clientHeight|0;if(smCv.width!==sw||smCv.height!==sh){smCv.width=sw;smCv.height=sh;}if(wfBuf.width!==wfCv.clientWidth||wfBuf.height!==wfCv.clientHeight){wfBuf.width=wfCv.clientWidth;wfBuf.height=wfCv.clientHeight;wfBCtx.fillStyle="#000";wfBCtx.fillRect(0,0,wfBuf.width,wfBuf.height);}}' +
|
||||
|
||||
// Управление мышью: шаг, колесо, перетаскивание
|
||||
'function tuneStep(e){const s=+q("stepSel").value||100;if(e.ctrlKey&&e.shiftKey)return 1000000;if(e.ctrlKey)return 1000;if(e.shiftKey)return 10;return s;}' +
|
||||
'function doWheel(e){e.preventDefault();e.stopPropagation();audioKick();const dir=e.deltaY<0?1:-1,st=tuneStep(e),base=Math.floor(vfoA/st)*st;vfoA=cl(dir>0?base+st:(Math.round(vfoA)===base?base-st:base),30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();ws2({cmd:"freq",hz:vfoA});}' +
|
||||
'function doWheel(e){e.preventDefault();e.stopPropagation();audioKick();const dir=e.deltaY<0?1:-1,st=tuneStep(e),base=Math.floor(vfoA/st)*st;vfoA=cl(dir>0?base+st:(Math.round(vfoA)===base?base-st:base),30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();pend("freq");ws2({cmd:"freq",hz:vfoA});}' +
|
||||
'function bindCv(cv){let wT=0;const wh=e=>{const n=performance.now();if(n-wT<20)return;wT=n;wTarget=cv.id;doWheel(e);};' +
|
||||
'cv.addEventListener("contextmenu",e=>e.preventDefault());' +
|
||||
'cv.addEventListener("mouseenter",()=>{wTarget=cv.id;});' +
|
||||
'cv.addEventListener("wheel",wh,{passive:false,capture:true});' +
|
||||
'cv.addEventListener("mousedown",e=>{audioKick();wTarget=cv.id;if(e.button===2){markerOn=Math.abs(markerX-e.offsetX)>=8||!markerOn;markerX=e.offsetX;return;}if(e.button!==0)return;dragging=true;dragX0=e.offsetX;dragCen0=cur.center_hz||0;dragVfo0=vfoA;dragMs=0;});' +
|
||||
'cv.addEventListener("mousemove",e=>{wTarget=cv.id;hoverX=e.offsetX;if(markerOn)markerX=e.offsetX;if(!dragging)return;const n=Date.now();if(n-dragMs<35)return;dragMs=n;const dHz=(e.offsetX-dragX0)/Math.max(1,cv.clientWidth)*(cur.span_hz||0);if(cur.ctun)ws2({cmd:"freq",hz:Math.round(dragCen0-dHz)});else{vfoA=cl(Math.round(dragVfo0-dHz),30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();ws2({cmd:"freq",hz:vfoA});}});' +
|
||||
'cv.addEventListener("mouseup",e=>{if(e.button===2)return;if(Math.abs(e.offsetX-dragX0)<4){vfoA=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/100)*100,30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();ws2({cmd:"freq",hz:vfoA});}dragging=false;});' +
|
||||
'cv.addEventListener("mousemove",e=>{wTarget=cv.id;hoverX=e.offsetX;if(markerOn)markerX=e.offsetX;if(!dragging)return;const n=Date.now();if(n-dragMs<35)return;dragMs=n;const dHz=(e.offsetX-dragX0)/Math.max(1,cv.clientWidth)*(cur.span_hz||0);if(cur.ctun)ws2({cmd:"freq",hz:Math.round(dragCen0-dHz)});else{vfoA=cl(Math.round(dragVfo0-dHz),30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();pend("freq");ws2({cmd:"freq",hz:vfoA});}});' +
|
||||
'cv.addEventListener("mouseup",e=>{if(e.button===2)return;if(Math.abs(e.offsetX-dragX0)<4){vfoA=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/100)*100,30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();pend("freq");ws2({cmd:"freq",hz:vfoA});}dragging=false;});' +
|
||||
'cv.addEventListener("mouseleave",()=>{if(!markerOn)hoverX=-1;wTarget="";dragging=false;});}' +
|
||||
'bindCv(specCv);bindCv(wfCv);' +
|
||||
'window.addEventListener("wheel",e=>{if(e.target?.closest?.(".vfo-ro"))return;if(!wTarget){if(e.target===specCv)wTarget="spec";else if(e.target===wfCv)wTarget="wf";}if(wTarget)doWheel(e);},{passive:false,capture:true});' +
|
||||
|
||||
// Opus декодер (CDN) и воспроизведение
|
||||
'let ws=null,opDec=null,opRdy=false;' +
|
||||
// Opus: async decodeFrame, очередь, оверлей для user gesture
|
||||
'let ws=null,opDec=null,opRdy=false,opQueue=[];' +
|
||||
'async function initOpus(){if(opRdy)return;' +
|
||||
'try{const m=await import("https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/+esm");' +
|
||||
'opDec=new m.OpusDecoder({channels:1,sampleRate:48000});' +
|
||||
'await opDec.ready;opRdy=true;}catch(e){console.error("Opus init:",e);}}' +
|
||||
'function playOpus(b){if(!opRdy||!audioCtx||!b?.length)return;' +
|
||||
'try{const r=opDec.decodeFrame(b);' +
|
||||
'await opDec.ready;opRdy=true;' +
|
||||
'const q2=opQueue;opQueue=[];for(const b of q2)await playOpus(b);' +
|
||||
'latEl.textContent="Opus ready";}catch(e){console.error("Opus init:",e);latEl.textContent="Opus FAILED: "+e.message;}}' +
|
||||
|
||||
'async function playOpus(b){' +
|
||||
'if(!b||!b.length)return;' +
|
||||
'if(!opRdy){opQueue.push(b);return;}' +
|
||||
'if(!audioCtx)return;' +
|
||||
'try{const r=await opDec.decodeFrame(b);' +
|
||||
'if(!r||!r.channelData||!r.samplesDecoded)return;' +
|
||||
'const pcm=r.channelData[0];' +
|
||||
'const buf=audioCtx.createBuffer(1,r.samplesDecoded,48000);' +
|
||||
@@ -423,19 +424,29 @@ begin
|
||||
'latEl.textContent="lat: "+Math.max(0,(audioT-now)*1000).toFixed(0)+"ms | Opus";' +
|
||||
'}catch(e){console.error("playOpus:",e);}}' +
|
||||
|
||||
// WebSocket подключение и обработка сообщений
|
||||
'function connect(){const p=location.protocol==="https:"?"wss":"ws";ws=new WebSocket(p+"://"+location.host+"/ws");ws.binaryType="arraybuffer";' +
|
||||
'ws.onopen=()=>{stEl.textContent="connected";initOpus().then(()=>{latEl.textContent=opRdy?"Opus ready":"Opus FAILED";});};' +
|
||||
'ws.onopen=()=>{stEl.textContent="connected";initOpus();};' +
|
||||
'ws.onclose=()=>{stEl.textContent="disconnected...";setTimeout(connect,3000);};' +
|
||||
'ws.onerror=()=>ws.close();' +
|
||||
'ws.onmessage=e=>{if(typeof e.data==="string"){try{applyState(JSON.parse(e.data));}catch(x){}return;}' +
|
||||
'const ab=e.data,t=(new DataView(ab)).getUint8(0);' +
|
||||
'if(t===0x53){const n=(ab.byteLength-1)>>2;const tmp=new Uint8Array(ab,1);const al=new ArrayBuffer(n*4);new Uint8Array(al).set(tmp);const b=Array.from(new Float32Array(al));drawSpec(b);drawWf(b);drawRuler();}' +
|
||||
'else if(t===0x41){audioKick();if(opRdy)playOpus(new Uint8Array(ab,1));}};}' +
|
||||
'else if(t===0x41){if(audioCtx&&audioCtx.state==="suspended")audioCtx.resume();playOpus(new Uint8Array(ab,1));}};}' +
|
||||
|
||||
// Оверлей для user gesture (AudioContext требует клика)
|
||||
'function showAudioOverlay(){' +
|
||||
'const ov=document.createElement("div");' +
|
||||
'ov.style.cssText="position:fixed;inset:0;background:rgba(0,0,0,.72);display:flex;align-items:center;justify-content:center;z-index:9999;cursor:pointer;";' +
|
||||
'const inner=document.createElement("div");' +
|
||||
'inner.textContent="\u25BA Click to start audio";' +
|
||||
'inner.style.cssText="color:#7fff7f;font-size:22px;font-family:monospace;text-align:center;padding:20px;border:1px solid #3d7a3d;border-radius:6px;background:#0c1a0c;";' +
|
||||
'ov.appendChild(inner);' +
|
||||
'ov.addEventListener("click",function(){audioKick();ov.remove();},{once:true});' +
|
||||
'document.body.appendChild(ov);}' +
|
||||
|
||||
// Инициализация
|
||||
'window.addEventListener("resize",resize);resize();renderVfos();connect();' +
|
||||
'document.addEventListener("click",()=>{if(audioCtx?.state==="suspended")audioCtx.resume();});' +
|
||||
'document.addEventListener("click",function(){if(audioCtx&&audioCtx.state==="suspended")audioCtx.resume();});' +
|
||||
'setTimeout(showAudioOverlay,300);' +
|
||||
'</script></body></html>';
|
||||
end;
|
||||
|
||||
|
||||
+17
-1
@@ -42,6 +42,22 @@ unit WebServer;
|
||||
дескриптора не прерывает блокирующий fpAccept/fpRecv в чужом потоке —
|
||||
только shutdown(SHUT_RDWR) гарантированно разблокирует их, позволяя
|
||||
потокам выйти и WaitFor завершиться без зависания.
|
||||
- (Audio fix 1) Исправлена константа OPUS_APPLICATION_AUDIO: было 2101
|
||||
(невалидное значение), стало 2049 — правильное значение. Неверная
|
||||
константа приводила к Err!=0 из opus_encoder_create, FOpusEnc=nil,
|
||||
FOpusReady=false — аудио не кодировалось совсем.
|
||||
- (Audio fix 2) Заголовки COOP/COEP убраны — они блокировали WebSocket
|
||||
и загрузку CDN ресурсов (fonts, opus-decoder), из-за чего
|
||||
FWebClientActive никогда не становился true и десктоп звук не
|
||||
отключался при подключении веб-клиента.
|
||||
- (Audio fix 3) В JS исправлен вызов декодера: decodeFrame → decode
|
||||
(актуальный API opus-decoder@0.7.7). decodeFrame не существует в этой
|
||||
версии — silent fail, звука нет.
|
||||
- (Audio fix 4) Добавлен оверлей "Click to start audio" — AudioContext
|
||||
нельзя создать из WebSocket callback (не user gesture). Оверлей
|
||||
гарантирует создание AudioContext при первом кликe пользователя.
|
||||
- (Audio fix 5) Буферизация Opus-пакетов пока WASM не инициализирован —
|
||||
первые пакеты больше не теряются при медленной загрузке CDN.
|
||||
}
|
||||
|
||||
{$IFDEF FPC}
|
||||
@@ -375,7 +391,7 @@ begin
|
||||
end;
|
||||
|
||||
FOpusEnc := FOpusCreate(OPUS_SAMPLE_RATE, OPUS_CHANNELS,
|
||||
2101 {OPUS_APPLICATION_AUDIO}, @Err);
|
||||
2049 {OPUS_APPLICATION_AUDIO}, @Err);
|
||||
if (FOpusEnc = nil) or (Err <> 0) then
|
||||
begin
|
||||
FreeLibrary(FOpusLib); FOpusLib := 0; Exit;
|
||||
|
||||
+3
-3
@@ -259,7 +259,7 @@
|
||||
},
|
||||
" 0: 0: 0: 0: 0: 0" : {
|
||||
"global" : {
|
||||
"volume" : 59,
|
||||
"volume" : 76,
|
||||
"drive_level" : 50,
|
||||
"active_vfo" : 0,
|
||||
"nr_enabled" : false,
|
||||
@@ -285,7 +285,7 @@
|
||||
"span_hz" : 1.9200000000000000E+005
|
||||
},
|
||||
"1" : {
|
||||
"vfo_a" : 3.7310000000000000E+006,
|
||||
"vfo_a" : 3.6400000000000000E+006,
|
||||
"vfo_b" : 3.7500000000000000E+006,
|
||||
"mode" : 0,
|
||||
"filter_idx" : 5,
|
||||
@@ -307,7 +307,7 @@
|
||||
"span_hz" : 1.9200000000000000E+005
|
||||
},
|
||||
"3" : {
|
||||
"vfo_a" : 7.1530000000000000E+006,
|
||||
"vfo_a" : 7.1470000000000000E+006,
|
||||
"vfo_b" : 7.1000000000000000E+006,
|
||||
"mode" : 0,
|
||||
"filter_idx" : 5,
|
||||
|
||||
Reference in New Issue
Block a user