| <!DOCTYPE html> |
| <html lang="ro"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"> |
| <title>TX โ Encoder</title> |
| <link href="https://fonts.googleapis.com/css2?family=Martian+Mono:wght@300;400;600&display=swap" rel="stylesheet"> |
| <style> |
| *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent} |
| :root{ |
| --bg:#f5f4f0; |
| --ink:#0a0a0a; |
| --ink2:#555; |
| --ink3:#aaa; |
| --line:#ddd; |
| --line2:#e8e8e8; |
| --surface:#fff; |
| --accent:#0a0a0a; |
| } |
| html,body{background:var(--bg);color:var(--ink);font-family:'Martian Mono',monospace;} |
| body{max-width:520px;margin:0 auto;min-height:100vh;padding-bottom:48px;} |
| |
| |
| nav{ |
| display:flex;align-items:center;justify-content:space-between; |
| padding:14px 20px; |
| border-bottom:1px solid var(--line); |
| background:var(--bg); |
| position:sticky;top:0;z-index:99; |
| } |
| .nav-logo{font-size:11px;font-weight:600;letter-spacing:4px;text-transform:uppercase;color:var(--ink);} |
| .nav-switch{ |
| display:flex;gap:0;border:1px solid var(--line);border-radius:6px;overflow:hidden; |
| } |
| .nav-switch a{ |
| font-family:'Martian Mono',monospace; |
| font-size:9px;letter-spacing:2px;text-transform:uppercase; |
| padding:7px 14px;text-decoration:none;color:var(--ink2); |
| transition:.15s;white-space:nowrap; |
| } |
| .nav-switch a:first-child{border-right:1px solid var(--line);} |
| .nav-switch a.on{background:var(--ink);color:#fff;} |
| .nav-switch a:hover:not(.on){background:var(--line2);} |
| |
| |
| .block{ |
| margin:16px 16px 0; |
| background:var(--surface); |
| border:1px solid var(--line); |
| border-radius:10px; |
| overflow:hidden; |
| } |
| .block-head{ |
| padding:11px 16px; |
| border-bottom:1px solid var(--line2); |
| display:flex;align-items:center;justify-content:space-between; |
| } |
| .bh-title{font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--ink2);} |
| .bh-r{font-size:9px;color:var(--ink3);} |
| .block-body{padding:16px;} |
| |
| |
| label{ |
| font-size:9px;letter-spacing:1px;text-transform:uppercase; |
| color:var(--ink3);display:block;margin-bottom:5px; |
| } |
| select,textarea{ |
| font-family:'Martian Mono',monospace;font-size:11px; |
| width:100%;background:var(--bg); |
| border:1px solid var(--line);border-radius:6px; |
| color:var(--ink);padding:9px 12px;outline:none; |
| -webkit-appearance:none;transition:.15s; |
| } |
| select:focus,textarea:focus{border-color:var(--ink3);} |
| textarea{resize:none;min-height:72px;line-height:1.7;} |
| .field{margin-bottom:14px;} |
| .field:last-child{margin-bottom:0;} |
| .row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;} |
| .range-wrap{display:flex;align-items:center;gap:10px;} |
| input[type=range]{ |
| -webkit-appearance:none;flex:1;height:2px; |
| background:var(--line);border-radius:2px;cursor:pointer; |
| } |
| input[type=range]::-webkit-slider-thumb{ |
| -webkit-appearance:none;width:16px;height:16px; |
| border-radius:50%;background:var(--ink);cursor:pointer; |
| } |
| .range-val{font-size:10px;color:var(--ink2);min-width:28px;text-align:right;} |
| input[type=file]{display:none;} |
| |
| |
| .btn-row{display:flex;gap:8px;} |
| button{ |
| font-family:'Martian Mono',monospace; |
| font-size:10px;letter-spacing:1px;text-transform:uppercase; |
| padding:10px 16px;border-radius:7px;flex:1; |
| border:1px solid var(--line);background:transparent; |
| color:var(--ink2);cursor:pointer;transition:.15s; |
| } |
| button:active{opacity:.6;} |
| button.primary{background:var(--ink);color:#fff;border-color:var(--ink);} |
| button.primary:hover{background:#222;} |
| button:hover:not(.primary){background:var(--line2);} |
| button:disabled{opacity:.25;cursor:not-allowed;} |
| button.sm{flex:none;font-size:9px;padding:7px 12px;} |
| |
| |
| .prog{height:2px;background:var(--line2);border-radius:2px;overflow:hidden;margin-top:12px;} |
| .prog-b{height:100%;width:0;background:var(--ink);transition:width .1s;border-radius:2px;} |
| |
| |
| .stats{display:flex;border-top:1px solid var(--line2);} |
| .stat{flex:1;padding:10px 14px;border-right:1px solid var(--line2);} |
| .stat:last-child{border-right:none;} |
| .stat-l{font-size:8px;letter-spacing:1px;text-transform:uppercase;color:var(--ink3);} |
| .stat-v{font-size:14px;color:var(--ink);margin-top:3px;font-weight:600;} |
| |
| |
| .log-wrap{border-top:1px solid var(--line2);} |
| .log-head{ |
| padding:6px 14px; |
| display:flex;align-items:center;justify-content:space-between; |
| border-bottom:1px solid var(--line2); |
| } |
| .log-head span{font-size:8px;letter-spacing:1px;text-transform:uppercase;color:var(--ink3);} |
| .log{ |
| padding:10px 14px;font-size:10px;line-height:1.8; |
| color:var(--ink3);max-height:80px;overflow-y:auto; |
| } |
| .log .ok{color:var(--ink);} |
| .log .warn{color:#888;} |
| .log .err{color:#bbb;} |
| |
| |
| .upload{ |
| border:1px dashed var(--line);border-radius:8px;padding:20px; |
| text-align:center;color:var(--ink3);font-size:10px; |
| letter-spacing:1px;cursor:pointer;transition:.2s;text-transform:uppercase; |
| } |
| .upload:hover{border-color:var(--ink3);} |
| |
| |
| #txCv{display:block;width:100%;background:var(--bg);image-rendering:pixelated;} |
| .canvas-wrap{background:var(--bg);border:1px solid var(--line2);border-radius:6px;overflow:hidden;} |
| |
| |
| #specCv{display:block;width:100%;height:52px;background:var(--ink);image-rendering:pixelated;} |
| |
| |
| .wav-badge{font-size:9px;color:var(--ink3);margin-top:8px;} |
| |
| |
| .enc-tabs{display:flex;gap:0;border-bottom:1px solid var(--line2);} |
| .enc-tab{ |
| font-family:'Martian Mono',monospace; |
| font-size:9px;letter-spacing:2px;text-transform:uppercase; |
| padding:10px 18px;border:none;background:transparent; |
| color:var(--ink3);cursor:pointer;border-bottom:2px solid transparent;transition:.15s; |
| } |
| .enc-tab.on{color:var(--ink);border-bottom-color:var(--ink);} |
| .enc-page{display:none;padding:16px;} |
| .enc-page.on{display:block;} |
| |
| ::-webkit-scrollbar{width:3px;} |
| ::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;} |
| </style> |
| </head> |
| <body> |
|
|
| <nav> |
| <div class="nav-logo">RadioยทTX</div> |
| <div class="nav-switch"> |
| <a href="encoder.html" class="on">TX Encoder</a> |
| <a href="receiver.html">RX Decoder</a> |
| </div> |
| </nav> |
|
|
| |
| <div class="block" style="margin-top:16px;margin-left:16px;margin-right:16px;"> |
| <div class="enc-tabs"> |
| <button class="enc-tab on" data-enc="fsk">FSK Text</button> |
| <button class="enc-tab" data-enc="sstv">SSTV Image</button> |
| </div> |
|
|
| |
| <div class="enc-page on" id="enc-fsk"> |
| <div class="field"> |
| <label>Mesaj de transmis</label> |
| <textarea id="fskText" placeholder="Scrie mesajul tฤu...">CQ CQ CQ DE RADIO-TX 73</textarea> |
| </div> |
| <div class="row2"> |
| <div class="field"> |
| <label>Baud rate</label> |
| <select id="fskBaud"> |
| <option value="45">45 bd โ RTTY</option> |
| <option value="100" selected>100 bd</option> |
| <option value="300">300 bd</option> |
| </select> |
| </div> |
| <div class="field"> |
| <label>Mark freq</label> |
| <select id="fskMark"> |
| <option value="1200" selected>1200 Hz</option> |
| <option value="1275">1275 Hz</option> |
| <option value="2125">2125 Hz</option> |
| </select> |
| </div> |
| </div> |
| <div class="row2"> |
| <div class="field"> |
| <label>Repetฤri</label> |
| <select id="fskRep"> |
| <option value="1">1ร</option> |
| <option value="3" selected>3ร</option> |
| <option value="5">5ร</option> |
| </select> |
| </div> |
| <div class="field"> |
| <label>Volum โ <span id="fskVolV">80</span>%</label> |
| <div class="range-wrap" style="margin-top:8px;"> |
| <input type="range" id="fskVol" min="10" max="100" value="80"> |
| </div> |
| </div> |
| </div> |
| <div class="btn-row"> |
| <button class="primary" id="btnFskTx">โถ Transmit</button> |
| <button id="btnFskStop" disabled>โ Stop</button> |
| <button class="sm" id="btnFskWav" disabled>โฌ WAV</button> |
| </div> |
| <div class="prog"><div class="prog-b" id="fskProg"></div></div> |
| <div class="wav-badge" id="fskWavBadge"></div> |
| </div> |
|
|
| |
| <div class="enc-page" id="enc-sstv"> |
| <div class="field"> |
| <div class="upload" id="uploadZone" onclick="document.getElementById('imgIn').click()"> |
| โ Apasฤ pentru imagine<br> |
| <span style="font-size:9px;opacity:.5;margin-top:4px;display:block">PNG / JPG / WEBP</span> |
| </div> |
| <input type="file" id="imgIn" accept="image/*"> |
| </div> |
| <div id="txPrevWrap" style="display:none;" class="field"> |
| <div class="canvas-wrap"><canvas id="txCv"></canvas></div> |
| </div> |
| <div class="row2"> |
| <div class="field"> |
| <label>Mod SSTV</label> |
| <select id="sstvMode"> |
| <option value="r8">Robot8 โ 8s</option> |
| <option value="r36" selected>Robot36 โ 36s</option> |
| <option value="r72">Robot72 โ 72s</option> |
| </select> |
| </div> |
| <div class="field"> |
| <label>Volum โ <span id="sstvVolV">80</span>%</label> |
| <div class="range-wrap" style="margin-top:8px;"> |
| <input type="range" id="sstvVol" min="10" max="100" value="80"> |
| </div> |
| </div> |
| </div> |
| <div class="btn-row"> |
| <button class="primary" id="btnSstvTx" disabled>โถ Transmit</button> |
| <button id="btnSstvStop" disabled>โ Stop</button> |
| <button class="sm" id="btnSstvWav" disabled>โฌ WAV</button> |
| </div> |
| <div class="prog"><div class="prog-b" id="sstvProg"></div></div> |
| <div class="wav-badge" id="sstvWavBadge"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="block"> |
| <div class="block-head"> |
| <span class="bh-title">Stats</span> |
| <span class="bh-r" id="txStatusBadge">IDLE</span> |
| </div> |
| <div class="stats"> |
| <div class="stat"><div class="stat-l">Mark</div><div class="stat-v" id="sMark">1200</div></div> |
| <div class="stat"><div class="stat-l">Space</div><div class="stat-v" id="sSpace">1030</div></div> |
| <div class="stat"><div class="stat-l">Duratฤ</div><div class="stat-v" id="sDur">โ</div></div> |
| </div> |
| </div> |
|
|
| |
| <div class="block"> |
| <div class="block-head"><span class="bh-title">Spectrum</span></div> |
| <canvas id="specCv"></canvas> |
| </div> |
|
|
| |
| <div class="block"> |
| <div class="log-wrap"> |
| <div class="log-head"> |
| <span>Log</span> |
| <button class="sm" onclick="copyLog('txLog')" style="font-size:8px;padding:4px 10px;">Copy</button> |
| </div> |
| <div class="log" id="txLog"><div class="ok">> Encoder ready.</div></div> |
| </div> |
| </div> |
|
|
| |
| <div class="block"> |
| <div class="block-head"><span class="bh-title">HT Guide</span></div> |
| <div class="block-body" style="font-size:10px;color:var(--ink2);line-height:2;"> |
| 1. Volum boxe <strong>60โ75%</strong> โ evitฤ clipping<br> |
| 2. Microfon HT la <strong>5โ10 cm</strong><br> |
| 3. Apasฤ <strong>PTT</strong> pe HT<br> |
| 4. Apasฤ <strong>โถ Transmit</strong><br> |
| 5. Elibereazฤ PTT dupฤ bara 100%<br><br> |
| FSK: Mark <strong>1200Hz</strong> ยท Space <strong>1030Hz</strong> ยท Shift <strong>170Hz</strong><br> |
| SSTV: Sync <strong>1200Hz</strong> ยท Negru <strong>1500Hz</strong> ยท Alb <strong>2300Hz</strong> |
| </div> |
| </div> |
|
|
| <script> |
| 'use strict'; |
| |
| |
| |
| |
| const SS=1200,SB=1500,SW=2300,SyncMs=9; |
| const MODES={ |
| r8: {w:160,h:120,msPerLine:60, name:'Robot8 B&W'}, |
| r36:{w:320,h:240,msPerLine:88, name:'Robot36 B&W'}, |
| r72:{w:320,h:240,msPerLine:176,name:'Robot72 B&W'}, |
| }; |
| |
| let AC=null,fskNode=null,sstvNode=null,txActive=false; |
| let fskPcm=null,fskSr=48000,sstvPcm=null,sstvSr=48000; |
| |
| function getAC(){ |
| if(!AC) AC=new(window.AudioContext||window.webkitAudioContext)(); |
| if(AC.state==='suspended') AC.resume(); |
| return AC; |
| } |
| |
| |
| document.querySelectorAll('.enc-tab').forEach(t=>{ |
| t.onclick=()=>{ |
| document.querySelectorAll('.enc-tab').forEach(x=>x.classList.remove('on')); |
| document.querySelectorAll('.enc-page').forEach(x=>x.classList.remove('on')); |
| t.classList.add('on'); |
| document.getElementById('enc-'+t.dataset.enc).classList.add('on'); |
| updateStats(); |
| }; |
| }); |
| |
| |
| document.getElementById('fskVol').oninput=function(){document.getElementById('fskVolV').textContent=this.value;}; |
| document.getElementById('sstvVol').oninput=function(){document.getElementById('sstvVolV').textContent=this.value;}; |
| |
| |
| function updateStats(){ |
| const baud=+document.getElementById('fskBaud').value; |
| const mark=+document.getElementById('fskMark').value; |
| const space=mark-170; |
| const txt=document.getElementById('fskText').value; |
| const reps=+document.getElementById('fskRep').value; |
| document.getElementById('sMark').textContent=mark; |
| document.getElementById('sSpace').textContent=space; |
| document.getElementById('sDur').textContent=((txt.length*reps*10)/baud).toFixed(1)+'s'; |
| } |
| ['fskBaud','fskMark','fskRep'].forEach(id=>document.getElementById(id).onchange=updateStats); |
| document.getElementById('fskText').oninput=updateStats; |
| updateStats(); |
| |
| |
| function log(msg,cls=''){ |
| const el=document.getElementById('txLog'); |
| const d=document.createElement('div'); |
| d.className=cls;d.textContent='> '+msg; |
| el.appendChild(d);el.scrollTop=el.scrollHeight; |
| } |
| function copyLog(id){ |
| const txt=[...document.getElementById(id).children].map(n=>n.textContent).join('\n'); |
| navigator.clipboard?.writeText(txt).catch(()=>{}); |
| } |
| |
| |
| function dlWav(f32,sr,name){ |
| const n=f32.length,ab=new ArrayBuffer(44+n*2),v=new DataView(ab); |
| const s=(o,str)=>{for(let i=0;i<str.length;i++)v.setUint8(o+i,str.charCodeAt(i));}; |
| s(0,'RIFF');v.setUint32(4,36+n*2,true);s(8,'WAVE');s(12,'fmt '); |
| v.setUint32(16,16,true);v.setUint16(20,1,true);v.setUint16(22,1,true); |
| v.setUint32(24,sr,true);v.setUint32(28,sr*2,true);v.setUint16(32,2,true);v.setUint16(34,16,true); |
| s(36,'data');v.setUint32(40,n*2,true); |
| for(let i=0;i<n;i++) v.setInt16(44+i*2,Math.max(-32768,Math.min(32767,f32[i]*32767)),true); |
| const url=URL.createObjectURL(new Blob([ab],{type:'audio/wav'})); |
| Object.assign(document.createElement('a'),{href:url,download:name}).click(); |
| setTimeout(()=>URL.revokeObjectURL(url),5000); |
| } |
| document.getElementById('btnFskWav').onclick=()=>{ |
| if(fskPcm){dlWav(fskPcm,fskSr,'fsk-'+Date.now()+'.wav');log('โฌ WAV FSK descฤrcat.','ok');} |
| }; |
| document.getElementById('btnSstvWav').onclick=()=>{ |
| if(sstvPcm){dlWav(sstvPcm,sstvSr,'sstv-'+Date.now()+'.wav');log('โฌ WAV SSTV descฤrcat.','ok');} |
| }; |
| |
| |
| function drawSpec(an){ |
| const cv=document.getElementById('specCv'); |
| cv.width=cv.offsetWidth||480; |
| const ctx=cv.getContext('2d'),W=cv.width,H=cv.height; |
| const fr=new Float32Array(an.frequencyBinCount); |
| an.getFloatFrequencyData(fr); |
| ctx.fillStyle='#0a0a0a';ctx.fillRect(0,0,W,H); |
| const bw=W/fr.length; |
| for(let i=0;i<fr.length;i++){ |
| const v=Math.max(0,(fr[i]+100)/100); |
| const h=v*H; |
| const g=Math.floor(v*220); |
| ctx.fillStyle=`rgb(${g},${g},${g})`; |
| ctx.fillRect(Math.floor(i*bw),H-h,Math.max(1,Math.ceil(bw)),h); |
| } |
| } |
| |
| |
| document.getElementById('btnFskTx').onclick=()=>{ |
| const txt=document.getElementById('fskText').value; if(!txt.trim())return; |
| const baud=+document.getElementById('fskBaud').value; |
| const mark=+document.getElementById('fskMark').value; |
| const space=mark-170; |
| const vol=document.getElementById('fskVol').value/100; |
| const reps=+document.getElementById('fskRep').value; |
| const ac=getAC(),sr=ac.sampleRate; |
| const spb=sr/baud; |
| const msg=('\x05\x05\x05'+txt+' \x04').repeat(reps); |
| const total=Math.ceil(msg.length*10*spb+sr*1.5); |
| const buf=ac.createBuffer(1,total,sr); |
| const data=buf.getChannelData(0); |
| let off=0,ph=0; |
| const tone=(f,n)=>{ |
| const ns=Math.round(n); |
| for(let i=0;i<ns&&off<data.length;i++,off++){ |
| data[off]=Math.sin(ph)*vol; |
| ph=(ph+2*Math.PI*f/sr)%(2*Math.PI); |
| } |
| }; |
| tone(mark,sr*.15); |
| for(const ch of msg){ |
| const code=ch.charCodeAt(0)&0xFF; |
| tone(space,spb); |
| for(let b=0;b<8;b++) tone((code>>b)&1?mark:space,spb); |
| tone(mark,spb); |
| } |
| tone(mark,sr*.3); |
| fskPcm=data.slice(0,off); fskSr=sr; |
| document.getElementById('btnFskWav').disabled=false; |
| document.getElementById('fskWavBadge').textContent='WAV: '+(fskPcm.length/sr).toFixed(1)+'s ยท '+(fskPcm.length*2/1024|0)+'KB'; |
| const src=ac.createBufferSource(); src.buffer=buf; |
| const an=ac.createAnalyser(); an.fftSize=1024; |
| src.connect(an); an.connect(ac.destination); |
| fskNode=src; txActive=true; src.start(); |
| document.getElementById('btnFskTx').disabled=true; |
| document.getElementById('btnFskStop').disabled=false; |
| document.getElementById('txStatusBadge').textContent='TX FSK'; |
| log('โถ FSK '+txt.length+' chars ร '+reps+' @ '+baud+'bd','ok'); |
| const t0=ac.currentTime,dur=off/sr; |
| (function anim(){ |
| if(!txActive)return; |
| document.getElementById('fskProg').style.width=Math.min(100,(ac.currentTime-t0)/dur*100)+'%'; |
| drawSpec(an); |
| requestAnimationFrame(anim); |
| })(); |
| src.onended=()=>{ |
| txActive=false; |
| document.getElementById('btnFskTx').disabled=false; |
| document.getElementById('btnFskStop').disabled=true; |
| document.getElementById('txStatusBadge').textContent='DONE'; |
| document.getElementById('fskProg').style.width='100%'; |
| log('โ Transmisie completฤ.','ok'); |
| }; |
| }; |
| document.getElementById('btnFskStop').onclick=()=>{ |
| if(fskNode){try{fskNode.stop();}catch(e){}} |
| txActive=false; |
| document.getElementById('btnFskTx').disabled=false; |
| document.getElementById('btnFskStop').disabled=true; |
| document.getElementById('txStatusBadge').textContent='STOP'; |
| }; |
| |
| |
| document.getElementById('imgIn').onchange=function(e){ |
| const f=e.target.files[0]; if(!f)return; |
| const url=URL.createObjectURL(f); |
| const img=new Image(); |
| img.onload=()=>{ |
| const m=MODES[document.getElementById('sstvMode').value]; |
| const cv=document.getElementById('txCv'); |
| cv.width=m.w; cv.height=m.h; |
| cv.getContext('2d').drawImage(img,0,0,m.w,m.h); |
| document.getElementById('txPrevWrap').style.display='block'; |
| document.getElementById('uploadZone').textContent='โ '+f.name; |
| document.getElementById('btnSstvTx').disabled=false; |
| URL.revokeObjectURL(url); |
| log('Imagine รฎncฤrcatฤ: '+m.w+'ร'+m.h,'ok'); |
| }; |
| img.src=url; |
| }; |
| |
| document.getElementById('btnSstvTx').onclick=()=>{ |
| const k=document.getElementById('sstvMode').value; |
| const m=MODES[k]; |
| const vol=document.getElementById('sstvVol').value/100; |
| const ac=getAC(),sr=ac.sampleRate; |
| const cv=document.getElementById('txCv'); |
| const px=cv.getContext('2d').getImageData(0,0,m.w,m.h); |
| const lineMs=SyncMs+m.msPerLine; |
| const totalMs=m.h*lineMs+600; |
| const totalS=Math.ceil(sr*totalMs/1000)+sr; |
| const buf=ac.createBuffer(1,totalS,sr); |
| const data=buf.getChannelData(0); |
| let off=0,ph=0; |
| const tone=(f,ms)=>{ |
| const n=Math.round(sr*ms/1000); |
| for(let i=0;i<n&&off<data.length;i++,off++){ |
| data[off]=Math.sin(ph)*vol; |
| ph=(ph+2*Math.PI*f/sr)%(2*Math.PI); |
| } |
| }; |
| |
| tone(1900,300);tone(1200,10);tone(1900,300); |
| tone(1200,30); |
| const vis=k==='r8'?0x02:k==='r36'?0x08:0x0C; |
| for(let b=0;b<8;b++) tone((vis>>b)&1?1100:1300,30); |
| tone(1200,30); |
| const mspp=m.msPerLine/m.w; |
| for(let y=0;y<m.h;y++){ |
| tone(SS,SyncMs); |
| for(let x=0;x<m.w;x++){ |
| const i=(y*m.w+x)*4; |
| const luma=0.299*px.data[i]+0.587*px.data[i+1]+0.114*px.data[i+2]; |
| tone(SB+(luma/255)*(SW-SB),mspp); |
| } |
| } |
| tone(SS,50); |
| sstvPcm=data.slice(0,off); sstvSr=sr; |
| document.getElementById('btnSstvWav').disabled=false; |
| document.getElementById('sstvWavBadge').textContent='WAV: '+(sstvPcm.length/sr|0)+'s ยท '+(sstvPcm.length*2/1048576).toFixed(1)+'MB'; |
| const src=ac.createBufferSource(); src.buffer=buf; |
| const an=ac.createAnalyser(); an.fftSize=512; |
| src.connect(an); an.connect(ac.destination); |
| sstvNode=src; txActive=true; src.start(); |
| document.getElementById('btnSstvTx').disabled=true; |
| document.getElementById('btnSstvStop').disabled=false; |
| document.getElementById('txStatusBadge').textContent='TX SSTV'; |
| log('โถ '+m.name+' โ '+m.w+'ร'+m.h,'ok'); |
| const t0=ac.currentTime; |
| (function anim(){ |
| if(!txActive)return; |
| const p=Math.min(100,(ac.currentTime-t0)*1000/totalMs*100); |
| document.getElementById('sstvProg').style.width=p+'%'; |
| drawSpec(an); |
| requestAnimationFrame(anim); |
| })(); |
| src.onended=()=>{ |
| txActive=false; |
| document.getElementById('btnSstvTx').disabled=false; |
| document.getElementById('btnSstvStop').disabled=true; |
| document.getElementById('txStatusBadge').textContent='DONE'; |
| document.getElementById('sstvProg').style.width='100%'; |
| log('โ SSTV complet. WAV disponibil.','ok'); |
| }; |
| }; |
| document.getElementById('btnSstvStop').onclick=()=>{ |
| if(sstvNode){try{sstvNode.stop();}catch(e){}} |
| txActive=false; |
| document.getElementById('btnSstvTx').disabled=false; |
| document.getElementById('btnSstvStop').disabled=true; |
| document.getElementById('txStatusBadge').textContent='STOP'; |
| }; |
| </script> |
| </body> |
| </html> |
|
|