Files
gamercomp/frontend/public/assets/music/musicEngine.js
T
2026-04-30 02:14:25 +00:00

849 lines
32 KiB
JavaScript

// musicEngine.js — Procedural music engine using Tone.js
// Loaded by HTML5 games via <script> tag; reads song definitions from MusicLibrary
// Global: window.MusicEngine
(function() {
'use strict';
// ─── Constants ───────────────────────────────────────────
var NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
var SCALES = {
major: [0, 2, 4, 5, 7, 9, 11],
minor: [0, 2, 3, 5, 7, 8, 10],
dorian: [0, 2, 3, 5, 7, 9, 10],
phrygian: [0, 1, 3, 5, 7, 8, 10],
lydian: [0, 2, 4, 6, 7, 9, 11],
mixolydian: [0, 2, 4, 5, 7, 9, 10],
harmonicMinor: [0, 2, 3, 5, 7, 8, 11],
melodicMinor: [0, 2, 3, 5, 7, 9, 11],
pentatonicMajor: [0, 2, 4, 7, 9],
pentatonicMinor: [0, 3, 5, 7, 10],
blues: [0, 3, 5, 6, 7, 10],
wholeTone: [0, 2, 4, 6, 8, 10],
chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
};
// Roman numeral to scale degree (0-indexed)
var ROMAN = { i: 0, ii: 1, iii: 2, iv: 3, v: 4, vi: 5, vii: 6 };
// Short synth type names to Tone.js constructor names
var SYNTH_MAP = {
FM: 'FMSynth', AM: 'AMSynth', Mono: 'MonoSynth', Poly: 'PolySynth',
Membrane: 'MembraneSynth', Metal: 'MetalSynth', Pluck: 'PluckSynth',
Duo: 'DuoSynth', Noise: 'NoiseSynth',
};
// ─── Seeded PRNG ─────────────────────────────────────────
function hashStr(s) {
var h = 0;
for (var i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
return Math.abs(h) || 1;
}
function seededRandom(seed) {
var s = hashStr(String(seed));
return function() {
s = (s * 16807) % 2147483647;
return (s - 1) / 2147483646;
};
}
// ─── Note Utilities ──────────────────────────────────────
function noteToMidi(n) {
var m = n.match(/^([A-G]#?)(\d+)$/);
return m ? (parseInt(m[2]) + 1) * 12 + NOTES.indexOf(m[1]) : 60;
}
function midiToNote(m) {
return NOTES[m % 12] + (Math.floor(m / 12) - 1);
}
function getScaleNotes(root, scale, octave) {
var ri = NOTES.indexOf(root);
if (ri < 0) return [];
return (SCALES[scale] || SCALES.major).map(function(semi) {
return midiToNote((octave + 1) * 12 + ri + semi);
});
}
function multiOctave(root, scale, lo, hi) {
var notes = [];
for (var o = lo; o <= hi; o++) notes = notes.concat(getScaleNotes(root, scale, o));
return notes;
}
// ─── Chord Parsing ───────────────────────────────────────
function parseChords(str, key, scale) {
if (!str || !str.trim()) return [];
var ri = NOTES.indexOf(key), intervals = SCALES[scale] || SCALES.major;
return str.trim().split(/\s+/).map(function(sym) {
var s = sym, sf = 0;
// Check for accidentals prefix (b or #)
if (s[0] === 'b') { sf = -1; s = s.slice(1); }
else if (s[0] === '#') { sf = 1; s = s.slice(1); }
// Check for 7th suffix
var has7 = s.endsWith('7');
if (has7) s = s.slice(0, -1);
// Check for diminished suffix
var dim = s.endsWith('\u00b0');
if (dim) s = s.slice(0, -1);
// Uppercase = major triad, lowercase = minor triad
var isUpper = s[0] === s[0].toUpperCase();
var deg = ROMAN[s.toLowerCase()];
if (deg === undefined) deg = 0;
var chordRoot = (ri + (intervals[deg % intervals.length] || 0) + sf + 12) % 12;
var baseMidi = 48 + chordRoot; // octave 3
var tones = dim ? [baseMidi, baseMidi + 3, baseMidi + 6]
: isUpper ? [baseMidi, baseMidi + 4, baseMidi + 7]
: [baseMidi, baseMidi + 3, baseMidi + 7];
if (has7) tones.push(isUpper ? baseMidi + 11 : baseMidi + 10);
return {
symbol: sym, root: midiToNote(baseMidi), rootMidi: baseMidi,
notes: tones.map(midiToNote), midiNotes: tones,
};
});
}
// ─── Event shorthand constructors ────────────────────────
function E(note, dur, vel, time) { return { note: note, duration: dur, velocity: vel, time: time }; }
function D(voice, time, vel) { return { voice: voice, time: time, velocity: vel }; }
// ─── Melody Generators ───────────────────────────────────
// Each: (scaleNotes, chord, intensity, barNum, prev, rng) -> [{note, duration, velocity, time}]
var melGen = {
'arp-up': function(sn, c, I) {
var n = [c.midiNotes[0], c.midiNotes[1], c.midiNotes[2], c.midiNotes[0] + 12];
return n.map(function(m, i) { return E(midiToNote(m), '8n', 0.5 + I * 0.4, i * 0.5); });
},
'arp-down': function(sn, c, I) {
var n = [c.midiNotes[0] + 12, c.midiNotes[2], c.midiNotes[1], c.midiNotes[0]];
return n.map(function(m, i) { return E(midiToNote(m), '8n', 0.5 + I * 0.4, i * 0.5); });
},
'arp-bounce': function(sn, c, I) {
var t = c.midiNotes, s = [t[0], t[1], t[2], t[1], t[0], t[2], t[1], t[0]];
return s.map(function(m, i) { return E(midiToNote(m), '8n', 0.45 + I * 0.4, i * 0.5); });
},
'stepwise': function(sn, c, I, b, p, rng) {
var ev = [], si = Math.max(0, Math.floor(sn.length * 0.3)), t = 0;
for (var i = 0; i < 6 && t < 4; i++) {
var d = rng() > 0.5 ? 0.5 : 1;
ev.push(E(sn[Math.min(si + i, sn.length - 1)], d > 0.75 ? '4n' : '8n', 0.5 + I * 0.35, t));
t += d;
}
return ev;
},
'leap': function(sn, c, I, b, p, rng) {
var ev = [], idx = Math.floor(sn.length * 0.3), t = 0;
for (var i = 0; i < 4 && t < 4; i++) {
ev.push(E(sn[Math.min(idx, sn.length - 1)], '4n', 0.55 + I * 0.35, t));
idx += Math.floor(rng() * 3) + 2;
if (idx >= sn.length) idx = Math.floor(sn.length * 0.3);
t += rng() > 0.3 ? 1 : 0.5;
}
return ev;
},
'sustained': function(sn, c, I) {
return [E(c.notes[0], '2n', 0.4 + I * 0.3, 0), E(c.notes[2] || c.notes[1], '2n', 0.35 + I * 0.3, 2)];
},
'rhythmic': function(sn, c, I, b, p, rng) {
var ev = [], r = c.notes[0];
for (var i = 0; i < 16; i++) {
if (rng() < 0.5 + I * 0.3)
ev.push(E(r, '16n', (i % 4 === 0 ? 0.7 : 0.4) * (0.5 + I * 0.5), i * 0.25));
}
return ev;
},
'call-response': function(sn, c, I) {
var h = Math.min(Math.floor(sn.length * 0.7), sn.length - 1), l = Math.floor(sn.length * 0.3);
return [
E(sn[h], '8n', 0.6 + I * 0.3, 0),
E(sn[Math.min(h + 1, sn.length - 1)], '8n', 0.55 + I * 0.3, 0.5),
E(sn[l], '8n', 0.5 + I * 0.3, 2),
E(sn[Math.max(l - 1, 0)], '4n', 0.5 + I * 0.3, 2.5),
];
},
'pentatonic': function(sn, c, I, b, p, rng) {
var ri = NOTES.indexOf(c.root.replace(/\d+/, ''));
var pn = SCALES.pentatonicMinor.map(function(s) { return midiToNote(48 + ri + s); });
var ev = [], t = 0;
while (t < 4) { ev.push(E(pn[Math.floor(rng() * pn.length)], '8n', 0.45 + I * 0.4, t)); t += 0.5; }
return ev;
},
'ostinato': function(sn, c, I) {
var si = 0;
for (var i = 0; i < sn.length; i++) if (noteToMidi(sn[i]) >= c.rootMidi) { si = i; break; }
var motif = [];
for (var j = 0; j < Math.min(4, sn.length - si); j++) motif.push(sn[si + j]);
var ev = [];
for (var b = 0; b < 4; b++) ev.push(E(motif[b % motif.length], '8n', 0.5 + I * 0.35, b));
return ev;
},
'descending': function(sn, c, I) {
var s = Math.min(Math.floor(sn.length * 0.7), sn.length - 1), ev = [], t = 0;
for (var i = s; i >= 0 && t < 4; i--, t += 0.5) ev.push(E(sn[i], '8n', 0.5 + I * 0.35, t));
return ev;
},
'ascending': function(sn, c, I) {
var s = Math.floor(sn.length * 0.2), ev = [], t = 0;
for (var i = s; i < sn.length && t < 4; i++, t += 0.5) ev.push(E(sn[i], '8n', 0.5 + I * 0.35, t));
return ev;
},
'chromatic': function(sn, c, I) {
var ev = [], m = c.rootMidi, t = 0;
for (var i = 0; i < 8 && t < 4; i++) {
ev.push(E(midiToNote(m), '8n', 0.5 + I * 0.3, t));
m += (i % 2 === 0) ? 1 : 2; t += 0.5;
}
return ev;
},
'fade': function(sn, c, I, bar) {
var ev = [], cnt = Math.max(1, 8 - bar * 2);
for (var i = 0; i < cnt && i * 0.5 < 4; i++)
ev.push(E(c.notes[i % c.notes.length], '4n', Math.max(0.1, (0.5 + I * 0.3) * (1 - i / 8)), i * 0.5));
return ev;
},
'trill': function(sn, c, I) {
var idx = 0;
for (var i = 0; i < sn.length; i++) if (noteToMidi(sn[i]) >= c.rootMidi) { idx = i; break; }
var n1 = sn[idx], n2 = sn[Math.min(idx + 1, sn.length - 1)], ev = [];
for (var t = 0; t < 4; t += 0.25)
ev.push(E(Math.round(t / 0.25) % 2 === 0 ? n1 : n2, '16n', 0.4 + I * 0.35, t));
return ev;
},
'fanfare': function(sn, c, I) {
var v = 0.6 + I * 0.25, V = 0.7 + I * 0.25;
return [E(c.notes[0], '4n.', V, 0), E(c.notes[1] || c.notes[0], '8n', v, 1.5),
E(c.notes[2] || c.notes[0], '4n.', V, 2), E(c.notes[0], '8n', v, 3.5)];
},
'blues': function(sn, c, I, b, p, rng) {
var rm = c.rootMidi, bn = [rm, rm + 3, rm + 5, rm + 6, rm + 7, rm + 10], ev = [], t = 0;
while (t < 4) {
var sw = (Math.round(t * 2) % 2 === 1) ? 0.08 : 0;
ev.push(E(midiToNote(bn[Math.floor(rng() * bn.length)]), '8n', 0.5 + I * 0.4, t + sw));
t += 0.5;
}
return ev;
},
'staccato': function(sn, c, I, b, p, rng) {
var ev = [];
for (var t = 0; t < 4; t += 0.5)
if (rng() < 0.7) ev.push(E(sn[Math.floor(rng() * sn.length)], '16n', 0.5 + I * 0.4, t));
return ev;
},
'legato': function(sn, c, I) {
var s = Math.floor(sn.length * 0.3);
return [E(sn[s], '2n', 0.45 + I * 0.3, 0),
E(sn[Math.min(s + 2, sn.length - 1)], '4n', 0.45 + I * 0.3, 2),
E(sn[Math.min(s + 4, sn.length - 1)], '4n', 0.4 + I * 0.3, 3)];
},
'random': function(sn, c, I, b, p, rng) {
var ev = [], t = 0, ds = [0.25, 0.5, 1], dn = ['16n', '8n', '4n'];
while (t < 4) {
var n = rng() < 0.5 ? c.notes[Math.floor(rng() * c.notes.length)] : sn[Math.floor(rng() * sn.length)];
var di = Math.floor(rng() * 3);
ev.push(E(n, dn[di], 0.4 + I * 0.4 * rng(), t));
t += ds[di];
}
return ev;
},
'silence': function() { return []; },
};
// ─── Bass Generators ─────────────────────────────────────
// Each: (chord, chordTones, scaleNotes, intensity, rng) -> [{note, duration, velocity, time}]
var bassGen = {
'root': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), v = 0.6 + I * 0.3, v2 = 0.55 + I * 0.3;
return [E(r, '4n', v, 0), E(r, '4n', v2, 1), E(r, '4n', v, 2), E(r, '4n', v2, 3)];
},
'root-fifth': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), f = midiToNote(c.rootMidi - 5), v = 0.6 + I * 0.3;
return [E(r, '4n', v, 0), E(f, '4n', v - 0.05, 1), E(r, '4n', v, 2), E(f, '4n', v - 0.05, 3)];
},
'driving': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), ev = [];
for (var t = 0; t < 4; t += 0.5)
ev.push(E(r, '8n', (t % 1 === 0 ? 0.65 : 0.5) + I * 0.25, t));
return ev;
},
'walking': function(c, _, __, I) {
var b = c.rootMidi - 12;
return [b, b + 2, b + 4, b + 5].map(function(m, i) {
return E(midiToNote(m), '4n', 0.55 + I * 0.3, i);
});
},
'arpeggiated': function(c) {
var a = c.midiNotes.map(function(m) { return m - 12; }), ev = [], t = 0;
for (var i = 0; t < 4; i++, t += 0.5)
ev.push(E(midiToNote(a[i % a.length]), '8n', 0.55, t));
return ev;
},
'octave': function(c, _, __, I) {
var lo = midiToNote(c.rootMidi - 24), hi = midiToNote(c.rootMidi - 12), v = 0.6 + I * 0.3;
return [E(lo, '4n', v, 0), E(hi, '4n', v - 0.05, 1), E(lo, '4n', v, 2), E(hi, '4n', v - 0.05, 3)];
},
'syncopated': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), v = 0.6 + I * 0.25;
return [E(r, '4n', v + 0.05, 0), E(r, '8n', v - 0.05, 1.5), E(r, '4n', v - 0.05, 2.5)];
},
'pedal': function(c) {
return [E(midiToNote(c.rootMidi - 12), '1n', 0.55, 0)];
},
'pumping': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), ev = [];
for (var t = 0; t < 4; t += 0.5)
ev.push(E(r, '8n', 0.5 + I * 0.25 + ((t === 0 || t === 2) ? 0.15 : 0), t));
return ev;
},
'fifths': function(c, _, __, I) {
var r = c.rootMidi - 12, f = r + 7, p = r + 4, v = 0.6 + I * 0.3;
return [E(midiToNote(r), '4n', v, 0), E(midiToNote(f), '8n', v - 0.05, 1),
E(midiToNote(p), '8n', v - 0.1, 1.5), E(midiToNote(r), '4n', v, 2),
E(midiToNote(f), '4n', v - 0.05, 3)];
},
'chromatic': function(c) {
var b = c.rootMidi - 12;
return [E(midiToNote(b - 2), '4n', 0.5, 0), E(midiToNote(b - 1), '4n', 0.55, 1),
E(midiToNote(b), '2n', 0.6, 2)];
},
'silence': function() { return []; },
};
// ─── Drum Generators ─────────────────────────────────────
// Each: (intensity, timeSig, barNum) -> [{voice, time, velocity}]
var drumGen = {
'standard': function(I) {
var e = [D('kick', 0, 0.8), D('kick', 2, 0.75), D('snare', 1, 0.7), D('snare', 3, 0.7)];
for (var t = 0; t < 4; t += 0.5)
e.push(D('hihat', t, (t % 1 === 0 ? 0.4 : 0.25) * (0.5 + I * 0.5)));
return e;
},
'four-on-floor': function(I) {
var e = [D('snare', 1, 0.65), D('snare', 3, 0.65)];
for (var b = 0; b < 4; b++) e.push(D('kick', b, 0.8));
for (var t = 0; t < 4; t += 0.5)
e.push(D('hihat', t, (t % 1 === 0 ? 0.35 : 0.2) * (0.5 + I * 0.5)));
return e;
},
'half-time': function() {
return [D('kick', 0, 0.8), D('snare', 2, 0.75), D('hihat', 0, 0.3), D('hihat', 2, 0.25)];
},
'breakbeat': function() {
var e = [D('kick', 0, 0.8), D('kick', 1.5, 0.7), D('kick', 3, 0.75),
D('snare', 1, 0.7), D('snare', 3, 0.7)];
for (var t = 0; t < 4; t += 0.5)
e.push(D('hihat', t, (t % 1 === 0 ? 0.3 : 0.2) + (t === 1.5 ? 0.15 : 0)));
return e;
},
'shuffle': function() {
var e = [D('kick', 0, 0.8), D('kick', 2, 0.75), D('snare', 1, 0.7), D('snare', 3, 0.7)];
for (var b = 0; b < 4; b++) { e.push(D('hihat', b, 0.35)); e.push(D('hihat', b + 0.67, 0.2)); }
return e;
},
'full': function(I) {
var e = [D('kick', 0, 0.85), D('kick', 1.5, 0.6), D('kick', 2, 0.8), D('kick', 3.5, 0.55),
D('snare', 1, 0.75), D('snare', 3, 0.75), D('tom', 2.5, 0.5), D('crash', 0, 0.3)];
for (var t = 0; t < 4; t += 0.25)
e.push(D('hihat', t, (t % 0.5 === 0 ? 0.3 : 0.15) * (0.5 + I * 0.5)));
return e;
},
'sparse': function() {
return [D('kick', 0, 0.7), D('snare', 2, 0.55)];
},
'build': function(I, ts, bar) {
var e = [D('kick', 0, 0.7)], density = Math.min(1, (bar || 0) * 0.15 + 0.1);
if (density > 0.2) e.push(D('kick', 2, 0.65));
if (density > 0.4) { e.push(D('snare', 1, 0.5)); e.push(D('snare', 3, 0.5)); }
if (density > 0.6) for (var t = 0; t < 4; t += 0.5) e.push(D('hihat', t, 0.25));
if (density > 0.8) e.push(D('crash', 0, 0.35));
return e;
},
'tribal': function() {
return [D('tom', 0, 0.8), D('tom', 0.75, 0.5), D('tom', 1.5, 0.65), D('kick', 2, 0.75),
D('tom', 2.5, 0.6), D('tom', 3, 0.7), D('tom', 3.5, 0.45)];
},
'double-time': function(I) {
var e = [];
for (var t = 0; t < 4; t += 0.5) e.push(D('kick', t, t % 1 === 0 ? 0.75 : 0.5));
for (var s = 0.5; s < 4; s += 1) e.push(D('snare', s, 0.6));
for (var h = 0; h < 4; h += 0.25) e.push(D('hihat', h, 0.2 + I * 0.15));
return e;
},
'kick-only': function() {
return [D('kick', 0, 0.75), D('kick', 1, 0.7), D('kick', 2, 0.75), D('kick', 3, 0.7)];
},
'hats-only': function(I) {
var e = [], step = I > 0.7 ? 0.25 : 0.5;
for (var t = 0; t < 4; t += step) e.push(D('hihat', t, t % 1 === 0 ? 0.4 : 0.25));
return e;
},
'march': function(I) {
var e = [D('kick', 0, 0.8), D('kick', 2, 0.75)];
for (var b = 0; b < 4; b++) {
e.push(D('snare', b, 0.65));
if (I > 0.5) { e.push(D('snare', b + 0.25, 0.35)); e.push(D('snare', b + 0.5, 0.3)); }
}
return e;
},
'waltz': function() {
return [D('kick', 0, 0.8), D('hihat', 0.67, 0.35), D('hihat', 1.33, 0.3),
D('kick', 2, 0.7), D('hihat', 2.67, 0.35), D('hihat', 3.33, 0.3)];
},
'none': function() { return []; },
};
// ─── Engine State ────────────────────────────────────────
var Tone = null;
var initialized = false;
var currentSong = null;
var playing = false;
var masterGain = null;
var currentNodes = []; // all disposable Tone.js nodes for current song
var currentParts = []; // scheduled parts for cleanup
var loopCbId = null;
// ─── Tone.js CDN Loading ─────────────────────────────────
async function loadToneJs() {
if (window.Tone) return window.Tone;
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = 'https://unpkg.com/tone@15.1.22/build/Tone.js';
script.onload = function() { resolve(window.Tone); };
script.onerror = function() { reject(new Error('Failed to load Tone.js')); };
document.head.appendChild(script);
});
}
// ─── Synth Creation ──────────────────────────────────────
var ROLE_DEFAULTS = {
lead: { envelope: { attack: 0.03, decay: 0.2, sustain: 0.8, release: 0.4 }, volume: -6 },
pad: { envelope: { attack: 0.5, decay: 0.3, sustain: 0.9, release: 2.0 }, volume: -12 },
bass: { envelope: { attack: 0.01, decay: 0.15, sustain: 0.7, release: 0.2 }, volume: -8 },
};
function createSynth(typeName, role, custom) {
var ctorName = SYNTH_MAP[typeName] || 'Synth';
var Ctor = Tone[ctorName];
if (!Ctor) Ctor = Tone.Synth;
var settings = Object.assign({}, ROLE_DEFAULTS[role] || {}, custom || {});
// PolySynth wraps an inner synth type
if (ctorName === 'PolySynth') return new Tone.PolySynth(Tone.Synth, settings);
// These synth types have non-standard constructors
if (ctorName === 'PluckSynth' || ctorName === 'NoiseSynth' || ctorName === 'MetalSynth') {
try { return new Ctor(custom || {}); } catch (e) { return new Ctor(); }
}
try { return new Ctor(settings); } catch (e) { return new Ctor(); }
}
// ─── Drum Kit ────────────────────────────────────────────
function createDrumKit() {
return {
kick: new Tone.MembraneSynth({ pitchDecay: 0.05, octaves: 6,
envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.4 }, volume: -6 }),
snare: new Tone.NoiseSynth({ noise: { type: 'white' },
envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.1 }, volume: -10 }),
hihat: new Tone.MetalSynth({ frequency: 400,
envelope: { attack: 0.001, decay: 0.05, release: 0.01 },
harmonicity: 5.1, modulationIndex: 32, resonance: 4000, octaves: 1.5, volume: -18 }),
tom: new Tone.MembraneSynth({ pitchDecay: 0.08, octaves: 4,
envelope: { attack: 0.001, decay: 0.25, sustain: 0, release: 0.3 }, volume: -8 }),
crash: new Tone.NoiseSynth({ noise: { type: 'white' },
envelope: { attack: 0.001, decay: 0.8, sustain: 0, release: 0.5 }, volume: -16 }),
};
}
// ─── Effects Chain ───────────────────────────────────────
// instruments -> filter -> [chorus] -> [distortion] -> delay -> reverb -> destination
function createFxChain(fx) {
var nodes = [], chain = [];
var filter = new Tone.Filter({ frequency: fx.filter || 5000, type: 'lowpass' });
nodes.push(filter); chain.push(filter);
if (fx.chorus > 0) {
var chorus = new Tone.Chorus({ frequency: 1.5, delayTime: 3.5, depth: 0.7, wet: fx.chorus });
chorus.start();
nodes.push(chorus); chain.push(chorus);
}
if (fx.distortion > 0) {
var dist = new Tone.Distortion({ distortion: fx.distortion, wet: fx.distortion });
nodes.push(dist); chain.push(dist);
}
var delay = new Tone.FeedbackDelay({ delayTime: '8n', feedback: 0.3, wet: fx.delay || 0 });
nodes.push(delay); chain.push(delay);
var reverb = new Tone.Reverb({ decay: 2.5, wet: fx.reverb || 0 });
nodes.push(reverb); chain.push(reverb);
return { nodes: nodes, chain: chain };
}
// ─── Cleanup ─────────────────────────────────────────────
function disposeAll() {
if (Tone) {
try { Tone.getTransport().cancel(0); } catch (e) {}
try { Tone.getTransport().stop(); } catch (e) {}
}
currentParts.forEach(function(p) { try { p.dispose(); } catch (e) {} });
currentParts = [];
currentNodes.forEach(function(n) { try { if (n && n.dispose) n.dispose(); } catch (e) {} });
currentNodes = [];
if (loopCbId !== null && Tone) {
try { Tone.getTransport().clear(loopCbId); } catch (e) {}
loopCbId = null;
}
}
// ─── Song Library Access ─────────────────────────────────
function getSongs() {
return (window.MusicLibrary && Array.isArray(window.MusicLibrary.songs))
? window.MusicLibrary.songs : [];
}
// ─── Song Scheduling ────────────────────────────────────
function scheduleSong(song) {
var transport = Tone.getTransport();
transport.cancel(0);
transport.stop();
transport.bpm.value = song.tempo || 120;
transport.position = 0;
var rng = seededRandom(song.id || 'default');
var key = song.key || 'C', scale = song.scale || 'minor';
var scaleNotes = multiOctave(key, scale, 3, 5);
// Create effects chain
var fx = Object.assign({ reverb: 0.2, delay: 0.1, filter: 5000, chorus: 0, distortion: 0 }, song.fx || {});
var effects = createFxChain(fx);
currentNodes = currentNodes.concat(effects.nodes);
// Master gain -> destination
masterGain = new Tone.Gain(0.7).toDestination();
currentNodes.push(masterGain);
// Wire effects chain: each node connects to the next, last connects to master gain
for (var ci = 0; ci < effects.chain.length - 1; ci++)
effects.chain[ci].connect(effects.chain[ci + 1]);
effects.chain[effects.chain.length - 1].connect(masterGain);
// Create melodic synths and route through effects
var synths = song.synths || { lead: 'FM', pad: 'Poly', bass: 'Mono' };
var ss = song.synthSettings || {};
var lead = createSynth(synths.lead, 'lead', ss.lead);
var pad = createSynth(synths.pad, 'pad', ss.pad);
var bass = createSynth(synths.bass, 'bass', ss.bass);
[lead, pad, bass].forEach(function(s) { currentNodes.push(s); s.connect(effects.chain[0]); });
// Create drum kit and route directly to master gain (no effects)
var drums = createDrumKit();
['kick', 'snare', 'hihat', 'tom', 'crash'].forEach(function(k) {
currentNodes.push(drums[k]);
drums[k].connect(masterGain);
});
// Build the complete event list from the song form
var form = song.form || ['verse', 'chorus'];
var sections = song.sections || {};
var allEvts = [], barOffset = 0;
for (var fi = 0; fi < form.length; fi++) {
var sec = sections[form[fi]];
if (!sec) continue;
var nBars = sec[0] || 4;
var chStr = sec[1] || 'i';
var mPat = sec[2] || 'stepwise';
var bPat = sec[3] || 'root';
var dPat = sec[4] || 'standard';
var intensity = sec[5] !== undefined ? sec[5] : 0.5;
var chords = parseChords(chStr, key, scale);
if (!chords.length) chords = parseChords('i', key, scale);
var mG = melGen[mPat] || melGen.stepwise;
var bG = bassGen[bPat] || bassGen.root;
var dG = drumGen[dPat] || drumGen.standard;
for (var bar = 0; bar < nBars; bar++) {
var chord = chords[bar % chords.length];
var bt = (barOffset + bar) * 4; // bar time in beats
// Melody events
try {
mG(scaleNotes, chord, intensity, bar, null, rng).forEach(function(e) {
allEvts.push({ type: 'lead', note: e.note, duration: e.duration, velocity: e.velocity, time: bt + e.time });
});
} catch (e) {}
// Pad chords (whole notes on chord tones)
if (chord.notes && chord.notes.length >= 2) {
for (var cn = 0; cn < Math.min(chord.notes.length, 3); cn++)
allEvts.push({ type: 'pad', note: chord.notes[cn], duration: '1n', velocity: 0.25 + intensity * 0.2, time: bt });
}
// Bass events
try {
bG(chord, chord.notes, scaleNotes, intensity, rng).forEach(function(e) {
allEvts.push({ type: 'bass', note: e.note, duration: e.duration, velocity: e.velocity, time: bt + e.time });
});
} catch (e) {}
// Drum events
try {
dG(intensity, 4, bar).forEach(function(e) {
allEvts.push({ type: 'drum', voice: e.voice, velocity: e.velocity, time: bt + e.time });
});
} catch (e) {}
}
barOffset += nBars;
}
var totalSeconds = (barOffset * 4 / song.tempo) * 60;
// Schedule all events on the Tone.js Transport
allEvts.forEach(function(evt) {
var timeInSec = (evt.time / song.tempo) * 60;
transport.schedule(function(t) {
try {
if (evt.type === 'lead') {
lead.triggerAttackRelease(evt.note, evt.duration, t, evt.velocity);
} else if (evt.type === 'pad' && typeof pad.triggerAttackRelease === 'function') {
pad.triggerAttackRelease(evt.note, evt.duration, t, evt.velocity);
} else if (evt.type === 'bass') {
bass.triggerAttackRelease(evt.note, evt.duration, t, evt.velocity);
} else if (evt.type === 'drum') {
var inst = drums[evt.voice];
if (!inst) return;
if (evt.voice === 'kick' || evt.voice === 'tom')
inst.triggerAttackRelease(evt.voice === 'kick' ? 'C1' : 'G2', '8n', t, evt.velocity);
else if (evt.voice === 'snare' || evt.voice === 'crash')
inst.triggerAttackRelease('8n', t, evt.velocity);
else if (evt.voice === 'hihat')
inst.triggerAttackRelease('32n', t, evt.velocity);
}
} catch (err) { /* skip individual note errors */ }
}, timeInSec);
});
// Schedule loop: when song ends, dispose and re-schedule (skip intro on loop)
loopCbId = transport.schedule(function() {
transport.stop();
setTimeout(function() {
if (playing && currentSong && currentSong.id === song.id) {
disposeAll();
scheduleSong(song);
Tone.getTransport().start();
if (window.MusicEngine.onSongEnd) {
try { window.MusicEngine.onSongEnd(song); } catch (e) {}
}
}
}, 50);
}, totalSeconds);
return { totalSeconds: totalSeconds, totalBars: barOffset };
}
// ─── Metadata helper ─────────────────────────────────────
function meta(s) {
return { id: s.id, name: s.name, mood: s.mood, energy: s.energy, tempo: s.tempo, tags: s.tags, desc: s.desc };
}
// ─── Public API ──────────────────────────────────────────
window.MusicEngine = {
onSongEnd: null,
/** Initialize the engine. Must be called on a user gesture (click/touch). */
async init() {
if (initialized) return;
Tone = await loadToneJs();
await Tone.start();
initialized = true;
},
/** Play a random song matching the given mood. Returns the song object or null. */
async playRandomSong(mood) {
if (!initialized) await this.init();
var songs = getSongs().filter(function(s) {
return s.mood && s.mood.toLowerCase() === mood.toLowerCase();
});
if (!songs.length) return null;
return this.playSong(songs[Math.floor(Math.random() * songs.length)].id);
},
/** Play a specific song by ID. Returns the song object or null. */
async playSong(songId) {
if (!initialized) await this.init();
var song = null;
getSongs().forEach(function(s) { if (s.id === songId) song = s; });
if (!song) return null;
this.stopMusic();
try {
currentSong = song;
playing = true;
scheduleSong(song);
Tone.getTransport().start();
return song;
} catch (e) {
this.stopMusic();
return null;
}
},
/** Stop the current song immediately and clean up all Tone.js nodes. */
stopMusic() {
playing = false;
currentSong = null;
disposeAll();
},
/** Set master volume (0 = silent, 1 = full). */
setVolume(level) {
var v = Math.max(0, Math.min(1, level));
if (masterGain) masterGain.gain.value = v;
if (Tone && Tone.getDestination) {
try { Tone.getDestination().volume.value = v === 0 ? -Infinity : 20 * Math.log10(v); } catch (e) {}
}
},
/** Fade out current song over duration (seconds), then stop. */
fadeOut(duration) {
duration = duration || 2;
if (!masterGain || !playing) { this.stopMusic(); return; }
var self = this;
try {
masterGain.gain.linearRampTo(0, duration);
setTimeout(function() { self.stopMusic(); }, duration * 1000 + 100);
} catch (e) { self.stopMusic(); }
},
/** Get the full catalog of songs with metadata. */
getCatalog() {
return getSongs().map(meta);
},
/** Filter songs by criteria: { mood, minEnergy, maxEnergy, minTempo, maxTempo, tags }. */
findSongs(filters) {
if (!filters) return this.getCatalog();
return getSongs().filter(function(s) {
if (filters.mood && s.mood && s.mood.toLowerCase() !== filters.mood.toLowerCase()) return false;
if (filters.minEnergy !== undefined && s.energy < filters.minEnergy) return false;
if (filters.maxEnergy !== undefined && s.energy > filters.maxEnergy) return false;
if (filters.minTempo !== undefined && s.tempo < filters.minTempo) return false;
if (filters.maxTempo !== undefined && s.tempo > filters.maxTempo) return false;
if (filters.tags && filters.tags.length) {
var st = s.tags || [], found = false;
for (var i = 0; i < filters.tags.length; i++)
if (st.indexOf(filters.tags[i]) >= 0) { found = true; break; }
if (!found) return false;
}
return true;
}).map(meta);
},
/** Get metadata for a specific song (includes key and scale). */
getMetadata(songId) {
var result = null;
getSongs().forEach(function(s) {
if (s.id === songId) result = {
id: s.id, name: s.name, mood: s.mood, energy: s.energy, tempo: s.tempo,
key: s.key, scale: s.scale, tags: s.tags, desc: s.desc,
};
});
return result;
},
/** Get currently playing song info, or null. */
getNowPlaying() {
return (currentSong && playing) ? meta(currentSong) : null;
},
/** Check if music is currently playing. */
isPlaying() { return playing; },
/** Pause playback. */
pause() {
if (playing && Tone) try { Tone.getTransport().pause(); } catch (e) {}
},
/** Resume playback after pause. */
resume() {
if (playing && Tone) try { Tone.getTransport().start(); } catch (e) {}
},
};
})();