MOON
Server: Apache
System: Linux server1.primemusicproductions.com 4.18.0-477.27.2.el8_8.x86_64 #1 SMP Fri Sep 29 08:21:01 EDT 2023 x86_64
User: primrwxj (1001)
PHP: 8.3.3
Disabled: NONE
Upload Files
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>