■ ■ ■ ilan_woodward — metronome /admin
'use strict'; /* metronome.js — grandpa keeps the time. Audio + pendulum share the Web Audio clock, so they can't drift apart the main beat lands exactly when the pendulum reaches each end of its swing, and subdivisions tick in between. Custom sounds: dropped files are decoded IN MEMORY only — never uploa gone when the page closes. We scan the waveform for its first transie (first sample above 30% of peak) and play from just before that point capped at half a second, so any file becomes a tight tick. */ (() => { const $ = (s) => document.querySelector(s); $('#year').textContent = new Date().getFullYear(); const pend = document.getElementById('pend'); const AMP = 6; // swing amplitude, degrees — stays within th const PIVOT = '170 300'; // pendulum pivot in the SVG let ctx = null; let running = false; let bpm = 100; let sub = 1; // ticks per beat let sound = 'blip'; let startT = 0; // audio-clock time of beat zero let nextSub = 0; // next unscheduled sub-tick time let subIdx = 0; // counts sub-ticks; % sub === 0 → main beat let timer = null; let noiseBuf = null; // shared white-noise burst for 'click' let customBuf = null; // the visitor's dropped sound (memory only) let customStart = 0; // seconds: just before the detected transien let customDur = 0.5; // seconds: capped playback window const TEMPI = [ [40, 'grave'], [60, 'largo'], [66, 'adagio'], [76, 'andante'], [108, 'moderato'], [120, 'allegro'], [168, 'vivace'], [200, 'presto' ]; const tempoWord = (b) => (TEMPI.find(([max]) => b < max) || TEMPI[TEMP function ensureCtx() { if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContex ctx.resume(); } function syncUi() { $('#m-bpm').textContent = bpm; $('#m-range').value = bpm; $('#m-num').value = bpm; $('#m-tempo').textContent = `// ${tempoWord(bpm)}`; } function rebase() { if (!running) return; startT = ctx.currentTime; nextSub = ctx.currentTime + 0.06; subIdx = 0; } function setBpm(v) { const b = Math.round(Number(v)); if (!isFinite(b)) return syncUi(); bpm = Math.min(240, Math.max(30, b)); rebase(); syncUi(); } /* ── voices ───────────────────────────────────────────────────────── function osc(type, freq, t, vol, dur) { const o = ctx.createOscillator(); const g = ctx.createGain(); o.type = type; o.frequency.value = freq; g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(vol, t + 0.002); g.gain.exponentialRampToValueAtTime(0.0001, t + dur); o.connect(g).connect(ctx.destination); o.start(t); o.stop(t + dur + 0.02); } /* the Maestro Rhythm King voice — the "Family Affair" sound: soft attack, sine-ish body with a little analog pitch droop, a quieter partial on top, and everything rolled off through a warm lowpass. No hard edges anywhere; that's the whole secret. */ function mellow(t, f, vol, dur, lpFreq) { const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = lpFreq; 12 1 2 3 4 5 6 7 8 9 10 11
metronome.exe

100 bpm

// allegro

Drop your own sound! (Maximum 10 second long file)
wav · mp3 · m4a — plays from its transient, lives only while this page is open

// main beat rings at each end of the swing · subdivisions tick in between · 30–240 bpm