// Rehearser microphone recording overlay and controls. // Loaded before rehearser.js; shared state and playback helpers resolve at call time. (function (root) { // ── Recording overlay ─────────────────────────────────────────────────────── function showRecOverlay(line) { const overlay = $('reh-rec-overlay'); if (!overlay) return; overlay.hidden = false; const cue = $('reh-rec-cue'), c = rehState.cast[line.speaker] || { color: '#89b4fa' }; if (cue) cue.innerHTML = `${escHtml(line.speaker)} — your line:
${escHtml(stripMarkdown(line.text))}
`; if ($('reh-rec-preview')) { $('reh-rec-preview').style.display='none'; $('reh-rec-preview').src=''; } if ($('reh-rec-confirm-row')) $('reh-rec-confirm-row').hidden = true; if ($('reh-rec-start')) $('reh-rec-start').disabled = false; if ($('reh-rec-stop')) $('reh-rec-stop').disabled = true; if ($('reh-rec-time')) $('reh-rec-time').textContent = '0:00'; rehState.lastRecBlob = null; } function hideRecOverlay() { const overlay = $('reh-rec-overlay'); if (overlay) overlay.hidden = true; stopRehMic(); } // ── Mic recording ─────────────────────────────────────────────────────────── function rehRenderMeter(level=0, db=-Infinity, clipped=false) { const meter = $('reh-mic-meter'); if (!meter) return; if (!meter.children.length) for (let i=0;i<18;i++){const b=document.createElement('div');b.className='bar';meter.appendChild(b);} const active = Math.round(Math.max(0,Math.min(1,level))*meter.children.length); [...meter.children].forEach((bar,i)=>{ bar.className='bar'; bar.style.height=(7+Math.min(i,active)*1.55)+'px'; if(i-12&&i>11)bar.classList.add('hot');if(clipped&&i>14)bar.classList.add('clip');} }); const el=$('reh-db-readout');if(el)el.textContent=Number.isFinite(db)?db.toFixed(1)+' dB':'-∞ dB'; } function rehStartMeter() { if (!rehState.recAnalyser) return; if (rehState.recMeterRaf) cancelAnimationFrame(rehState.recMeterRaf); const data=new Float32Array(rehState.recAnalyser.fftSize), canvas=$('reh-live-wave'), RING=300, ADD=10; rehState.recWaveRing = new Float32Array(RING); const tick=()=>{ rehState.recAnalyser.getFloatTimeDomainData(data); let sum=0,peak=0; for(const s of data){sum+=s*s;peak=Math.max(peak,Math.abs(s));} const rms=Math.sqrt(sum/data.length), db=rms>0?20*Math.log10(rms):-Infinity; rehRenderMeter((db+60)/60,db,peak>0.98); if(canvas&&rehState.recWaveRing){ const ring=rehState.recWaveRing; ring.copyWithin(0,ADD); for(let i=0;i0.98?'#f38ba8':db>-12?'#f9e2af':'#a6e3a1'; ctx.lineWidth=1.5; const mid=h/2; for(let i=0;i{try{if(n)n.disconnect();}catch(_){}}); if (rehState.recStream) rehState.recStream.getTracks().forEach(t=>t.stop()); if (rehState.recDestStream) rehState.recDestStream.getTracks().forEach(t=>t.stop()); if (rehState.recAudioCtx) rehState.recAudioCtx.close().catch(()=>{}); Object.assign(rehState,{recStream:null,recDestStream:null,recSourceNode:null,recGainNode:null,recAnalyser:null,recAudioCtx:null,recWaveRing:null}); rehRenderMeter(); const wc=$('reh-live-wave');if(wc)wc.getContext('2d').clearRect(0,0,wc.width,wc.height); } $('reh-rec-start')?.addEventListener('click', async () => { try { await startRehMic(); rehState.recChunks=[]; rehState.recSecs=0; if($('reh-rec-time'))$('reh-rec-time').textContent='0:00'; if($('reh-rec-start'))$('reh-rec-start').disabled=true; if($('reh-rec-stop'))$('reh-rec-stop').disabled=false; if($('reh-rec-confirm-row'))$('reh-rec-confirm-row').hidden=true; rehState.recTimer=setInterval(()=>{ rehState.recSecs++; if($('reh-rec-time'))$('reh-rec-time').textContent=Math.floor(rehState.recSecs/60)+':'+String(rehState.recSecs%60).padStart(2,'0'); },1000); rehState.mediaRec=new MediaRecorder(rehState.recDestStream||rehState.recStream,{audioBitsPerSecond:256000}); rehState.mediaRec.ondataavailable=e=>{if(e.data.size)rehState.recChunks.push(e.data);}; rehState.mediaRec.onstop=()=>{ clearInterval(rehState.recTimer); if($('reh-rec-start'))$('reh-rec-start').disabled=false; if($('reh-rec-stop'))$('reh-rec-stop').disabled=true; const blob=new Blob(rehState.recChunks,{type:rehState.mediaRec.mimeType||'audio/webm'}); const url=URL.createObjectURL(blob); const p=$('reh-rec-preview');if(p){p.src=url;p.style.display='';} if($('reh-rec-confirm-row'))$('reh-rec-confirm-row').hidden=false; rehState.lastRecBlob=blob; }; rehState.mediaRec.start(100); } catch(e){stopRehMic();toast(await microphoneErrorMessage(e),'error');} }); $('reh-rec-stop')?.addEventListener('click', () => { if(rehState.mediaRec?.state!=='inactive')rehState.mediaRec.stop(); }); $('reh-rec-keep')?.addEventListener('click', () => { if (rehState.lastRecBlob) rehState.clips.push({lineIndex:rehState.lineIndex, speaker:rehState.lines[rehState.lineIndex]?.speaker, type:'me', blob:rehState.lastRecBlob}); stopRehMic(); hideRecOverlay(); rehState.lineIndex++; startPlay(); }); $('reh-rec-redo')?.addEventListener('click', () => { stopRehMic(); const l=rehState.lines[rehState.lineIndex]; if(l)showRecOverlay(l); }); $('reh-skip-line')?.addEventListener('click', () => { rehState.clips.push({lineIndex:rehState.lineIndex, speaker:rehState.lines[rehState.lineIndex]?.speaker, type:'skip'}); stopRehMic(); hideRecOverlay(); rehState.lineIndex++; startPlay(); }); Object.assign(root, { showRecOverlay, hideRecOverlay, startRehMic, stopRehMic, microphoneErrorMessage, }); root.TTVC=root.TTVC||{}; root.TTVC.rehearser=root.TTVC.rehearser||{}; Object.assign(root.TTVC.rehearser,{ recording:{ showOverlay:showRecOverlay, hideOverlay:hideRecOverlay, start:startRehMic, stop:stopRehMic, }, }); })(window);