mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
add browser mic TX: Opus-encoded mic audio from web client to WDSP TX chain
- Browser: AudioWorklet accumulates 960-sample frames, AudioEncoder encodes to Opus, sends binary WS frame 'M' + raw Opus packet - Server: opcode $02 handler decodes Opus via libopus, calls OnWebMic callback - MainForm: WebOnMic converts PSingle→Double, pushes to PushTXMicSamplesD; ApplyMOX switches mic source to txmsWeb when web client active, restores on TX-off - WDSPEngine: added txmsWeb to TTXMicSource enum Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+41
-1
@@ -469,7 +469,8 @@ begin
|
||||
'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||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")+" | "+(cur.transmitting?"TX":"RX")+" | "+mn+" | "+((activeVfoHz()/1e6).toFixed(3))+" MHz";}' +
|
||||
'stEl.textContent=(cur.connected?"OK":"OFF")+" | "+(cur.running?"RUN":"STOP")+" | "+(cur.transmitting?"TX":"RX")+" | "+mn+" | "+((activeVfoHz()/1e6).toFixed(3))+" MHz";' +
|
||||
'const nowTx=!!cur.transmitting;if(nowTx!==prevTx){prevTx=nowTx;if(nowTx)startTXMic();else stopTXMic();}}' +
|
||||
|
||||
'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];}' +
|
||||
@@ -584,6 +585,7 @@ begin
|
||||
|
||||
// Opus: async decodeFrame, очередь, оверлей для user gesture
|
||||
'let ws=null,opDec=null,opRdy=false,opQueue=[];' +
|
||||
'let txMicStream=null,txMicACtx=null,txMicProc=null,txMicEnc=null,prevTx=false;' +
|
||||
'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});' +
|
||||
@@ -607,6 +609,44 @@ begin
|
||||
'latEl.textContent="lat: "+Math.max(0,(audioT-now)*1000).toFixed(0)+"ms | Opus";' +
|
||||
'}catch(e){console.error("playOpus:",e);}}' +
|
||||
|
||||
'async function startTXMic(){if(txMicStream)return;' +
|
||||
'try{' +
|
||||
'txMicStream=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:48000,echoCancellation:true,noiseSuppression:true}});' +
|
||||
'txMicACtx=new AudioContext({sampleRate:48000});' +
|
||||
'const msrc=txMicACtx.createMediaStreamSource(txMicStream);' +
|
||||
'txMicEnc=new AudioEncoder({' +
|
||||
'output:(chunk)=>{' +
|
||||
'if(!ws||ws.readyState!==1)return;' +
|
||||
'const pkt=new Uint8Array(1+chunk.byteLength);pkt[0]=0x4D;' +
|
||||
'chunk.copyTo(pkt.subarray(1));ws.send(pkt.buffer);},' +
|
||||
'error:(e)=>console.error("txenc:",e)});' +
|
||||
'txMicEnc.configure({codec:"opus",sampleRate:48000,numberOfChannels:1,bitrate:32000});' +
|
||||
// AudioWorklet processor: accumulates render quanta (128 samples) into 960-sample Opus frames
|
||||
'const wcSrc=' +
|
||||
'"class P extends AudioWorkletProcessor{" +' +
|
||||
'"constructor(){super();this.a=new Float32Array(960);this.p=0;}" +' +
|
||||
'"process(inp){const d=inp[0]&&inp[0][0];if(!d)return true;" +' +
|
||||
'"let o=0;while(o<d.length){const t=Math.min(960-this.p,d.length-o);" +' +
|
||||
'"this.a.set(d.subarray(o,o+t),this.p);this.p+=t;o+=t;" +' +
|
||||
'"if(this.p===960){this.port.postMessage(this.a.slice());this.p=0;}}" +' +
|
||||
'"return true;}}registerProcessor(\"txmic\",P);";' +
|
||||
'const burl=URL.createObjectURL(new Blob([wcSrc],{type:"application/javascript"}));' +
|
||||
'await txMicACtx.audioWorklet.addModule(burl);URL.revokeObjectURL(burl);' +
|
||||
'let txTs=0;' +
|
||||
'txMicProc=new AudioWorkletNode(txMicACtx,"txmic");' +
|
||||
'txMicProc.port.onmessage=function(ev){' +
|
||||
'if(!txMicEnc||txMicEnc.state!=="configured")return;' +
|
||||
'const ad=new AudioData({format:"f32-planar",sampleRate:48000,numberOfFrames:960,numberOfChannels:1,timestamp:txTs,data:ev.data});' +
|
||||
'txTs+=20000;txMicEnc.encode(ad);ad.close();};' +
|
||||
'msrc.connect(txMicProc);txMicProc.connect(txMicACtx.destination);' +
|
||||
'}catch(e){console.error("startTXMic:",e);stopTXMic();}}' +
|
||||
|
||||
'function stopTXMic(){' +
|
||||
'if(txMicProc){txMicProc.disconnect();txMicProc=null;}' +
|
||||
'if(txMicEnc){try{txMicEnc.close();}catch(ex){}txMicEnc=null;}' +
|
||||
'if(txMicACtx){txMicACtx.close();txMicACtx=null;}' +
|
||||
'if(txMicStream){txMicStream.getTracks().forEach(function(t){t.stop();});txMicStream=null;}}' +
|
||||
|
||||
'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();};' +
|
||||
'ws.onclose=()=>{stEl.textContent="disconnected...";setTimeout(connect,3000);};' +
|
||||
|
||||
Reference in New Issue
Block a user