Diablo2D — Shadows of Tristram: complete browser ARPG
- 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
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
/* ============================================================
|
||||
* 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);
|
||||
Reference in New Issue
Block a user