User:Inthar/common.js: Difference between revisions
Blanked by request Tag: Blanking |
No edit summary |
||
| Line 1: | Line 1: | ||
(function () { | |||
'use strict'; | |||
var ctx = null; | |||
var activeNodes = []; | |||
var activeBtn = null; | |||
var STORAGE_KEY = 'edoChordPlayTimbre'; | |||
var TIMBRES = ['triangle', 'sawtooth', 'square', 'sine']; | |||
var TIMBRE_LABELS = { triangle: 'Triangle', sawtooth: 'Sawtooth', square: 'Square', sine: 'Sine' }; | |||
var LOOK_AHEAD = 0.00; // 50ms buffer so the audio thread can schedule | |||
var BASE_FREQ = 261.63; // middle C; pitch is set via detune (cents) on top | |||
var timbre = (function () { | |||
try { | |||
var saved = localStorage.getItem(STORAGE_KEY); | |||
return TIMBRES.indexOf(saved) >= 0 ? saved : 'triangle'; | |||
} catch (e) { return 'triangle'; } | |||
})(); | |||
function setTimbre(t) { | |||
if (TIMBRES.indexOf(t) < 0) return; | |||
timbre = t; | |||
try { localStorage.setItem(STORAGE_KEY, t); } catch (e) {} | |||
document.querySelectorAll('.edo-chord-timbre').forEach(function (sel) { | |||
sel.value = t; | |||
}); | |||
} | |||
function getCtx() { | |||
if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)(); | |||
return ctx; | |||
} | |||
function disposeNode(node) { | |||
try { node.osc.disconnect(); } catch (e) {} | |||
try { node.gain.disconnect(); } catch (e) {} | |||
var idx = activeNodes.indexOf(node); | |||
if (idx >= 0) activeNodes.splice(idx, 1); | |||
} | |||
function stopAll() { | |||
if (!ctx) return; | |||
var now = ctx.currentTime + LOOK_AHEAD; | |||
// Snapshot and clear before stopping, so onended handlers find no matches and just dispose | |||
var toStop = activeNodes; | |||
activeNodes = []; | |||
toStop.forEach(function (n) { | |||
try { | |||
n.gain.gain.cancelScheduledValues(now); | |||
n.gain.gain.setValueAtTime(n.gain.gain.value, now); | |||
n.gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.05); | |||
n.osc.stop(now + 0.06); | |||
} catch (e) {} | |||
}); | |||
if (activeBtn) { activeBtn.classList.remove('playing'); activeBtn = null; } | |||
} | |||
function playChord(centsArr, btn) { | |||
stopAll(); | |||
var c = getCtx(); | |||
if (c.state === 'suspended') c.resume(); | |||
var now = c.currentTime + LOOK_AHEAD; | |||
var attack = 0.02, sustain = 0.2, release = 5; | |||
var totalDur = attack + sustain + release; | |||
var peak = 0.18 / Math.sqrt(centsArr.length); | |||
centsArr.forEach(function (cents) { | |||
var osc = c.createOscillator(); | |||
var gain = c.createGain(); | |||
osc.type = timbre; | |||
osc.frequency.value = BASE_FREQ; | |||
osc.detune.value = cents; | |||
gain.gain.setValueAtTime(0, now); | |||
gain.gain.linearRampToValueAtTime(peak, now + attack); | |||
gain.gain.setValueAtTime(peak, now + attack + sustain); | |||
gain.gain.exponentialRampToValueAtTime(0.0001, now + totalDur); | |||
osc.connect(gain).connect(c.destination); | |||
osc.start(now); | |||
osc.stop(now + totalDur + 0.1); | |||
var node = { osc: osc, gain: gain }; | |||
activeNodes.push(node); | |||
osc.onended = function () { disposeNode(node); }; | |||
}); | |||
if (btn) { | |||
btn.classList.add('playing'); | |||
activeBtn = btn; | |||
setTimeout(function () { | |||
if (activeBtn === btn) { btn.classList.remove('playing'); activeBtn = null; } | |||
}, totalDur * 1000); | |||
} | |||
} | |||
function injectSelectors() { | |||
var tables = new Set(); | |||
document.querySelectorAll('.edo-chord-play').forEach(function (btn) { | |||
var table = btn.closest('table'); | |||
if (table) tables.add(table); | |||
}); | |||
tables.forEach(function (table) { | |||
var firstTh = table.querySelector('th'); | |||
if (!firstTh || firstTh.querySelector('.edo-chord-timbre')) return; | |||
var sel = document.createElement('select'); | |||
sel.className = 'edo-chord-timbre'; | |||
sel.title = 'Synth timbre'; | |||
TIMBRES.forEach(function (t) { | |||
var opt = document.createElement('option'); | |||
opt.value = t; | |||
opt.textContent = TIMBRE_LABELS[t]; | |||
if (t === timbre) opt.selected = true; | |||
sel.appendChild(opt); | |||
}); | |||
sel.addEventListener('change', function () { setTimbre(sel.value); }); | |||
sel.addEventListener('click', function (e) { e.stopPropagation(); }); | |||
firstTh.innerHTML = ''; | |||
firstTh.appendChild(sel); | |||
}); | |||
} | |||
function handle(e) { | |||
var btn = e.target.closest && e.target.closest('.edo-chord-play'); | |||
if (!btn) return; | |||
e.preventDefault(); | |||
if (btn === activeBtn) { stopAll(); return; } | |||
var cents; | |||
if (btn.dataset.cents) { | |||
cents = btn.dataset.cents.split(',').map(Number); | |||
} else { | |||
var edo = parseInt(btn.dataset.edo, 10); | |||
var steps = (btn.dataset.steps || '').split(',').map(Number); | |||
if (!edo || !steps.length) return; | |||
var stepSize = 1200 / edo; | |||
cents = steps.map(function (s) { return s * stepSize; }); | |||
} | |||
if (!cents.length || cents.some(isNaN)) return; | |||
playChord(cents, btn); | |||
} | |||
document.addEventListener('click', handle); | |||
document.addEventListener('keydown', function (e) { | |||
if ((e.key === 'Enter' || e.key === ' ') && e.target.classList && e.target.classList.contains('edo-chord-play')) { | |||
handle(e); | |||
} | |||
}); | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', injectSelectors); | |||
} else { | |||
injectSelectors(); | |||
} | |||
}()); | |||