feat: Adding audio player + music to play
This commit is contained in:
+99
-44
@@ -1,24 +1,63 @@
|
||||
// Sound effects, synthesised with the Web Audio API: no audio files. Everything is soft
|
||||
// (slow attacks, low levels, filtered noise) so it stays in the background of the desk.
|
||||
// Sound effects, synthesised with the Web Audio API: no audio files. Everything is soft: slow
|
||||
// attacks, long fades, no deep bass, and a short room reverb under it all so sounds melt away
|
||||
// instead of stopping. Each sound belongs to a mixer group the player can turn down on its own.
|
||||
// Browsers keep audio locked until the first click or key press; unlock() resumes it.
|
||||
|
||||
export function createAudio({ enabled = () => true, volume = () => 0.6 } = {}) {
|
||||
// Mixer groups, in the order the mixer shows them.
|
||||
export const MIX_GROUPS = ["stamp", "paper", "page", "sign", "telegram", "alert", "chime", "tick", "countdown", "click", "ending"];
|
||||
|
||||
const GROUP_OF = {
|
||||
stampYes: "stamp", stampNo: "stamp", paper: "paper", page: "page", sign: "sign", telegram: "telegram",
|
||||
alert: "alert", chime: "chime", tick: "tick", tock: "tick", clockTick: "countdown", clockTock: "countdown",
|
||||
click: "click", fall: "ending", fanfare: "ending",
|
||||
};
|
||||
|
||||
export function createAudio({ enabled = () => true, volume = () => 0.6, mix = () => ({}) } = {}) {
|
||||
let ctx = null;
|
||||
let master = null;
|
||||
let noiseBuf = null;
|
||||
let out = null; // where the sound being built sends its notes (a gain node per play)
|
||||
|
||||
function context() {
|
||||
if (!ctx) {
|
||||
const AC = globalThis.AudioContext ?? globalThis.webkitAudioContext;
|
||||
if (!AC) return null;
|
||||
try {
|
||||
ctx = new AC();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
master = ctx.createGain();
|
||||
master.connect(ctx.destination);
|
||||
if (ctx) return ctx;
|
||||
const AC = globalThis.AudioContext ?? globalThis.webkitAudioContext;
|
||||
if (!AC) return null;
|
||||
try {
|
||||
ctx = new AC();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
// master -> no deep bass -> no harsh treble -> gentle limiter -> speakers, plus a soft room send
|
||||
master = ctx.createGain();
|
||||
const lowCut = ctx.createBiquadFilter();
|
||||
lowCut.type = "highpass";
|
||||
lowCut.frequency.value = 140;
|
||||
const highCut = ctx.createBiquadFilter();
|
||||
highCut.type = "lowpass";
|
||||
highCut.frequency.value = 4800;
|
||||
highCut.Q.value = 0.4;
|
||||
const limiter = ctx.createDynamicsCompressor();
|
||||
limiter.threshold.value = -22;
|
||||
limiter.knee.value = 24;
|
||||
limiter.ratio.value = 6;
|
||||
limiter.attack.value = 0.01;
|
||||
limiter.release.value = 0.35;
|
||||
master.connect(lowCut).connect(highCut).connect(limiter).connect(ctx.destination);
|
||||
|
||||
const room = ctx.createConvolver();
|
||||
const len = Math.floor(ctx.sampleRate * 1.5);
|
||||
const ir = ctx.createBuffer(2, len, ctx.sampleRate);
|
||||
for (let ch = 0; ch < 2; ch++) {
|
||||
const d = ir.getChannelData(ch);
|
||||
for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2.6);
|
||||
}
|
||||
room.buffer = ir;
|
||||
const wet = ctx.createGain();
|
||||
wet.gain.value = 0.28;
|
||||
const roomTone = ctx.createBiquadFilter();
|
||||
roomTone.type = "lowpass";
|
||||
roomTone.frequency.value = 2600;
|
||||
highCut.connect(wet).connect(room).connect(roomTone).connect(limiter);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@@ -28,8 +67,8 @@ export function createAudio({ enabled = () => true, volume = () => 0.6 } = {}) {
|
||||
}
|
||||
for (const ev of ["pointerdown", "keydown"]) document.addEventListener(ev, unlock, { passive: true });
|
||||
|
||||
// One note: a sine (or other) wave with an attack and an exponential fade.
|
||||
function tone(c, at, { freq, to, type = "sine", dur = 0.2, gain = 0.2, attack = 0.008 }) {
|
||||
// One note: a wave with an attack and an exponential fade.
|
||||
function tone(c, at, { freq, to, type = "sine", dur = 0.3, gain = 0.1, attack = 0.02 }) {
|
||||
const o = c.createOscillator();
|
||||
const g = c.createGain();
|
||||
o.type = type;
|
||||
@@ -38,13 +77,13 @@ export function createAudio({ enabled = () => true, volume = () => 0.6 } = {}) {
|
||||
g.gain.setValueAtTime(0.0001, at);
|
||||
g.gain.linearRampToValueAtTime(gain, at + attack);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, at + dur);
|
||||
o.connect(g).connect(master);
|
||||
o.connect(g).connect(out);
|
||||
o.start(at);
|
||||
o.stop(at + dur + 0.05);
|
||||
}
|
||||
|
||||
// Filtered noise: paper, pages, thumps and clicks.
|
||||
function noise(c, at, { dur = 0.15, gain = 0.1, type = "bandpass", freq = 2000, to, q = 0.8, attack = 0.02 }) {
|
||||
// Filtered noise: paper, pages, soft impacts and ticks.
|
||||
function noise(c, at, { dur = 0.2, gain = 0.06, type = "bandpass", freq = 2000, to, q = 0.6, attack = 0.03 }) {
|
||||
noiseBuf ??= (() => {
|
||||
const b = c.createBuffer(1, c.sampleRate, c.sampleRate);
|
||||
const d = b.getChannelData(0);
|
||||
@@ -63,55 +102,66 @@ export function createAudio({ enabled = () => true, volume = () => 0.6 } = {}) {
|
||||
g.gain.setValueAtTime(0.0001, at);
|
||||
g.gain.linearRampToValueAtTime(gain, at + attack);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, at + dur);
|
||||
src.connect(f).connect(g).connect(master);
|
||||
src.connect(f).connect(g).connect(out);
|
||||
src.start(at);
|
||||
src.stop(at + dur + 0.05);
|
||||
}
|
||||
|
||||
const SOUNDS = {
|
||||
// A padded "thup" rather than a thud: mid-range, slow to start, with a soft paper tail.
|
||||
stampYes: (c, t) => {
|
||||
tone(c, t, { freq: 150, to: 55, dur: 0.16, gain: 0.5 });
|
||||
noise(c, t, { type: "lowpass", freq: 500, dur: 0.07, gain: 0.25, attack: 0.004 });
|
||||
noise(c, t, { freq: 2200, dur: 0.04, gain: 0.07, attack: 0.004 });
|
||||
tone(c, t, { freq: 230, to: 130, dur: 0.32, gain: 0.2, attack: 0.016 });
|
||||
noise(c, t, { type: "lowpass", freq: 1100, dur: 0.18, gain: 0.09, attack: 0.014, q: 0.4 });
|
||||
noise(c, t + 0.02, { freq: 2000, to: 1400, dur: 0.32, gain: 0.025, attack: 0.03 });
|
||||
},
|
||||
stampNo: (c, t) => {
|
||||
tone(c, t, { freq: 118, to: 44, dur: 0.18, gain: 0.5 });
|
||||
noise(c, t, { type: "lowpass", freq: 420, dur: 0.08, gain: 0.28, attack: 0.004 });
|
||||
noise(c, t, { freq: 1800, dur: 0.04, gain: 0.07, attack: 0.004 });
|
||||
tone(c, t, { freq: 190, to: 108, dur: 0.34, gain: 0.2, attack: 0.016 });
|
||||
noise(c, t, { type: "lowpass", freq: 900, dur: 0.2, gain: 0.09, attack: 0.014, q: 0.4 });
|
||||
noise(c, t + 0.02, { freq: 1700, to: 1200, dur: 0.32, gain: 0.025, attack: 0.03 });
|
||||
},
|
||||
paper: (c, t) => noise(c, t, { freq: 2600, to: 1200, dur: 0.17, gain: 0.1, attack: 0.04 }),
|
||||
paper: (c, t) => noise(c, t, { freq: 2400, to: 1100, dur: 0.36, gain: 0.075, attack: 0.09 }),
|
||||
page: (c, t) => {
|
||||
noise(c, t, { freq: 500, to: 3200, dur: 0.24, gain: 0.11, attack: 0.05 });
|
||||
noise(c, t + 0.2, { freq: 2400, dur: 0.06, gain: 0.05, attack: 0.01 });
|
||||
noise(c, t, { freq: 450, to: 2800, dur: 0.52, gain: 0.085, attack: 0.13 });
|
||||
noise(c, t + 0.4, { freq: 2200, dur: 0.18, gain: 0.035, attack: 0.03 });
|
||||
},
|
||||
sign: (c, t) => {
|
||||
noise(c, t, { freq: 3400, to: 2600, dur: 0.2, gain: 0.05, q: 2, attack: 0.05 });
|
||||
tone(c, t + 0.22, { freq: 140, to: 60, dur: 0.14, gain: 0.3 });
|
||||
noise(c, t, { freq: 3200, to: 2400, dur: 0.42, gain: 0.04, q: 2, attack: 0.11 });
|
||||
tone(c, t + 0.44, { freq: 200, to: 125, dur: 0.28, gain: 0.15, attack: 0.02 });
|
||||
noise(c, t + 0.44, { type: "lowpass", freq: 1000, dur: 0.14, gain: 0.06, attack: 0.012, q: 0.4 });
|
||||
},
|
||||
telegram: (c, t) => {
|
||||
for (let i = 0; i < 4; i++) noise(c, t + i * 0.07, { type: "highpass", freq: 3200, dur: 0.02, gain: 0.1, attack: 0.002 });
|
||||
tone(c, t + 0.34, { freq: 1320, dur: 0.7, gain: 0.1, attack: 0.01 });
|
||||
tone(c, t + 0.34, { freq: 1980, dur: 0.45, gain: 0.04, attack: 0.01 });
|
||||
for (let i = 0; i < 4; i++) noise(c, t + i * 0.09, { type: "highpass", freq: 3200, dur: 0.045, gain: 0.05, attack: 0.007 });
|
||||
tone(c, t + 0.44, { freq: 1320, dur: 1.6, gain: 0.07, attack: 0.04 });
|
||||
tone(c, t + 0.44, { freq: 1980, dur: 1.1, gain: 0.022, attack: 0.04 });
|
||||
},
|
||||
alert: (c, t) => {
|
||||
tone(c, t, { freq: 233, dur: 0.4, gain: 0.2, type: "triangle", attack: 0.03 });
|
||||
tone(c, t + 0.24, { freq: 175, dur: 0.55, gain: 0.2, type: "triangle", attack: 0.03 });
|
||||
tone(c, t, { freq: 233, dur: 0.95, gain: 0.12, type: "triangle", attack: 0.16 });
|
||||
tone(c, t + 0.32, { freq: 175, dur: 1.25, gain: 0.12, type: "triangle", attack: 0.16 });
|
||||
},
|
||||
chime: (c, t) => {
|
||||
tone(c, t, { freq: 659, dur: 0.7, gain: 0.12, attack: 0.02 });
|
||||
tone(c, t + 0.16, { freq: 880, dur: 0.9, gain: 0.1, attack: 0.02 });
|
||||
tone(c, t, { freq: 659, dur: 1.4, gain: 0.08, attack: 0.06 });
|
||||
tone(c, t + 0.22, { freq: 880, dur: 1.8, gain: 0.065, attack: 0.06 });
|
||||
},
|
||||
tick: (c, t) => tone(c, t, { freq: 1000, to: 850, dur: 0.07, gain: 0.02, attack: 0.007 }),
|
||||
tock: (c, t) => tone(c, t, { freq: 760, to: 650, dur: 0.07, gain: 0.02, attack: 0.007 }),
|
||||
// The countdown on the day before a new proposal: a small wooden clock.
|
||||
clockTick: (c, t) => {
|
||||
tone(c, t, { freq: 1500, to: 1150, dur: 0.1, gain: 0.05, attack: 0.005 });
|
||||
noise(c, t, { freq: 2800, q: 4, dur: 0.06, gain: 0.025, attack: 0.005 });
|
||||
},
|
||||
clockTock: (c, t) => {
|
||||
tone(c, t, { freq: 1150, to: 880, dur: 0.1, gain: 0.05, attack: 0.005 });
|
||||
noise(c, t, { freq: 2200, q: 4, dur: 0.06, gain: 0.025, attack: 0.005 });
|
||||
},
|
||||
tick: (c, t) => tone(c, t, { freq: 1100, to: 900, dur: 0.035, gain: 0.03, attack: 0.002 }),
|
||||
tock: (c, t) => tone(c, t, { freq: 820, to: 700, dur: 0.035, gain: 0.03, attack: 0.002 }),
|
||||
click: (c, t) => {
|
||||
tone(c, t, { freq: 900, to: 600, dur: 0.04, gain: 0.06, attack: 0.002 });
|
||||
noise(c, t, { type: "highpass", freq: 3000, dur: 0.015, gain: 0.04, attack: 0.002 });
|
||||
tone(c, t, { freq: 800, to: 600, dur: 0.09, gain: 0.045, attack: 0.007 });
|
||||
noise(c, t, { type: "highpass", freq: 3000, dur: 0.035, gain: 0.02, attack: 0.005 });
|
||||
},
|
||||
fall: (c, t) => {
|
||||
[330, 247, 196, 147].forEach((f, i) => tone(c, t + i * 0.45, { freq: f, dur: 0.9, gain: 0.14, attack: 0.04, type: "triangle" }));
|
||||
[262, 220, 196, 165].forEach((f, i) => tone(c, t + i * 0.6, { freq: f, dur: 1.9, gain: 0.09, attack: 0.16 }));
|
||||
},
|
||||
fanfare: (c, t) => {
|
||||
[392, 494, 587, 784].forEach((f, i) => tone(c, t + i * 0.14, { freq: f, dur: 1.1, gain: 0.09, attack: 0.03 }));
|
||||
[392, 494, 587, 784].forEach((f, i) => tone(c, t + i * 0.2, { freq: f, dur: 1.9, gain: 0.07, attack: 0.07 }));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -124,7 +174,12 @@ export function createAudio({ enabled = () => true, volume = () => 0.6 } = {}) {
|
||||
c.resume?.().catch?.(() => {});
|
||||
return; // still locked: nothing to hear yet
|
||||
}
|
||||
const level = Math.max(0, Math.min(1, mix()[GROUP_OF[name]] ?? 1));
|
||||
if (level < 0.005) return;
|
||||
master.gain.value = Math.max(0, Math.min(1, volume()));
|
||||
out = c.createGain();
|
||||
out.gain.value = level;
|
||||
out.connect(master);
|
||||
try {
|
||||
make(c, c.currentTime + 0.005);
|
||||
} catch {
|
||||
@@ -132,5 +187,5 @@ export function createAudio({ enabled = () => true, volume = () => 0.6 } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
return { play, unlock, names: Object.keys(SOUNDS) };
|
||||
return { play, unlock, names: Object.keys(SOUNDS), groupOf: (name) => GROUP_OF[name] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user