- Isometric canvas renderer (depth-sorted, FOV/fog, additive lighting) - 3 classes x 20 skills, 4 acts x 4 floors + boss lairs, torment I-X - Diablo-style loot: rarities, affix tiers, 14 legendaries, vendor, stash - Rogue camp with 6 NPCs: Charsi/Akara/Kashya/Cain/Gheed/storage - NPC quest chain (accept -> hunt -> turn in) with rewards & gating - Procedural WebAudio SFX + generative music, EN/VI localization - Saves, settings, waypoints, hardcore mode, PWA manifest - 93-assertion headless suite + browser E2E via CDP
439 lines
18 KiB
JavaScript
439 lines
18 KiB
JavaScript
/* ============================================================
|
|
* Diablo2D — audio.js : procedural WebAudio engine
|
|
* Synthesized SFX + adaptive generative music. Zero assets.
|
|
* ============================================================ */
|
|
'use strict';
|
|
window.D2 = window.D2 || {};
|
|
(function (D2) {
|
|
|
|
let ctx = null;
|
|
let masterGain = null, sfxGain = null, musicGain = null;
|
|
let noiseBuf = null;
|
|
let unlocked = false;
|
|
|
|
const vol = { master: 0.8, music: 0.55, sfx: 0.9 };
|
|
|
|
let activeVoices = 0;
|
|
const MAX_VOICES = 28;
|
|
|
|
/* ---------- core plumbing ---------- */
|
|
|
|
function unlock() {
|
|
if (unlocked && ctx && ctx.state === 'running') return;
|
|
try {
|
|
if (!ctx) {
|
|
ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
masterGain = ctx.createGain();
|
|
sfxGain = ctx.createGain();
|
|
musicGain = ctx.createGain();
|
|
sfxGain.connect(masterGain);
|
|
musicGain.connect(masterGain);
|
|
masterGain.connect(ctx.destination);
|
|
applyVolumes();
|
|
noiseBuf = makeNoiseBuffer();
|
|
}
|
|
if (ctx.state === 'suspended') ctx.resume();
|
|
unlocked = true;
|
|
if (pendingMood) playMusic(pendingMood);
|
|
} catch (e) {
|
|
console.warn('[audio] init failed', e);
|
|
}
|
|
}
|
|
|
|
function makeNoiseBuffer() {
|
|
const len = ctx.sampleRate * 1.2;
|
|
const buf = ctx.createBuffer(1, len, ctx.sampleRate);
|
|
const d = buf.getChannelData(0);
|
|
for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
|
|
return buf;
|
|
}
|
|
|
|
function setVolumes(m, mu, sx) {
|
|
if (m !== undefined) vol.master = m;
|
|
if (mu !== undefined) vol.music = mu;
|
|
if (sx !== undefined) vol.sfx = sx;
|
|
applyVolumes();
|
|
}
|
|
|
|
function applyVolumes() {
|
|
if (!masterGain) return;
|
|
masterGain.gain.value = vol.master;
|
|
musicGain.gain.value = vol.music;
|
|
sfxGain.gain.value = vol.sfx;
|
|
}
|
|
|
|
function ready() { return unlocked && ctx && ctx.state === 'running'; }
|
|
|
|
function canPlay() {
|
|
if (!ready()) return false;
|
|
if (activeVoices >= MAX_VOICES) return false;
|
|
activeVoices++;
|
|
setTimeout(() => { activeViewsDec(); }, 4000);
|
|
return true;
|
|
}
|
|
function activeViewsDec() { activeVoices = Math.max(0, activeVoices - 1); }
|
|
|
|
/* ---------- synth primitives ---------- */
|
|
|
|
function outNode(dest, pan) {
|
|
if (pan !== undefined && ctx.createStereoPanner) {
|
|
const p = ctx.createStereoPanner();
|
|
p.pan.value = Math.max(-1, Math.min(1, pan));
|
|
p.connect(dest);
|
|
return p;
|
|
}
|
|
return dest;
|
|
}
|
|
|
|
/** basic enveloped oscillator */
|
|
function tone(o) {
|
|
if (!canPlay()) return;
|
|
const t0 = ctx.currentTime + (o.delay || 0);
|
|
const osc = ctx.createOscillator();
|
|
osc.type = o.type || 'sine';
|
|
osc.frequency.setValueAtTime(Math.max(20, o.freq), t0);
|
|
if (o.freqEnd) osc.frequency.exponentialRampToValueAtTime(Math.max(20, o.freqEnd), t0 + o.dur);
|
|
if (o.detune) osc.detune.value = o.detune;
|
|
|
|
const g = ctx.createGain();
|
|
const a = o.attack || 0.005;
|
|
const v = (o.vol || 0.5);
|
|
g.gain.setValueAtTime(0.0001, t0);
|
|
g.gain.exponentialRampToValueAtTime(v, t0 + a);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t0 + o.dur);
|
|
|
|
let node = g;
|
|
if (o.filter) {
|
|
const f = ctx.createBiquadFilter();
|
|
f.type = o.filter.type || 'lowpass';
|
|
f.frequency.value = o.filter.freq || 800;
|
|
f.Q.value = o.filter.q || 0.8;
|
|
g.connect(f); node = f;
|
|
}
|
|
|
|
osc.connect(g);
|
|
node.connect(outNode(o.dest || sfxGain, o.pan));
|
|
osc.start(t0);
|
|
osc.stop(t0 + o.dur + 0.05);
|
|
osc.onended = activeViewsDec;
|
|
}
|
|
|
|
/** enveloped filtered noise burst */
|
|
function noise(o) {
|
|
if (!canPlay()) return;
|
|
const t0 = ctx.currentTime + (o.delay || 0);
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noiseBuf;
|
|
src.loop = true;
|
|
src.playbackRate.value = o.rate || 1;
|
|
|
|
const f = ctx.createBiquadFilter();
|
|
f.type = o.filterType || 'bandpass';
|
|
f.frequency.setValueAtTime(o.freq || 1000, t0);
|
|
if (o.freqEnd) f.frequency.exponentialRampToValueAtTime(Math.max(30, o.freqEnd), t0 + o.dur);
|
|
f.Q.value = o.q || 1;
|
|
|
|
const g = ctx.createGain();
|
|
const a = o.attack || 0.004;
|
|
const v = (o.vol || 0.5);
|
|
g.gain.setValueAtTime(0.0001, t0);
|
|
g.gain.exponentialRampToValueAtTime(v, t0 + a);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t0 + o.dur);
|
|
|
|
src.connect(f); f.connect(g);
|
|
g.connect(outNode(o.dest || sfxGain, o.pan));
|
|
src.start(t0);
|
|
src.stop(t0 + o.dur + 0.05);
|
|
src.onended = activeViewsDec;
|
|
}
|
|
|
|
const mtof = m => 440 * Math.pow(2, (m - 69) / 12);
|
|
|
|
/* ---------- sfx recipes ---------- */
|
|
|
|
const sfxRecipes = {
|
|
click() { tone({ type:'square', freq:2200, dur:.04, vol:.10 }); },
|
|
hover() { tone({ type:'sine', freq:1400, dur:.03, vol:.05 }); },
|
|
error() { tone({ type:'square', freq:160, freqEnd:110, dur:.16, vol:.16 }); },
|
|
swing() { noise({ filterType:'bandpass', freq:2600, freqEnd:500, dur:.13, vol:.22, q:1.6 }); },
|
|
hit() { noise({ filterType:'lowpass', freq:900, freqEnd:200, dur:.09, vol:.32 });
|
|
tone({ type:'sine', freq:150, freqEnd:70, dur:.08, vol:.30 }); },
|
|
crit() { noise({ filterType:'lowpass', freq:900, freqEnd:200, dur:.09, vol:.32 });
|
|
tone({ type:'sine', freq:150, freqEnd:70, dur:.08, vol:.30 });
|
|
tone({ type:'triangle', freq:1800, freqEnd:900, dur:.12, vol:.18, delay:.02 }); },
|
|
playerhurt(){ tone({ type:'sawtooth', freq:190, freqEnd:80, dur:.18, vol:.26 });
|
|
noise({ filterType:'lowpass', freq:600, freqEnd:150, dur:.14, vol:.25 }); },
|
|
shoot() { noise({ filterType:'highpass', freq:1800, freqEnd:3400, dur:.07, vol:.17 }); },
|
|
fireball() { noise({ filterType:'lowpass', freq:400, freqEnd:2400, dur:.3, vol:.24 });
|
|
tone({ type:'sawtooth', freq:120, freqEnd:60, dur:.28, vol:.12 }); },
|
|
explode() { noise({ filterType:'lowpass', freq:2500, freqEnd:60, dur:.55, vol:.45 });
|
|
tone({ type:'sine', freq:90, freqEnd:34, dur:.5, vol:.42 }); },
|
|
ice() { tone({ type:'sine', freq:2300, dur:.14, vol:.11 });
|
|
tone({ type:'sine', freq:3100, dur:.12, vol:.09, delay:.05 });
|
|
noise({ filterType:'highpass', freq:5200, dur:.18, vol:.10 }); },
|
|
lightning() { noise({ filterType:'bandpass', freq:3000, q:.6, dur:.16, vol:.26 });
|
|
tone({ type:'sawtooth', freq:800, freqEnd:200, dur:.12, vol:.10 }); },
|
|
poison() { noise({ filterType:'bandpass', freq:500, q:2, freqEnd:220, dur:.3, vol:.18 }); },
|
|
coin() { tone({ type:'sine', freq:mtof(98), dur:.09, vol:.14 });
|
|
tone({ type:'sine', freq:mtof(105), dur:.14, vol:.14, delay:.06 }); },
|
|
pickup() { tone({ type:'triangle', freq:mtof(88), dur:.1, vol:.16 });
|
|
tone({ type:'triangle', freq:mtof(95), dur:.16, vol:.16, delay:.07 }); },
|
|
legendary() { [76,83,88,95].forEach((m,i)=>tone({type:'sine',freq:mtof(m),dur:.5,vol:.14,delay:i*.09}));
|
|
noise({filterType:'highpass',freq:6000,dur:.7,vol:.06,delay:.1}); },
|
|
potion() { tone({ type:'sine', freq:520, freqEnd:280, dur:.12, vol:.16 });
|
|
tone({ type:'sine', freq:430, freqEnd:210, dur:.14, vol:.16, delay:.11 }); },
|
|
levelup() { [64,68,71,76].forEach((m,i)=>tone({type:'triangle',freq:mtof(m),dur:.42,vol:.2,delay:i*.11}));
|
|
tone({ type:'sine', freq:mtof(88), dur:.9, vol:.12, delay:.44 }); },
|
|
die() { tone({ type:'sawtooth', freq:220, freqEnd:40, dur:.4, vol:.22 });
|
|
noise({ filterType:'lowpass', freq:800, freqEnd:100, dur:.35, vol:.2 }); },
|
|
bossdie() { noise({ filterType:'lowpass', freq:2500, freqEnd:60, dur:.55, vol:.45 });
|
|
tone({ type:'sine', freq:90, freqEnd:34, dur:.5, vol:.42 });
|
|
[52,46,40].forEach((m,i)=>tone({type:'sawtooth',freq:mtof(m),dur:1.1,vol:.22,delay:i*.16})); },
|
|
door() { tone({ type:'sawtooth', freq:90, freqEnd:130, dur:.5, vol:.12 });
|
|
noise({ filterType:'bandpass', freq:300, q:3, dur:.45, vol:.10 }); },
|
|
stairs() { tone({ type:'sine', freq:180, freqEnd:60, dur:.7, vol:.2 });
|
|
tone({ type:'sine', freq:270, freqEnd:90, dur:.7, vol:.12, delay:.1 }); },
|
|
shrine() { [57,64,69,72].forEach((m,i)=>tone({type:'sine',freq:mtof(m),dur:1.4,vol:.12,delay:i*.05,attack:.3}));
|
|
noise({filterType:'highpass',freq:7000,dur:1.2,vol:.05}); },
|
|
teleport() { tone({ type:'sine', freq:300, freqEnd:2400, dur:.25, vol:.16 });
|
|
noise({ filterType:'highpass', freq:1000, freqEnd:5000, dur:.25, vol:.14 }); },
|
|
buff() { [60,67,72].forEach((m,i)=>tone({type:'triangle',freq:mtof(m),dur:.7,vol:.14,delay:i*.04,attack:.05})); },
|
|
nova() { noise({ filterType:'bandpass', freq:300, freqEnd:3800, dur:.4, vol:.3, q:.7 });
|
|
tone({ type:'sawtooth', freq:70, freqEnd:200, dur:.35, vol:.16 }); },
|
|
bossroar() { tone({ type:'sawtooth', freq:110, freqEnd:38, dur:1.2, vol:.4 });
|
|
tone({ type:'square', freq:74, freqEnd:30, dur:1.2, vol:.22 });
|
|
noise({ filterType:'lowpass', freq:900, freqEnd:80, dur:1.1, vol:.3 }); },
|
|
buy() { coin(); coin(); },
|
|
sell() { coin(); },
|
|
questdone() { [67,72,76].forEach((m,i)=>tone({type:'sine',freq:mtof(m),dur:.6,vol:.16,delay:i*.1,attack:.03})); },
|
|
};
|
|
|
|
function sfx(name, opts = {}) {
|
|
if (!ready()) return;
|
|
const fn = sfxRecipes[name];
|
|
if (!fn) return;
|
|
try { fn(opts); } catch (e) { /* never crash gameplay over audio */ }
|
|
}
|
|
|
|
/* ---------- generative music ---------- */
|
|
|
|
// scale = semitone offsets (minor/major/dorian etc.), patterns are step arrays of scale indices or null
|
|
const MOODS = {
|
|
title: { root: 45, bpm: 58, scale: [0,2,3,5,7,8,10],
|
|
bass: [0,null,null,null,null,null,null,null,-2,null,null,null,null,null,null,null],
|
|
pad: [[0,3,7],[ -2,2,5 ]], padBeats: 8,
|
|
leadProb: .16, leadOct: 2, drums: null, bell: true },
|
|
|
|
town: { root: 48, bpm: 92, scale: [0,2,4,5,7,9,11],
|
|
bass: [0,null,null,4,null,null,2,null,5,null,null,4,null,null,1,null],
|
|
pad: [[0,4,7],[3,5,9],[-3,2,4],[0,4,7]], padBeats: 4,
|
|
leadProb: .3, leadOct: 2, drums: null, pluck: true },
|
|
|
|
crypt: { root: 38, bpm: 62, scale: [0,2,3,5,7,8,10],
|
|
bass: [0,null,null,null,null,null,0,null,-2,null,null,null,null,null,-2,null],
|
|
pad: [[0,3,7],[0,3,8],[1,5,8],[0,3,7]], padBeats: 8,
|
|
leadProb: .1, leadOct: 3, drums: null, choir: true },
|
|
|
|
cave: { root: 40, bpm: 76, scale: [0,2,3,5,7,9,10],
|
|
bass: [0,null,null,null,5,null,null,null,3,null,null,null,7,null,null,null],
|
|
pad: [[0,3,7],[5,7,10]], padBeats: 8,
|
|
leadProb: .12, leadOct: 2,
|
|
drums: { kick:[0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1], snare:[], hat:[0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0] },
|
|
drip: true },
|
|
|
|
hell: { root: 33, bpm: 128, scale: [0,1,3,5,6,8,10],
|
|
bass: [0,0,null,0,null,0,1,null,0,0,null,0,3,1,0,null],
|
|
pad: [[0,1,6],[0,1,6]], padBeats: 8,
|
|
leadProb: .2, leadOct: 1,
|
|
drums: { kick:[1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0], snare:[0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0], hat:[1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1] } },
|
|
|
|
boss: { root: 31, bpm: 142, scale: [0,1,3,5,7,8,10],
|
|
bass: [0,0,3,0,0,5,3,1,0,0,3,0,6,5,3,1],
|
|
pad: [[0,1,6],[-1,2,6]], padBeats: 8,
|
|
leadProb: .3, leadOct: 2,
|
|
drums: { kick:[1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0], snare:[0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1], hat:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] } },
|
|
};
|
|
|
|
let pendingMood = null;
|
|
let currentMood = null;
|
|
let seqTimer = null;
|
|
let nextNoteTime = 0;
|
|
let step = 0;
|
|
let barCount = 0;
|
|
|
|
function playMusic(mood) {
|
|
if (!MOODS[mood]) mood = 'crypt';
|
|
pendingMood = mood;
|
|
if (!ready()) return;
|
|
if (currentMood === mood && seqTimer) return;
|
|
stopMusicNow();
|
|
currentMood = mood;
|
|
step = 0; barCount = 0;
|
|
nextNoteTime = ctx.currentTime + 0.08;
|
|
seqTimer = setInterval(seqTick, 60);
|
|
}
|
|
|
|
function stopMusic(fadeSec = 0.8) {
|
|
pendingMood = null;
|
|
if (!ctx || !musicGain) return;
|
|
const t0 = ctx.currentTime;
|
|
musicGain.gain.cancelScheduledValues(t0);
|
|
musicGain.gain.setValueAtTime(musicGain.gain.value, t0);
|
|
musicGain.gain.linearRampToValueAtTime(0.0001, t0 + fadeSec);
|
|
stopMusicNow(fadeSec);
|
|
setTimeout(() => { if (musicGain && !seqTimer) musicGain.gain.value = vol.music; }, fadeSec * 1000 + 60);
|
|
}
|
|
|
|
function stopMusicNow(delaySec = 0) {
|
|
if (seqTimer) { clearInterval(seqTimer); seqTimer = null; }
|
|
currentMood = null;
|
|
}
|
|
|
|
function seqTick() {
|
|
if (!currentMood || !ready()) return;
|
|
const M = MOODS[currentMood];
|
|
const spb = 60 / M.bpm; // seconds per beat (quarter)
|
|
const stepDur = spb / 4; // 16th steps
|
|
while (nextNoteTime < ctx.currentTime + 0.18) {
|
|
scheduleStep(M, step, nextNoteTime, stepDur);
|
|
nextNoteTime += stepDur;
|
|
step = (step + 1) % 16;
|
|
if (step === 0) barCount++;
|
|
}
|
|
}
|
|
|
|
function scheduleStep(M, st, time, stepDur) {
|
|
const scale = M.scale;
|
|
|
|
/* bass line */
|
|
const bIdx = M.bass[st];
|
|
if (bIdx !== null && bIdx !== undefined) {
|
|
const midi = M.root + bIdx + (scale[0] === undefined ? 0 : 0);
|
|
musicTone({ type: 'triangle', freq: mtof(midi), dur: stepDur * 2.4, vol: .17, time,
|
|
filter: { type: 'lowpass', freq: 320 } });
|
|
if (M.drums) musicTone({ type: 'sawtooth', freq: mtof(midi - 12), dur: stepDur * 1.6, vol: .06, time, filter:{type:'lowpass',freq:200} });
|
|
}
|
|
|
|
/* drums */
|
|
if (M.drums) {
|
|
if (M.drums.kick[st]) musicKick(time);
|
|
if (M.drums.snare[st]) musicNoise({ filterType: 'bandpass', freq: 1900, q: .8, dur: .09, vol: .1, time });
|
|
if (M.drums.hat[st]) musicNoise({ filterType: 'highpass', freq: 7500, dur: .03, vol: .045, time });
|
|
}
|
|
|
|
/* pad chord each padBeats */
|
|
if (M.pad && st === 0) {
|
|
const chord = M.pad[Math.floor(barCount / (M.padBeats / 16 || 1)) % M.pad.length] || M.pad[0];
|
|
for (const iv of chord) {
|
|
const midi = M.root + 12 + iv;
|
|
musicPad(midi, M.padBeats * stepDur, time);
|
|
}
|
|
}
|
|
|
|
/* lead / bell melody */
|
|
if (Math.random() < M.leadProb && st % 2 === 0) {
|
|
const deg = scale[Math.floor(Math.random() * scale.length)];
|
|
const midi = M.root + 12 * (M.leadOct || 2) + deg;
|
|
if (M.bell) musicBell(midi, time);
|
|
else if (M.pluck) musicPluck(midi, time);
|
|
else if (M.choir) musicChoirNote(midi, time, stepDur * 6);
|
|
else musicPluck(midi, time);
|
|
}
|
|
|
|
/* cave drip ambience */
|
|
if (M.drip && Math.random() < .05) {
|
|
musicTone({ type: 'sine', freq: 1700 + Math.random() * 900, dur: .1, vol: .035, time });
|
|
}
|
|
}
|
|
|
|
/* music-routed voice wrappers (respect global volume via musicGain) */
|
|
function musicTone(o) {
|
|
const saveDest = o.dest; o.dest = musicGain; o.time = o.time;
|
|
toneAt(o);
|
|
}
|
|
function musicNoise(o) { o.dest = musicGain; noiseAt(o); }
|
|
|
|
/* time-scheduled variants (absolute ctx time) — separate from sfx versions */
|
|
function toneAt(o) {
|
|
if (!ready()) return;
|
|
const t0 = o.time !== undefined ? o.time : ctx.currentTime;
|
|
const osc = ctx.createOscillator();
|
|
osc.type = o.type || 'sine';
|
|
osc.frequency.setValueAtTime(Math.max(20, o.freq), t0);
|
|
if (o.freqEnd) osc.frequency.exponentialRampToValueAtTime(Math.max(20, o.freqEnd), t0 + o.dur);
|
|
const g = ctx.createGain();
|
|
const a = o.attack || 0.01;
|
|
g.gain.setValueAtTime(0.0001, t0);
|
|
g.gain.exponentialRampToValueAtTime(o.vol || .2, t0 + a);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t0 + o.dur);
|
|
let node = g;
|
|
if (o.filter) {
|
|
const f = ctx.createBiquadFilter();
|
|
f.type = o.filter.type; f.frequency.value = o.filter.freq;
|
|
g.connect(f); node = f;
|
|
}
|
|
osc.connect(g); node.connect(o.dest || musicGain);
|
|
osc.start(t0); osc.stop(t0 + o.dur + .05);
|
|
}
|
|
|
|
function noiseAt(o) {
|
|
if (!ready()) return;
|
|
const t0 = o.time !== undefined ? o.time : ctx.currentTime;
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noiseBuf; src.loop = true;
|
|
const f = ctx.createBiquadFilter();
|
|
f.type = o.filterType || 'bandpass';
|
|
f.frequency.setValueAtTime(o.freq || 1000, t0);
|
|
if (o.freqEnd) f.frequency.exponentialRampToValueAtTime(Math.max(30, o.freqEnd), t0 + o.dur);
|
|
f.Q.value = o.q || 1;
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(.0001, t0);
|
|
g.gain.exponentialRampToValueAtTime(o.vol || .2, t0 + (o.attack || .005));
|
|
g.gain.exponentialRampToValueAtTime(.0001, t0 + o.dur);
|
|
src.connect(f); f.connect(g); g.connect(o.dest || musicGain);
|
|
src.start(t0); src.stop(t0 + o.dur + .05);
|
|
}
|
|
|
|
function musicPad(midi, dur, time) {
|
|
[-7, +7].forEach(det => {
|
|
const osc = ctx.createOscillator();
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.value = mtof(midi);
|
|
osc.detune.value = det;
|
|
const f = ctx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 620;
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(.0001, time);
|
|
g.gain.linearRampToValueAtTime(.028, time + dur * .35);
|
|
g.gain.linearRampToValueAtTime(.0001, time + dur);
|
|
osc.connect(f); f.connect(g); g.connect(musicGain);
|
|
osc.start(time); osc.stop(time + dur + .1);
|
|
});
|
|
}
|
|
|
|
function musicBell(midi, time) {
|
|
toneAt({ type: 'sine', freq: mtof(midi), dur: 1.6, vol: .07, time, attack: .003 });
|
|
toneAt({ type: 'sine', freq: mtof(midi) * 2.01, dur: .9, vol: .03, time, attack: .003 });
|
|
}
|
|
|
|
function musicPluck(midi, time) {
|
|
toneAt({ type: 'triangle', freq: mtof(midi), dur: .3, vol: .09, time, attack: .002,
|
|
filter: { type: 'lowpass', freq: 2200 } });
|
|
}
|
|
|
|
function musicChoirNote(midi, time, dur) {
|
|
[-5, +5].forEach(det => toneAt({ type: 'sawtooth', freq: mtof(midi), detune: det, dur, vol: .022, time, attack: dur * .3,
|
|
filter: { type: 'lowpass', freq: 480 } }));
|
|
}
|
|
|
|
function musicKick(time) {
|
|
toneAt({ type: 'sine', freq: 130, freqEnd: 38, dur: .16, vol: .3, time, attack: .002 });
|
|
}
|
|
|
|
D2.audio = {
|
|
unlock, ready,
|
|
setVolumes, get volumes() { return { ...vol }; },
|
|
sfx, playMusic, stopMusic,
|
|
get currentMood() { return currentMood || pendingMood; },
|
|
};
|
|
})(window.D2);
|