File: //home/primrwxj/lateraleffects.com/Index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Humming Hz Meter (Target 130 Hz)</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 24px; }
.card { max-width: 720px; padding: 18px; border: 1px solid #ddd; border-radius: 14px; }
button { padding: 10px 14px; border-radius: 10px; border: 1px solid #ccc; background: #fff; }
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-top: 12px; }
.big { font-size: 44px; font-weight: 700; margin: 8px 0; }
.muted { color: #555; }
.barwrap { height: 14px; background: #eee; border-radius: 999px; overflow: hidden; }
.bar { height: 100%; width: 0%; background: #111; transition: width 120ms linear; }
.status { font-weight: 650; }
input[type="number"] { padding: 8px 10px; border-radius: 10px; border: 1px solid #ccc; width: 110px; }
.pill { display:inline-block; padding: 4px 10px; border-radius: 999px; border: 1px solid #ddd; }
.good { border-color: #0a0; }
.warn { border-color: #b60; }
</style>
</head>
<body>
<div class="card">
<h1 style="margin-top:0">Humming Hz Meter</h1>
<div class="muted">
Measures your humming pitch (fundamental frequency) and guides you to a target (default <b>130 Hz</b>).
</div>
<div class="row">
<button id="startBtn">Start</button>
<button id="stopBtn" disabled>Stop</button>
<label class="pill">
Target Hz:
<input id="targetHz" type="number" value="130" min="20" max="500" step="1" />
</label>
<label class="pill">
Tolerance ±Hz:
<input id="tolHz" type="number" value="2" min="0.5" max="20" step="0.5" />
</label>
</div>
<div class="big" id="hzReadout">— Hz</div>
<div class="row" style="justify-content:space-between">
<div class="status" id="statusText">Tap Start and hum.</div>
<div class="muted">Confidence: <span id="confText">—</span></div>
</div>
<div style="margin-top:12px">
<div class="muted" style="margin-bottom:6px">Closeness to target</div>
<div class="barwrap"><div class="bar" id="closenessBar"></div></div>
<div class="muted" style="margin-top:6px">
Tip: steady hum + consistent breath gives the most stable reading.
</div>
</div>
<hr style="margin:18px 0; border:none; border-top:1px solid #eee" />
<div class="muted">
Notes:
<ul>
<li>This measures <b>pitch (Hz)</b>, not volume. If you want volume too, we can add a dB meter.</li>
<li>Background noise can confuse detection — hum close to the mic in a quiet room.</li>
<li>Some voices may find 130 Hz low/high; you can adjust target anytime.</li>
</ul>
</div>
</div>
<script>
(() => {
let audioCtx, analyser, mediaStream, source;
let rafId = null;
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const hzReadout = document.getElementById('hzReadout');
const statusText= document.getElementById('statusText');
const confText = document.getElementById('confText');
const targetHzEl= document.getElementById('targetHz');
const tolHzEl = document.getElementById('tolHz');
const bar = document.getElementById('closenessBar');
function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }
// Autocorrelation pitch detection (good, lightweight, classic)
function autoCorrelate(buf, sampleRate) {
// Remove DC offset
let size = buf.length;
let rms = 0;
for (let i=0;i<size;i++){ let val = buf[i]; rms += val*val; }
rms = Math.sqrt(rms/size);
if (rms < 0.01) return { freq: null, confidence: 0 }; // too quiet
// Trim leading/trailing silence-ish
let r1 = 0, r2 = size - 1;
const thresh = 0.2;
for (let i=0;i<size/2;i++) { if (Math.abs(buf[i]) < thresh) r1 = i; else break; }
for (let i=1;i<size/2;i++) { if (Math.abs(buf[size-i]) < thresh) r2 = size-i; else break; }
buf = buf.slice(r1, r2);
size = buf.length;
if (size < 32) return { freq: null, confidence: 0 };
const c = new Array(size).fill(0);
for (let i=0;i<size;i++){
for (let j=0;j<size-i;j++){
c[i] = c[i] + buf[j] * buf[j+i];
}
}
let d = 0;
while (d < size-1 && c[d] > c[d+1]) d++; // find first valley
let maxval = -1, maxpos = -1;
for (let i=d;i<size;i++){
if (c[i] > maxval) { maxval = c[i]; maxpos = i; }
}
if (maxpos <= 0) return { freq: null, confidence: 0 };
// Parabolic interpolation for better precision
const x1 = c[maxpos-1] || 0;
const x2 = c[maxpos];
const x3 = c[maxpos+1] || 0;
const a = (x1 + x3 - 2*x2) / 2;
const b = (x3 - x1) / 2;
const shift = (a !== 0) ? (-b / (2*a)) : 0;
const period = maxpos + shift;
const freq = sampleRate / period;
// confidence heuristic: peak strength vs rms
const confidence = clamp(maxval / (size * rms * rms), 0, 1);
// Filter out improbable ranges for humming (you can widen if needed)
if (freq < 50 || freq > 400) return { freq: null, confidence: 0 };
return { freq, confidence };
}
function updateUI(freq, confidence) {
const target = parseFloat(targetHzEl.value || "130");
const tol = parseFloat(tolHzEl.value || "2");
if (!freq) {
hzReadout.textContent = "— Hz";
confText.textContent = "—";
statusText.textContent = "Hum steadily…";
bar.style.width = "0%";
startBtn.className = "";
return;
}
const rounded = Math.round(freq * 10) / 10;
hzReadout.textContent = `${rounded} Hz`;
confText.textContent = `${Math.round(confidence * 100)}%`;
const diff = Math.abs(freq - target);
const inRange = diff <= tol;
// closeness: 0 at diff>=30, 100 at diff=0 (you can tune)
const closeness = clamp(100 * (1 - (diff / 30)), 0, 100);
bar.style.width = `${closeness}%`;
if (inRange) {
statusText.textContent = `✅ Locked in near ${target} Hz (±${tol} Hz). Keep it steady.`;
statusText.parentElement?.classList?.add?.("good");
} else {
const direction = (freq < target) ? "raise" : "lower";
statusText.textContent = `You’re ${diff.toFixed(1)} Hz away — ${direction} your pitch toward ${target} Hz.`;
}
}
function loop() {
const bufferLength = analyser.fftSize;
const buf = new Float32Array(bufferLength);
analyser.getFloatTimeDomainData(buf);
const { freq, confidence } = autoCorrelate(buf, audioCtx.sampleRate);
// simple smoothing for readability
if (!loop.lastFreq) loop.lastFreq = freq;
let smoothed = freq;
if (freq && loop.lastFreq) smoothed = loop.lastFreq * 0.85 + freq * 0.15;
loop.lastFreq = smoothed || loop.lastFreq;
updateUI(smoothed, confidence);
rafId = requestAnimationFrame(loop);
}
async function start() {
try {
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: false
}
});
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
source = audioCtx.createMediaStreamSource(mediaStream);
source.connect(analyser);
startBtn.disabled = true;
stopBtn.disabled = false;
statusText.textContent = "Listening… hum now.";
loop.lastFreq = null;
loop();
} catch (e) {
console.error(e);
statusText.textContent = "Mic permission denied or unavailable.";
}
}
function stop() {
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
if (mediaStream) {
mediaStream.getTracks().forEach(t => t.stop());
mediaStream = null;
}
if (audioCtx) {
audioCtx.close();
audioCtx = null;
}
startBtn.disabled = false;
stopBtn.disabled = true;
hzReadout.textContent = "— Hz";
confText.textContent = "—";
bar.style.width = "0%";
statusText.textContent = "Stopped.";
}
startBtn.addEventListener('click', start);
stopBtn.addEventListener('click', stop);
})();
</script>
</body>
</html>