User:Pailiaq/common.js

From Xenharmonic Wiki
Revision as of 01:22, 26 May 2026 by Pailiaq (talk | contribs) (testing)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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 activeNodes = [];
    var activeBtn = null;

    function getCtx() {
        if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
        return ctx;
    }

    function stopAll() {
        if (!ctx) return;
        var now = ctx.currentTime;
        activeNodes.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) {}
        });
        activeNodes = [];
        if (activeBtn) { activeBtn.classList.remove('playing'); activeBtn = null; }
    }

    function playChord(centsArr, btn) {
        stopAll();
        var c = getCtx();
        if (c.state === 'suspended') c.resume();
        var now = c.currentTime;
        var base = 261.63;                  // middle C
        var attack = 0.02, sustain = 2.5, release = 1.5;
        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 = 'triangle';
            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);
            activeNodes.push({ osc: osc, gain: gain });
        });

        if (btn) {
            btn.classList.add('playing');
            activeBtn = btn;
            setTimeout(function () {
                if (activeBtn === btn) { btn.classList.remove('playing'); activeBtn = null; }
            }, totalDur * 1000);
        }
    }

    function handle(e) {
        var btn = e.target.closest && e.target.closest('.edo-chord-play');
        if (!btn) return;
        e.preventDefault();
        if (btn === activeBtn) { stopAll(); return; }   // click again to stop
        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);
        }
    });
}());