User:Pailiaq/common.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(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();
}
}());