/* widget: Geluidsmeter */
/* =========================================================
Live noise meter — mic input stays local, never recorded or sent anywhere
==========================================================*/
function mountNoise(root, initState){
let zoneGreen = (initState && initState.zoneGreen) || 40;
let zoneYellow = (initState && initState.zoneYellow) || 70;
let running = false, audioCtx = null, analyser = null, stream = null, rafId = null, warnTimer = null;
let level = 0, peak = 0, timeData = null;
root.innerHTML = `
`;
const gauge = root.querySelector(".nm-gauge"),
fill = root.querySelector(".nm-fill"),
markGreen = root.querySelector(".nm-mark.green"),
markYellow = root.querySelector(".nm-mark.yellow"),
face = root.querySelector(".nm-face"),
status = root.querySelector(".nm-status"),
toggle = root.querySelector(".nm-toggle"),
z1 = root.querySelector(".nm-z1"),
z2 = root.querySelector(".nm-z2");
z1.value = zoneGreen;
z2.value = zoneYellow;
function currentZone(){ return level < zoneGreen ? "green" : level < zoneYellow ? "yellow" : "red"; }
function draw(){
fill.style.height = Math.round(level) + "%";
fill.className = "nm-fill " + currentZone();
markGreen.style.bottom = zoneGreen + "%";
markYellow.style.bottom = zoneYellow + "%";
face.textContent = !running ? "🎙️" : currentZone() === "green" ? "🙂" : currentZone() === "yellow" ? "😐" : "😬";
status.textContent = !running ? T("noiseOff")
: T("noiseStatus_" + currentZone());
gauge.classList.toggle("warn", running && currentZone() === "red");
}
function loop(){
if(!running || !document.body.contains(root)){ stop(); return; }
analyser.getByteTimeDomainData(timeData);
let sum = 0;
for(let i = 0; i < timeData.length; i++){ const v = (timeData[i] - 128) / 128; sum += v * v; }
const rms = Math.sqrt(sum / timeData.length);
const pct = Math.min(100, rms * 100 * 4.2);
level = level * 0.75 + pct * 0.25;
peak = Math.max(peak * 0.995, level);
draw();
rafId = requestAnimationFrame(loop);
}
async function start(){
try{
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
}catch(e){
status.textContent = T("noiseDenied");
return;
}
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
if(audioCtx.state === "suspended") await audioCtx.resume().catch(()=>{});
const source = audioCtx.createMediaStreamSource(stream);
analyser = audioCtx.createAnalyser();
analyser.fftSize = 1024;
timeData = new Uint8Array(analyser.fftSize);
source.connect(analyser);
running = true; level = 0; peak = 0;
labels();
loop();
}
function stop(){
running = false;
if(rafId) cancelAnimationFrame(rafId);
rafId = null;
if(stream){ stream.getTracks().forEach(t => t.stop()); stream = null; }
analyser = null;
level = 0;
labels();
draw();
}
toggle.addEventListener("click", ()=>{ if(running) stop(); else start(); });
z1.addEventListener("input", ()=>{
zoneGreen = Math.min(+z1.value, +z2.value - 5);
z1.value = zoneGreen;
draw();
});
z2.addEventListener("input", ()=>{
zoneYellow = Math.max(+z2.value, zoneGreen + 5);
z2.value = zoneYellow;
draw();
});
/* geluid over rood laten lopen is een stoorzender op zich - een korte,
zachte waarschuwing volstaat, geen herhaald alarm */
const watchWarn = ()=>{
clearInterval(warnTimer);
warnTimer = setInterval(()=>{
if(running && currentZone() === "red") sndBad();
}, 6000);
};
watchWarn();
function labels(){
root.querySelector(".nm-mission").textContent = T("wg_noise_d");
toggle.textContent = running ? "⏸ " + T("noiseStop") : "🎙️ " + T("noiseStart");
root.querySelectorAll(".nm-zone-lbl")[0].textContent = T("noiseZoneGreen");
root.querySelectorAll(".nm-zone-lbl")[1].textContent = T("noiseZoneYellow");
root.querySelector(".nm-privacy").textContent = T("noisePrivacy");
draw();
}
document.addEventListener("langchange", labels);
labels();
return { getState: ()=>({ zoneGreen, zoneYellow }) };
}