User:Pailiaq/common.js: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Pailiaq (talk | contribs)
No edit summary
Pailiaq (talk | contribs)
No edit summary
Line 3: Line 3:


     var ctx = null;
     var ctx = null;
     var activeNodes = [];
     var activeOscNodes = [];
    var activeSfHandles = [];
     var activeBtn = null;
     var activeBtn = null;
     var STORAGE_KEY = 'edoChordPlayTimbre';
     var STORAGE_KEY = 'edoChordPlayTimbre';
    var TIMBRES = ['triangle', 'sawtooth', 'square', 'sine'];
    var TIMBRE_LABELS = { triangle: 'Triangle', sawtooth: 'Sawtooth', square: 'Square', sine: 'Sine' };


     var timbre = (function () {
    // 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);
             return TIMBRES.indexOf(saved) >= 0 ? saved : 'triangle';
             for (var i = 0; i < TIMBRES.length; i++) if (TIMBRES[i].id === saved) return saved;
         } catch (e) { return 'triangle'; }
         } catch (e) {}
        return 'triangle';
     })();
     })();


     function setTimbre(t) {
     function getTimbre() {
         if (TIMBRES.indexOf(t) < 0) return;
         for (var i = 0; i < TIMBRES.length; i++) if (TIMBRES[i].id === timbreId) return TIMBRES[i];
         timbre = t;
        return TIMBRES[0];
         try { localStorage.setItem(STORAGE_KEY, t); } catch (e) {}
    }
        // Keep every selector on the page in sync
 
         document.querySelectorAll('.edo-chord-timbre').forEach(function (sel) {
    function setTimbre(id) {
            sel.value = t;
         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;
         activeNodes.forEach(function (n) {
         activeOscNodes.forEach(function (n) {
             try {
             try {
                 n.gain.gain.cancelScheduledValues(now);
                 n.gain.gain.cancelScheduledValues(now);
Line 42: Line 86:
             } catch (e) {}
             } catch (e) {}
         });
         });
         activeNodes = [];
         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 playChord(centsArr, btn) {
    // ── Oscillator playback ─────────────────────────────────────────────
        stopAll();
     function playOscillator(centsArr, waveform) {
         var c = getCtx();
         var c = getCtx();
        if (c.state === 'suspended') c.resume();
         var now = c.currentTime;
         var now = c.currentTime;
         var base = 261.63;
         var base = 261.63;
         var attack = 0.02, sustain = 0.2, release = 5;
         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 = timbre;
             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);
             activeNodes.push({ osc: osc, gain: gain });
             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; }
             }, totalDur * 1000);
             }, 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 timbre';
             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 = TIMBRE_LABELS[t];
                 opt.textContent = t.label;
                 if (t === timbre) opt.selected = true;
                 if (t.id === timbreId) opt.selected = true;
                 sel.appendChild(opt);
                 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();
    }
}());