User:Pailiaq/common.js: Difference between revisions

Pailiaq (talk | contribs)
No edit summary
Pailiaq (talk | contribs)
No edit summary
 
(5 intermediate revisions by the same user not shown)
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.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 () {
     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) {}
        // Keep every selector on the page in sync
         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) {}
         });
         });
        activeNodes = [];
         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 base = 261.63;
         var attack = 0.02, sustain = 0.2, release = 5;
         var attack = 0.02, sustain = 2.5, release = 1.5;
         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 60: Line 69:
             var gain = c.createGain();
             var gain = c.createGain();
             osc.type = timbre;
             osc.type = timbre;
             osc.frequency.value = base * Math.pow(2, cents / 1200);
             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);
             activeNodes.push({ osc: osc, gain: gain });
 
             var node = { osc: osc, gain: gain };
            activeNodes.push(node);
            osc.onended = function () { disposeNode(node); };
         });
         });


Line 111: Line 124:
         e.preventDefault();
         e.preventDefault();
         if (btn === activeBtn) { stopAll(); return; }
         if (btn === activeBtn) { stopAll(); return; }
         var edo = parseInt(btn.dataset.edo, 10);
 
        var steps = (btn.dataset.steps || '').split(',').map(Number);
         var cents;
        if (!edo || !steps.length) return;
        if (btn.dataset.cents) {
        var stepSize = 1200 / edo;
            cents = btn.dataset.cents.split(',').map(Number);
        playChord(steps.map(function (s) { return s * stepSize; }), btn);
        } 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);
     }
     }