Skip to content

Instantly share code, notes, and snippets.

@selalipop
Last active December 16, 2025 11:54
Show Gist options
  • Select an option

  • Save selalipop/81c6a50b57c41e63cba0ec0abfdfed68 to your computer and use it in GitHub Desktop.

Select an option

Save selalipop/81c6a50b57c41e63cba0ec0abfdfed68 to your computer and use it in GitHub Desktop.
Run in dev console while visiting npmjs.com/login to make the wombat giggle to the beat: click an EQ band to follow it, and adjust thresholds to set sensitivity
(async function bassSelector() {
const bands = [
{ name: 'Sub Bass', freq: 40, range: 20, color: '#ff0055' },
{ name: 'Low Bass', freq: 80, range: 20, color: '#ff5500' },
{ name: 'Mid Bass', freq: 120, range: 20, color: '#ffaa00' },
{ name: 'Upper Bass', freq: 160, range: 20, color: '#aaff00' },
{ name: 'Low Mid', freq: 250, range: 50, color: '#00ff55' },
{ name: 'Mid', freq: 400, range: 100, color: '#00ffaa' },
{ name: 'Upper Mid', freq: 800, range: 200, color: '#00aaff' },
{ name: 'Kick Punch', freq: 100, range: 30, color: '#aa00ff' },
];
const config = {
selectedBand: 1,
spikeThreshold: 30,
cooldownMs: 100,
averageWindow: 15,
ariaLabelMatch: "wombat visibly giggle"
};
// Disable all audio processing to preserve dynamics for spike detection
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
}
});
const audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
const analyser = audioContext.createAnalyser();
analyser.fftSize = 4096;
analyser.smoothingTimeConstant = 0.4;
source.connect(analyser);
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const frequencyResolution = audioContext.sampleRate / analyser.fftSize;
bands.forEach(band => {
band.minBin = Math.floor((band.freq - band.range) / frequencyResolution);
band.maxBin = Math.ceil((band.freq + band.range) / frequencyResolution);
band.history = [];
band.peakSpike = 0;
});
// Peak picking state machine
const peakPicker = {
state: 'waiting', // waiting | rising | triggered
previousSpike: 0,
process(spike, threshold) {
let shouldFire = false;
if (this.state === 'waiting' && spike > threshold) {
this.state = 'rising';
} else if (this.state === 'rising') {
if (spike < this.previousSpike) {
// Just passed the peak - fire!
shouldFire = true;
this.state = 'triggered';
}
} else if (this.state === 'triggered' && spike < threshold * 0.5) {
// Fallen back down, ready for next hit
this.state = 'waiting';
}
this.previousSpike = spike;
return shouldFire;
}
};
// Robust click mechanism
const clicker = {
lastClickTime: 0,
isClickPending: false,
triggerClick() {
const now = Date.now();
if (this.isClickPending || now - this.lastClickTime < config.cooldownMs) {
return false;
}
const button = document.querySelector(`[aria-label*="${config.ariaLabelMatch}"]`);
if (!button) return false;
this.isClickPending = true;
this.lastClickTime = now;
const rect = button.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const eventProps = {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
};
button.dispatchEvent(new PointerEvent('pointerdown', eventProps));
button.dispatchEvent(new MouseEvent('mousedown', eventProps));
button.dispatchEvent(new PointerEvent('pointerup', eventProps));
button.dispatchEvent(new MouseEvent('mouseup', eventProps));
button.dispatchEvent(new MouseEvent('click', eventProps));
this.isClickPending = false;
return true;
}
};
const debugDiv = document.createElement('div');
debugDiv.id = 'bass-debugger';
debugDiv.innerHTML = `
<style>
#bass-debugger * { box-sizing: border-box; }
.bd-band {
padding: 8px;
margin: 4px 0;
border-radius: 6px;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.15s, background 0.15s;
}
.bd-band:hover { background: #333; }
.bd-band.selected { border-color: #0ff; background: #1a3a4a; }
.bd-meters { display: flex; gap: 4px; margin: 4px 0; height: 20px; }
.bd-meter-bg { background: #333; height: 100%; border-radius: 3px; overflow: hidden; flex: 1; position: relative; }
.bd-meter-fill { height: 100%; transition: width 0.05s; position: absolute; left: 0; top: 0; }
.bd-avg-line { position: absolute; top: 0; bottom: 0; width: 2px; background: #fff; opacity: 0.7; }
.bd-spike-indicator {
position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
font-size: 12px; opacity: 0; transition: opacity 0.1s;
}
.bd-spike-indicator.flash { opacity: 1; }
.bd-band-header { display: flex; justify-content: space-between; align-items: center; }
.bd-band-name { font-weight: bold; }
.bd-band-info { font-size: 10px; color: #888; }
.bd-level { font-size: 10px; color: #aaa; display: flex; justify-content: space-between; }
</style>
<div style="position:fixed;top:10px;right:10px;background:#1a1a2e;color:#eee;padding:15px;border-radius:10px;font-family:monospace;font-size:12px;z-index:999999;width:340px;max-height:90vh;overflow-y:auto;box-shadow:0 4px 25px rgba(0,0,0,0.6);">
<div style="font-weight:bold;margin-bottom:5px;color:#0ff;font-size:14px;">🎵 Spike Detector</div>
<div style="font-size:10px;color:#888;margin-bottom:10px;">Peak picking • Fires at top of each spike</div>
<div id="bd-bands"></div>
<div style="margin-top:15px;padding-top:10px;border-top:1px solid #333;">
<div style="margin-bottom:8px;">
<label style="color:#888;">Spike Threshold: <span id="bd-threshold-val">${config.spikeThreshold}</span></label>
<input type="range" id="bd-threshold" min="5" max="100" value="${config.spikeThreshold}" style="width:100%;margin-top:4px;">
</div>
<div style="margin-bottom:8px;">
<label style="color:#888;">Average Window: <span id="bd-window-val">${config.averageWindow}</span> frames</label>
<input type="range" id="bd-window" min="5" max="60" value="${config.averageWindow}" style="width:100%;margin-top:4px;">
</div>
<div>Button: <span id="bd-button">checking...</span></div>
<div>Status: <span id="bd-status" style="color:#0f0;">Listening...</span></div>
</div>
<div style="margin-top:10px;font-size:10px;color:#666;">
stopBassClicker() to close
</div>
</div>
`;
document.body.appendChild(debugDiv);
const bandsContainer = document.getElementById('bd-bands');
bands.forEach((band, i) => {
const bandEl = document.createElement('div');
bandEl.className = 'bd-band' + (i === config.selectedBand ? ' selected' : '');
bandEl.dataset.index = i;
bandEl.innerHTML = `
<div class="bd-band-header">
<span class="bd-band-name" style="color:${band.color}">${band.name}</span>
<span class="bd-band-info">${band.freq}Hz ±${band.range}</span>
</div>
<div class="bd-meters">
<div class="bd-meter-bg" title="Level vs Average">
<div class="bd-meter-fill" id="bd-meter-${i}" style="background:${band.color};width:0%;opacity:0.7;"></div>
<div class="bd-avg-line" id="bd-avg-${i}" style="left:0%;"></div>
<span class="bd-spike-indicator" id="bd-flash-${i}">⚡</span>
</div>
<div class="bd-meter-bg" title="Spike (above avg)" style="flex:0.5;">
<div class="bd-meter-fill" id="bd-spike-${i}" style="background:#fff;width:0%;"></div>
</div>
</div>
<div class="bd-level">
<span>Now: <span id="bd-level-${i}">0</span></span>
<span>Avg: <span id="bd-avg-val-${i}">0</span></span>
<span>Δ: <span id="bd-delta-${i}">0</span></span>
</div>
`;
bandEl.addEventListener('click', () => {
document.querySelectorAll('.bd-band').forEach(el => el.classList.remove('selected'));
bandEl.classList.add('selected');
config.selectedBand = i;
// Reset peak picker when switching bands
peakPicker.state = 'waiting';
peakPicker.previousSpike = 0;
console.log(`Selected: ${band.name} (${band.freq}Hz)`);
});
bandsContainer.appendChild(bandEl);
});
document.getElementById('bd-threshold').addEventListener('input', (e) => {
config.spikeThreshold = parseInt(e.target.value);
document.getElementById('bd-threshold-val').textContent = config.spikeThreshold;
});
document.getElementById('bd-window').addEventListener('input', (e) => {
config.averageWindow = parseInt(e.target.value);
document.getElementById('bd-window-val').textContent = config.averageWindow;
});
const buttonEl = document.getElementById('bd-button');
const statusEl = document.getElementById('bd-status');
let isRunning = true;
function getLevel(band) {
let sum = 0;
for (let bin = band.minBin; bin <= band.maxBin && bin < bufferLength; bin++) {
sum += dataArray[bin];
}
return sum / (band.maxBin - band.minBin + 1);
}
function analyze() {
if (!isRunning) return;
if (audioContext.state === 'suspended') {
audioContext.resume();
}
analyser.getByteFrequencyData(dataArray);
bands.forEach((band, i) => {
const level = getLevel(band);
// Calculate average from PREVIOUS frames only (before adding current)
const avg = band.history.length > 0
? band.history.reduce((a, b) => a + b, 0) / band.history.length
: level;
const spike = Math.max(0, level - avg);
// Now add current level to history for next frame
band.history.push(level);
if (band.history.length > config.averageWindow) {
band.history.shift();
}
if (spike > band.peakSpike) band.peakSpike = spike;
document.getElementById(`bd-meter-${i}`).style.width = (level / 255 * 100) + '%';
document.getElementById(`bd-avg-${i}`).style.left = (avg / 255 * 100) + '%';
document.getElementById(`bd-spike-${i}`).style.width = Math.min(100, spike / 80 * 100) + '%';
document.getElementById(`bd-level-${i}`).textContent = level.toFixed(0);
document.getElementById(`bd-avg-val-${i}`).textContent = avg.toFixed(0);
document.getElementById(`bd-delta-${i}`).textContent = spike.toFixed(0);
const flashEl = document.getElementById(`bd-flash-${i}`);
if (spike > config.spikeThreshold) {
flashEl.classList.add('flash');
setTimeout(() => flashEl.classList.remove('flash'), 150);
}
});
const selected = bands[config.selectedBand];
const level = getLevel(selected);
// Use the already-computed average from previous frames
const avg = selected.history.length > 1
? (selected.history.slice(0, -1).reduce((a, b) => a + b, 0) / (selected.history.length - 1))
: level;
const spike = Math.max(0, level - avg);
const button = document.querySelector(`[aria-label*="${config.ariaLabelMatch}"]`);
buttonEl.textContent = button ? '✅ Found' : '❌ Not found';
buttonEl.style.color = button ? '#0f0' : '#f00';
// Use peak picker instead of simple threshold
const shouldFire = peakPicker.process(spike, config.spikeThreshold);
if (shouldFire) {
const clicked = clicker.triggerClick();
if (clicked) {
statusEl.textContent = `⚡ PEAK! (+${spike.toFixed(0)})`;
statusEl.style.color = '#0ff';
setTimeout(() => {
statusEl.textContent = 'Listening...';
statusEl.style.color = '#0f0';
}, 300);
}
}
requestAnimationFrame(analyze);
}
analyze();
window.bassConfig = config;
window.bassBands = bands;
window.peakPicker = peakPicker; // Expose for debugging
window.stopBassClicker = () => {
isRunning = false;
stream.getTracks().forEach(track => track.stop());
audioContext.close();
document.getElementById('bass-debugger')?.remove();
console.log("🛑 Stopped");
};
console.log("Spike detector ready! (with peak picking, raw audio)");
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment