User:Pailiaq/common.js: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
No edit summary |
||
| Line 3: | Line 3: | ||
var ctx = null; | var ctx = null; | ||
var | var activeOscNodes = []; | ||
var activeSfHandles = []; | |||
var activeBtn = null; | var activeBtn = null; | ||
var STORAGE_KEY = 'edoChordPlayTimbre'; | var STORAGE_KEY = 'edoChordPlayTimbre'; | ||
var | // Two categories: oscillators (built-in Web Audio) and soundfonts (lazy-loaded from CDN) | ||
var TIMBRES = [ | |||
{ id: 'triangle', label: 'Triangle', group: 'Synth', type: 'osc', value: 'triangle' }, | |||
{ id: 'sawtooth', label: 'Sawtooth', group: 'Synth', type: 'osc', value: 'sawtooth' }, | |||
{ id: 'square', label: 'Square', group: 'Synth', type: 'osc', value: 'square' }, | |||
{ id: 'sine', label: 'Sine', group: 'Synth', type: 'osc', value: 'sine' }, | |||
{ id: 'piano', label: 'Grand piano', group: 'Acoustic', type: 'sf', value: 'acoustic_grand_piano' }, | |||
{ id: 'epiano', label: 'Electric piano', group: 'Acoustic', type: 'sf', value: 'electric_piano_1' }, | |||
{ id: 'organ', label: 'Organ', group: 'Acoustic', type: 'sf', value: 'church_organ' }, | |||
{ id: 'flute', label: 'Flute', group: 'Winds', type: 'sf', value: 'flute' }, | |||
{ id: 'clarinet', label: 'Clarinet', group: 'Winds', type: 'sf', value: 'clarinet' }, | |||
{ id: 'oboe', label: 'Oboe', group: 'Winds', type: 'sf', value: 'oboe' }, | |||
{ id: 'violin', label: 'Violin', group: 'Strings', type: 'sf', value: 'violin' }, | |||
{ id: 'cello', label: 'Cello', group: 'Strings', type: 'sf', value: 'cello' }, | |||
{ id: 'strings', label: 'String ensemble', group: 'Strings', type: 'sf', value: 'string_ensemble_1' }, | |||
{ id: 'choir', label: 'Choir', group: 'Voice', type: 'sf', value: 'choir_aahs' } | |||
]; | |||
var timbreId = (function () { | |||
try { | try { | ||
var saved = localStorage.getItem(STORAGE_KEY); | var saved = localStorage.getItem(STORAGE_KEY); | ||
for (var i = 0; i < TIMBRES.length; i++) if (TIMBRES[i].id === saved) return saved; | |||
} catch (e) { return 'triangle'; | } catch (e) {} | ||
return 'triangle'; | |||
})(); | })(); | ||
function | function getTimbre() { | ||
if (TIMBRES. | for (var i = 0; i < TIMBRES.length; i++) if (TIMBRES[i].id === timbreId) return TIMBRES[i]; | ||
return TIMBRES[0]; | |||
try { localStorage.setItem(STORAGE_KEY, | } | ||
document.querySelectorAll('.edo-chord-timbre').forEach(function ( | function setTimbre(id) { | ||
timbreId = id; | |||
try { localStorage.setItem(STORAGE_KEY, id); } catch (e) {} | |||
document.querySelectorAll('.edo-chord-timbre').forEach(function (s) { s.value = id; }); | |||
} | } | ||
| Line 31: | Line 50: | ||
} | } | ||
// ── Soundfont loading ─────────────────────────────────────────────── | |||
var soundfontLibPromise = null; | |||
function loadSoundfontLib() { | |||
if (window.Soundfont) return Promise.resolve(); | |||
if (soundfontLibPromise) return soundfontLibPromise; | |||
soundfontLibPromise = new Promise(function (resolve, reject) { | |||
var s = document.createElement('script'); | |||
s.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/soundfont-player.min.js'; | |||
s.onload = function () { resolve(); }; | |||
s.onerror = function () { reject(new Error('Failed to load soundfont-player')); }; | |||
document.head.appendChild(s); | |||
}); | |||
return soundfontLibPromise; | |||
} | |||
var instrumentCache = {}; | |||
function loadInstrument(name) { | |||
if (instrumentCache[name]) return instrumentCache[name]; | |||
instrumentCache[name] = loadSoundfontLib().then(function () { | |||
return window.Soundfont.instrument(getCtx(), name); | |||
}); | |||
return instrumentCache[name]; | |||
} | |||
// ── Stopping ──────────────────────────────────────────────────────── | |||
function stopAll() { | function stopAll() { | ||
if (!ctx) return; | if (!ctx) return; | ||
var now = ctx.currentTime; | var now = ctx.currentTime; | ||
activeOscNodes.forEach(function (n) { | |||
try { | try { | ||
n.gain.gain.cancelScheduledValues(now); | n.gain.gain.cancelScheduledValues(now); | ||
| Line 42: | Line 86: | ||
} catch (e) {} | } catch (e) {} | ||
}); | }); | ||
activeOscNodes = []; | |||
activeSfHandles.forEach(function (h) { try { h.stop(now); } catch (e) {} }); | |||
activeSfHandles = []; | |||
if (activeBtn) { activeBtn.classList.remove('playing'); activeBtn = null; } | if (activeBtn) { activeBtn.classList.remove('playing'); activeBtn = null; } | ||
} | } | ||
function | // ── Oscillator playback ───────────────────────────────────────────── | ||
function playOscillator(centsArr, waveform) { | |||
var c = getCtx(); | var c = getCtx(); | ||
var now = c.currentTime; | var now = c.currentTime; | ||
var base = 261.63; | var base = 261.63; | ||
var attack = 0.02, sustain = 0.2, release = | var attack = 0.02, sustain = 0.2, release = 4; | ||
var totalDur = attack + sustain + release; | var totalDur = attack + sustain + release; | ||
var peak = 0.18 / Math.sqrt(centsArr.length); | var peak = 0.18 / Math.sqrt(centsArr.length); | ||
| Line 59: | Line 104: | ||
var osc = c.createOscillator(); | var osc = c.createOscillator(); | ||
var gain = c.createGain(); | var gain = c.createGain(); | ||
osc.type = | osc.type = waveform; | ||
osc.frequency.value = base * Math.pow(2, cents / 1200); | osc.frequency.value = base * Math.pow(2, cents / 1200); | ||
gain.gain.setValueAtTime(0, now); | gain.gain.setValueAtTime(0, now); | ||
| Line 68: | Line 113: | ||
osc.start(now); | osc.start(now); | ||
osc.stop(now + totalDur + 0.1); | osc.stop(now + totalDur + 0.1); | ||
activeOscNodes.push({ osc: osc, gain: gain }); | |||
}); | }); | ||
return totalDur; | |||
} | |||
// ── Soundfont playback ────────────────────────────────────────────── | |||
function playSoundfont(centsArr, instrumentName, btn) { | |||
var dur = 3.5; | |||
var loading = !instrumentCache[instrumentName]; | |||
if (loading && btn) btn.classList.add('loading'); | |||
loadInstrument(instrumentName).then(function (inst) { | |||
if (loading && btn) btn.classList.remove('loading'); | |||
if (btn && btn !== activeBtn) return; // user clicked something else | |||
var c = getCtx(); | |||
var now = c.currentTime; | |||
var gain = 0.7 / Math.sqrt(centsArr.length); | |||
// fractional MIDI: middle C (60) + cents/100 | |||
centsArr.forEach(function (cents) { | |||
var midi = 60 + cents / 100; | |||
var handle = inst.play(midi, now, { duration: dur, gain: gain }); | |||
activeSfHandles.push(handle); | |||
}); | |||
}).catch(function (err) { | |||
if (loading && btn) btn.classList.remove('loading'); | |||
console.error('Soundfont failed, falling back to triangle:', err); | |||
playOscillator(centsArr, 'triangle'); | |||
}); | |||
return dur; | |||
} | |||
// ── Top-level dispatch ────────────────────────────────────────────── | |||
function playChord(centsArr, btn) { | |||
stopAll(); | |||
var c = getCtx(); | |||
if (c.state === 'suspended') c.resume(); | |||
var t = getTimbre(); | |||
var dur = (t.type === 'osc') | |||
? playOscillator(centsArr, t.value) | |||
: playSoundfont(centsArr, t.value, btn); | |||
if (btn) { | if (btn) { | ||
btn.classList.add('playing'); | btn.classList.add('playing'); | ||
| Line 76: | Line 158: | ||
setTimeout(function () { | setTimeout(function () { | ||
if (activeBtn === btn) { btn.classList.remove('playing'); activeBtn = null; } | if (activeBtn === btn) { btn.classList.remove('playing'); activeBtn = null; } | ||
}, | }, dur * 1000); | ||
} | } | ||
} | } | ||
// ── Selector injection ────────────────────────────────────────────── | |||
function injectSelectors() { | function injectSelectors() { | ||
var tables = new Set(); | var tables = new Set(); | ||
| Line 91: | Line 174: | ||
var sel = document.createElement('select'); | var sel = document.createElement('select'); | ||
sel.className = 'edo-chord-timbre'; | sel.className = 'edo-chord-timbre'; | ||
sel.title = 'Synth | sel.title = 'Synth / instrument'; | ||
var groups = {}; | |||
TIMBRES.forEach(function (t) { | TIMBRES.forEach(function (t) { | ||
if (!groups[t.group]) { | |||
var og = document.createElement('optgroup'); | |||
og.label = t.group; | |||
groups[t.group] = og; | |||
sel.appendChild(og); | |||
} | |||
var opt = document.createElement('option'); | var opt = document.createElement('option'); | ||
opt.value = t; | opt.value = t.id; | ||
opt.textContent = | opt.textContent = t.label; | ||
if (t === | if (t.id === timbreId) opt.selected = true; | ||
groups[t.group].appendChild(opt); | |||
}); | }); | ||
sel.addEventListener('change', function () { setTimbre(sel.value); }); | sel.addEventListener('change', function () { setTimbre(sel.value); }); | ||
Revision as of 02:14, 26 May 2026
(function () {
'use strict';
var ctx = null;
var activeOscNodes = [];
var activeSfHandles = [];
var activeBtn = null;
var STORAGE_KEY = 'edoChordPlayTimbre';
// Two categories: oscillators (built-in Web Audio) and soundfonts (lazy-loaded from CDN)
var TIMBRES = [
{ id: 'triangle', label: 'Triangle', group: 'Synth', type: 'osc', value: 'triangle' },
{ id: 'sawtooth', label: 'Sawtooth', group: 'Synth', type: 'osc', value: 'sawtooth' },
{ id: 'square', label: 'Square', group: 'Synth', type: 'osc', value: 'square' },
{ id: 'sine', label: 'Sine', group: 'Synth', type: 'osc', value: 'sine' },
{ id: 'piano', label: 'Grand piano', group: 'Acoustic', type: 'sf', value: 'acoustic_grand_piano' },
{ id: 'epiano', label: 'Electric piano', group: 'Acoustic', type: 'sf', value: 'electric_piano_1' },
{ id: 'organ', label: 'Organ', group: 'Acoustic', type: 'sf', value: 'church_organ' },
{ id: 'flute', label: 'Flute', group: 'Winds', type: 'sf', value: 'flute' },
{ id: 'clarinet', label: 'Clarinet', group: 'Winds', type: 'sf', value: 'clarinet' },
{ id: 'oboe', label: 'Oboe', group: 'Winds', type: 'sf', value: 'oboe' },
{ id: 'violin', label: 'Violin', group: 'Strings', type: 'sf', value: 'violin' },
{ id: 'cello', label: 'Cello', group: 'Strings', type: 'sf', value: 'cello' },
{ id: 'strings', label: 'String ensemble', group: 'Strings', type: 'sf', value: 'string_ensemble_1' },
{ id: 'choir', label: 'Choir', group: 'Voice', type: 'sf', value: 'choir_aahs' }
];
var timbreId = (function () {
try {
var saved = localStorage.getItem(STORAGE_KEY);
for (var i = 0; i < TIMBRES.length; i++) if (TIMBRES[i].id === saved) return saved;
} catch (e) {}
return 'triangle';
})();
function getTimbre() {
for (var i = 0; i < TIMBRES.length; i++) if (TIMBRES[i].id === timbreId) return TIMBRES[i];
return TIMBRES[0];
}
function setTimbre(id) {
timbreId = id;
try { localStorage.setItem(STORAGE_KEY, id); } catch (e) {}
document.querySelectorAll('.edo-chord-timbre').forEach(function (s) { s.value = id; });
}
function getCtx() {
if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
return ctx;
}
// ── Soundfont loading ───────────────────────────────────────────────
var soundfontLibPromise = null;
function loadSoundfontLib() {
if (window.Soundfont) return Promise.resolve();
if (soundfontLibPromise) return soundfontLibPromise;
soundfontLibPromise = new Promise(function (resolve, reject) {
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/soundfont-player.min.js';
s.onload = function () { resolve(); };
s.onerror = function () { reject(new Error('Failed to load soundfont-player')); };
document.head.appendChild(s);
});
return soundfontLibPromise;
}
var instrumentCache = {};
function loadInstrument(name) {
if (instrumentCache[name]) return instrumentCache[name];
instrumentCache[name] = loadSoundfontLib().then(function () {
return window.Soundfont.instrument(getCtx(), name);
});
return instrumentCache[name];
}
// ── Stopping ────────────────────────────────────────────────────────
function stopAll() {
if (!ctx) return;
var now = ctx.currentTime;
activeOscNodes.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) {}
});
activeOscNodes = [];
activeSfHandles.forEach(function (h) { try { h.stop(now); } catch (e) {} });
activeSfHandles = [];
if (activeBtn) { activeBtn.classList.remove('playing'); activeBtn = null; }
}
// ── Oscillator playback ─────────────────────────────────────────────
function playOscillator(centsArr, waveform) {
var c = getCtx();
var now = c.currentTime;
var base = 261.63;
var attack = 0.02, sustain = 0.2, release = 4;
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 = waveform;
osc.frequency.value = base * Math.pow(2, cents / 1200);
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);
activeOscNodes.push({ osc: osc, gain: gain });
});
return totalDur;
}
// ── Soundfont playback ──────────────────────────────────────────────
function playSoundfont(centsArr, instrumentName, btn) {
var dur = 3.5;
var loading = !instrumentCache[instrumentName];
if (loading && btn) btn.classList.add('loading');
loadInstrument(instrumentName).then(function (inst) {
if (loading && btn) btn.classList.remove('loading');
if (btn && btn !== activeBtn) return; // user clicked something else
var c = getCtx();
var now = c.currentTime;
var gain = 0.7 / Math.sqrt(centsArr.length);
// fractional MIDI: middle C (60) + cents/100
centsArr.forEach(function (cents) {
var midi = 60 + cents / 100;
var handle = inst.play(midi, now, { duration: dur, gain: gain });
activeSfHandles.push(handle);
});
}).catch(function (err) {
if (loading && btn) btn.classList.remove('loading');
console.error('Soundfont failed, falling back to triangle:', err);
playOscillator(centsArr, 'triangle');
});
return dur;
}
// ── Top-level dispatch ──────────────────────────────────────────────
function playChord(centsArr, btn) {
stopAll();
var c = getCtx();
if (c.state === 'suspended') c.resume();
var t = getTimbre();
var dur = (t.type === 'osc')
? playOscillator(centsArr, t.value)
: playSoundfont(centsArr, t.value, btn);
if (btn) {
btn.classList.add('playing');
activeBtn = btn;
setTimeout(function () {
if (activeBtn === btn) { btn.classList.remove('playing'); activeBtn = null; }
}, dur * 1000);
}
}
// ── Selector injection ──────────────────────────────────────────────
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 / instrument';
var groups = {};
TIMBRES.forEach(function (t) {
if (!groups[t.group]) {
var og = document.createElement('optgroup');
og.label = t.group;
groups[t.group] = og;
sel.appendChild(og);
}
var opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.label;
if (t.id === timbreId) opt.selected = true;
groups[t.group].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 edo = parseInt(btn.dataset.edo, 10);
var steps = (btn.dataset.steps || '').split(',').map(Number);
if (!edo || !steps.length) return;
var stepSize = 1200 / edo;
playChord(steps.map(function (s) { return s * stepSize; }), 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();
}
}());