/* 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"); root.querySelector(".nm-settings-label").textContent = T("widgetSettings"); 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 }) }; }