162 lines
7.5 KiB
JavaScript
162 lines
7.5 KiB
JavaScript
// 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 = `<span class="reh-rec-cue-name" style="color:${c.color}">${escHtml(line.speaker)}</span> — your line:<div class="reh-rec-cue-text">${escHtml(stripMarkdown(line.text))}</div>`;
|
|
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<active){bar.classList.add('on');if(db>-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;i<ADD;i++) ring[RING-ADD+i]=data[Math.floor(i*data.length/ADD)];
|
|
const ctx=canvas.getContext('2d'),w=canvas.width,h=canvas.height;
|
|
ctx.clearRect(0,0,w,h); ctx.beginPath();
|
|
ctx.strokeStyle=peak>0.98?'#f38ba8':db>-12?'#f9e2af':'#a6e3a1'; ctx.lineWidth=1.5;
|
|
const mid=h/2;
|
|
for(let i=0;i<RING;i++){const x=(i/RING)*w,y=mid-ring[i]*mid*0.85;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}
|
|
ctx.stroke();
|
|
}
|
|
rehState.recMeterRaf=requestAnimationFrame(tick);
|
|
};
|
|
tick();
|
|
}
|
|
|
|
async function startRehMic() {
|
|
if (rehState.recDestStream) return;
|
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
rehState.recStream = await requestMicrophoneStream({ raw: true });
|
|
if (AudioCtx) {
|
|
rehState.recAudioCtx = new AudioCtx();
|
|
rehState.recSourceNode = rehState.recAudioCtx.createMediaStreamSource(rehState.recStream);
|
|
rehState.recGainNode = rehState.recAudioCtx.createGain();
|
|
rehState.recAnalyser = rehState.recAudioCtx.createAnalyser();
|
|
rehState.recAnalyser.fftSize = 1024;
|
|
const dest = rehState.recAudioCtx.createMediaStreamDestination();
|
|
rehState.recSourceNode.connect(rehState.recGainNode);
|
|
rehState.recGainNode.connect(rehState.recAnalyser);
|
|
rehState.recGainNode.connect(dest);
|
|
rehState.recDestStream = dest.stream;
|
|
rehStartMeter();
|
|
} else { rehState.recDestStream = rehState.recStream; }
|
|
}
|
|
|
|
function stopRehMic() {
|
|
if (rehState.recMeterRaf) cancelAnimationFrame(rehState.recMeterRaf);
|
|
rehState.recMeterRaf = null;
|
|
[rehState.recSourceNode,rehState.recGainNode,rehState.recAnalyser].forEach(n=>{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);
|