User:Pailiaq/common.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 8: | Line 8: | ||
var TIMBRES = ['triangle', 'sawtooth', 'square', 'sine']; | var TIMBRES = ['triangle', 'sawtooth', 'square', 'sine']; | ||
var TIMBRE_LABELS = { triangle: 'Triangle', sawtooth: 'Sawtooth', square: 'Square', sine: 'Sine' }; | var TIMBRE_LABELS = { triangle: 'Triangle', sawtooth: 'Sawtooth', square: 'Square', sine: 'Sine' }; | ||
var LOOK_AHEAD = 0.05; // 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 () { | var timbre = (function () { | ||
| Line 20: | Line 22: | ||
timbre = t; | timbre = t; | ||
try { localStorage.setItem(STORAGE_KEY, t); } catch (e) {} | try { localStorage.setItem(STORAGE_KEY, t); } catch (e) {} | ||
document.querySelectorAll('.edo-chord-timbre').forEach(function (sel) { | document.querySelectorAll('.edo-chord-timbre').forEach(function (sel) { | ||
sel.value = t; | sel.value = t; | ||
| Line 29: | Line 30: | ||
if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)(); | if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)(); | ||
return ctx; | 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() { | function stopAll() { | ||
if (!ctx) return; | if (!ctx) return; | ||
var now = ctx.currentTime; | var now = ctx.currentTime + LOOK_AHEAD; | ||
activeNodes.forEach(function (n) { | // Snapshot and clear before stopping, so onended handlers find no matches and just dispose | ||
var toStop = activeNodes; | |||
activeNodes = []; | |||
toStop.forEach(function (n) { | |||
try { | try { | ||
n.gain.gain.cancelScheduledValues(now); | n.gain.gain.cancelScheduledValues(now); | ||
| Line 42: | Line 53: | ||
} catch (e) {} | } catch (e) {} | ||
}); | }); | ||
if (activeBtn) { activeBtn.classList.remove('playing'); activeBtn = null; } | if (activeBtn) { activeBtn.classList.remove('playing'); activeBtn = null; } | ||
} | } | ||
| Line 50: | Line 60: | ||
var c = getCtx(); | var c = getCtx(); | ||
if (c.state === 'suspended') c.resume(); | if (c.state === 'suspended') c.resume(); | ||
var now = c.currentTime | var now = c.currentTime + LOOK_AHEAD; | ||
var attack = 0.02, sustain = 0.2, release = 5; | var attack = 0.02, sustain = 0.2, release = 5; | ||
var totalDur = attack + sustain + release; | var totalDur = attack + sustain + release; | ||
| Line 60: | Line 69: | ||
var gain = c.createGain(); | var gain = c.createGain(); | ||
osc.type = timbre; | osc.type = timbre; | ||
osc.frequency.value = | osc.frequency.value = BASE_FREQ; | ||
osc.detune.value = cents; | |||
gain.gain.setValueAtTime(0, now); | gain.gain.setValueAtTime(0, now); | ||
gain.gain.linearRampToValueAtTime(peak, now + attack); | gain.gain.linearRampToValueAtTime(peak, now + attack); | ||
| Line 68: | Line 78: | ||
osc.start(now); | osc.start(now); | ||
osc.stop(now + totalDur + 0.1); | osc.stop(now + totalDur + 0.1); | ||
var node = { osc: osc, gain: gain }; | |||
activeNodes.push(node); | |||
osc.onended = function () { disposeNode(node); }; | |||
}); | }); | ||
| Line 106: | Line 119: | ||
} | } | ||
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('click', handle); | ||