|
Tags: Replaced Manual revert |
| Line 14: |
Line 14: |
| }); | | }); |
| }); | | }); |
|
| |
| (function () {
| |
| 'use strict';
| |
|
| |
| var ctx = null;
| |
| var activeNodes = [];
| |
| var activeBtn = null;
| |
| var STORAGE_KEY = 'edoChordPlayTimbre';
| |
| var TIMBRES = ['triangle', 'sawtooth', 'square', '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 () {
| |
| try {
| |
| var saved = localStorage.getItem(STORAGE_KEY);
| |
| return TIMBRES.indexOf(saved) >= 0 ? saved : 'triangle';
| |
| } catch (e) { return 'triangle'; }
| |
| })();
| |
|
| |
| function setTimbre(t) {
| |
| if (TIMBRES.indexOf(t) < 0) return;
| |
| timbre = t;
| |
| try { localStorage.setItem(STORAGE_KEY, t); } catch (e) {}
| |
| document.querySelectorAll('.edo-chord-timbre').forEach(function (sel) {
| |
| sel.value = t;
| |
| });
| |
| }
| |
|
| |
| function getCtx() {
| |
| if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
| |
| 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() {
| |
| if (!ctx) return;
| |
| var now = ctx.currentTime + LOOK_AHEAD;
| |
| // Snapshot and clear before stopping, so onended handlers find no matches and just dispose
| |
| var toStop = activeNodes;
| |
| activeNodes = [];
| |
| toStop.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) {}
| |
| });
| |
| 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 + LOOK_AHEAD;
| |
| var attack = 0.02, sustain = 0.2, release = 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 = timbre;
| |
| osc.frequency.value = BASE_FREQ;
| |
| osc.detune.value = cents;
| |
| 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);
| |
|
| |
| var node = { osc: osc, gain: gain };
| |
| activeNodes.push(node);
| |
| osc.onended = function () { disposeNode(node); };
| |
| });
| |
|
| |
| if (btn) {
| |
| btn.classList.add('playing');
| |
| activeBtn = btn;
| |
| setTimeout(function () {
| |
| if (activeBtn === btn) { btn.classList.remove('playing'); activeBtn = null; }
| |
| }, totalDur * 1000);
| |
| }
| |
| }
| |
|
| |
| 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 timbre';
| |
| TIMBRES.forEach(function (t) {
| |
| var opt = document.createElement('option');
| |
| opt.value = t;
| |
| opt.textContent = TIMBRE_LABELS[t];
| |
| if (t === timbre) opt.selected = true;
| |
| sel.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 cents;
| |
| if (btn.dataset.cents) {
| |
| cents = btn.dataset.cents.split(',').map(Number);
| |
| } 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);
| |
| }
| |
|
| |
| 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();
| |
| }
| |
| }());
| |