Download encoder.html from vsmdvic/novatx: direct link, hf CLI and curl.
- Browser
- Download file 22.1 kB
-
https://huggingface.co/spaces/vsmdvic/novatx/resolve/main/encoder.html
- Command line
-
hf download hf://spaces/vsmdvic/novatx/encoder.html
-
curl -L -o encoder.html https://huggingface.co/spaces/vsmdvic/novatx/resolve/main/encoder.html
22.1 kB
| <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 */ | |
| 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);} | |
| /* BLOCKS */ | |
| .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;} | |
| /* FORM */ | |
| 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;} | |
| /* BUTTONS */ | |
| .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;} | |
| /* PROGRESS */ | |
| .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 */ | |
| .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 */ | |
| .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 */ | |
| .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);} | |
| /* CANVAS */ | |
| #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;} | |
| /* SPECTRUM */ | |
| #specCv{display:block;width:100%;height:52px;background:var(--ink);image-rendering:pixelated;} | |
| /* WAV badge */ | |
| .wav-badge{font-size:9px;color:var(--ink3);margin-top:8px;} | |
| /* Tabs inside encoder */ | |
| .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> | |
| <!-- ENCODER TABS --> | |
| <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> | |
| <!-- FSK PAGE --> | |
| <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> | |
| <!-- SSTV PAGE --> | |
| <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> | |
| <!-- STATS --> | |
| <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> | |
| <!-- SPECTRUM --> | |
| <div class="block"> | |
| <div class="block-head"><span class="bh-title">Spectrum</span></div> | |
| <canvas id="specCv"></canvas> | |
| </div> | |
| <!-- LOG --> | |
| <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> | |
| <!-- GUIDE --> | |
| <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> | |
| ; | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // ENCODER โ FSK + SSTV B&W Transmitter | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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; | |
| } | |
| // โโ Tabs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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(); | |
| }; | |
| }); | |
| // โโ Sliders โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| document.getElementById('fskVol').oninput=function(){document.getElementById('fskVolV').textContent=this.value;}; | |
| document.getElementById('sstvVol').oninput=function(){document.getElementById('sstvVolV').textContent=this.value;}; | |
| // โโ Stats โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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(); | |
| // โโ Log โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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(()=>{}); | |
| } | |
| // โโ WAV Export โโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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');} | |
| }; | |
| // โโ Spectrum โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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); | |
| } | |
| } | |
| // โโ FSK TX โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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'; | |
| }; | |
| // โโ SSTV TX โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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); | |
| } | |
| }; | |
| // VIS header Robot8 B&W (0x02) โ compatible with all B&W decoders | |
| 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> | |