You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
8.5 KiB
351 lines
8.5 KiB
import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
|
// Original procedural foley: materials, rhythm and envelopes differ by action.
|
|
// Versioned URLs keep browsers from reusing the previous, similar-sounding pack.
|
|
const rate = 44100;
|
|
const directory = new URL('../static/sounds/v2/', import.meta.url);
|
|
await mkdir(directory, { recursive: true });
|
|
let seed = 1781,
|
|
low = 0;
|
|
function noise() {
|
|
seed = (1664525 * seed + 1013904223) >>> 0;
|
|
return seed / 2147483648 - 1;
|
|
}
|
|
function filtered() {
|
|
const n = noise();
|
|
low += 0.12 * (n - low);
|
|
return { low, high: n - low };
|
|
}
|
|
const wave = (f, t) => Math.sin(2 * Math.PI * f * t);
|
|
function hit(t, start, frequencies, decay = 60, strength = 1) {
|
|
const age = t - start;
|
|
if (age < 0) return 0;
|
|
const body = frequencies.reduce(
|
|
(sum, f, i) =>
|
|
sum + (wave(f, age) * Math.exp(-age * decay * (1 + i * 0.7))) / (i + 1),
|
|
0,
|
|
);
|
|
return strength * (body * 0.55 + noise() * Math.exp(-age * 700) * 0.45);
|
|
}
|
|
function pluck(t, start, f, damping = 6) {
|
|
const age = t - start;
|
|
if (age < 0) return 0;
|
|
return (
|
|
(wave(f, age) +
|
|
0.35 * wave(f * 2, age) * Math.exp(-age * 8) +
|
|
0.14 * wave(f * 3, age)) *
|
|
Math.exp(-age * damping)
|
|
);
|
|
}
|
|
const cupImpacts = Array.from({ length: 19 }, (_, i) => ({
|
|
start: i * 0.028 + ((i * 7) % 5) * 0.002,
|
|
pitch: 480 + ((i * 317) % 1900),
|
|
gain: 0.35 + (i % 4) * 0.13,
|
|
}));
|
|
const diceImpacts = [
|
|
{ t: 0, f: 180, g: 1 },
|
|
{ t: 0.13, f: 780, g: 0.85 },
|
|
{ t: 0.27, f: 1140, g: 0.7 },
|
|
{ t: 0.42, f: 920, g: 0.55 },
|
|
{ t: 0.59, f: 1340, g: 0.4 },
|
|
{ t: 0.72, f: 1040, g: 0.25 },
|
|
];
|
|
const effects = {
|
|
'chess-move': [
|
|
0.19,
|
|
(t) =>
|
|
hit(t, 0, [165, 390, 940], 42) + filtered().low * Math.exp(-t * 70) * 0.5,
|
|
],
|
|
'chess-capture': [
|
|
0.36,
|
|
(t) =>
|
|
hit(t, 0, [480, 970], 85, 0.65) +
|
|
hit(t, 0.09, [125, 290, 690], 26) +
|
|
filtered().low * Math.exp(-Math.abs(t - 0.12) * 35) * 0.3,
|
|
],
|
|
'checkers-move': [0.12, (t) => hit(t, 0, [1180, 2870, 4160], 85)],
|
|
'checkers-capture': [
|
|
0.43,
|
|
(t) =>
|
|
[0, 0.12, 0.24].reduce(
|
|
(s, at, i) =>
|
|
s + hit(t, at, [1320 - i * 120, 3080 - i * 230], 65, 1 - i * 0.18),
|
|
0,
|
|
),
|
|
],
|
|
'go-place': [
|
|
0.18,
|
|
(t) =>
|
|
hit(t, 0, [2850, 4790, 7100], 58, 0.75) +
|
|
hit(t, 0.013, [690, 1530], 120, 0.25),
|
|
],
|
|
'go-capture': [
|
|
0.31,
|
|
(t) =>
|
|
[0, 0.034, 0.071, 0.12, 0.16].reduce(
|
|
(s, at, i) =>
|
|
s + hit(t, at, [2530 + i * 277, 4360 + i * 140], 95, 0.65),
|
|
0,
|
|
),
|
|
],
|
|
'parchis-move': [
|
|
0.18,
|
|
(t) => hit(t, 0, [720, 1640], 78) + hit(t, 0.03, [330, 1390], 90, 0.4),
|
|
],
|
|
'parchis-capture': [
|
|
0.42,
|
|
(t) =>
|
|
filtered().high * Math.sin(Math.PI * Math.min(1, t / 0.28)) ** 2 * 0.4 +
|
|
hit(t, 0.25, [370, 1280], 50),
|
|
],
|
|
'oca-move': [
|
|
0.41,
|
|
(t) =>
|
|
[0, 0.09, 0.18, 0.27].reduce(
|
|
(s, at, i) => s + hit(t, at, [290 + i * 80, 850 + i * 150], 60, 0.6),
|
|
0,
|
|
),
|
|
],
|
|
'oca-bonus': [
|
|
0.72,
|
|
(t) =>
|
|
[0, 0.26].reduce((sum, start) => {
|
|
const a = t - start;
|
|
if (a < 0 || a > 0.23) return sum;
|
|
const env = Math.sin((Math.PI * a) / 0.23) ** 2;
|
|
return (
|
|
sum +
|
|
env *
|
|
(wave(195 + 40 * a, a) * 0.32 +
|
|
wave(850 + 400 * a, a) * 0.2 +
|
|
wave(1420, a) * 0.1 +
|
|
filtered().low * 0.4)
|
|
);
|
|
}, 0),
|
|
],
|
|
'oca-penalty': [
|
|
0.8,
|
|
(t) =>
|
|
(wave(220 * Math.exp(-t * 2.4), t) * 0.6 +
|
|
wave(440 * Math.exp(-t * 2.4), t) * 0.2) *
|
|
Math.exp(-t * 5),
|
|
],
|
|
'cup-shake': [
|
|
0.55,
|
|
(t) =>
|
|
cupImpacts.reduce(
|
|
(s, impact) =>
|
|
s +
|
|
hit(
|
|
t,
|
|
impact.start,
|
|
[impact.pitch, impact.pitch * 1.64],
|
|
145,
|
|
impact.gain,
|
|
),
|
|
0,
|
|
),
|
|
],
|
|
'dice-roll': [
|
|
0.94,
|
|
(t) =>
|
|
diceImpacts.reduce(
|
|
(s, impact) =>
|
|
s +
|
|
hit(
|
|
t,
|
|
impact.t,
|
|
[impact.f, impact.f * 2.17, impact.f * 3.6],
|
|
impact.t ? 70 : 24,
|
|
impact.g,
|
|
),
|
|
0,
|
|
),
|
|
],
|
|
'card-play': [
|
|
0.29,
|
|
(t) => {
|
|
const n = filtered();
|
|
return (
|
|
n.high * Math.sin(Math.PI * Math.min(1, t / 0.24)) ** 1.5 * 0.6 +
|
|
hit(t, 0.205, [210, 950], 140, 0.35)
|
|
);
|
|
},
|
|
],
|
|
'card-collect': [
|
|
0.65,
|
|
(t) => {
|
|
const n = filtered();
|
|
return (
|
|
n.high *
|
|
(Math.exp(-(((t - 0.12) / 0.05) ** 2)) * 0.45 +
|
|
Math.exp(-(((t - 0.36) / 0.1) ** 2)) * 0.6) +
|
|
hit(t, 0.5, [190, 650], 100, 0.3)
|
|
);
|
|
},
|
|
],
|
|
'tute-card': [
|
|
0.22,
|
|
(t) =>
|
|
filtered().high * Math.exp(-t * 17) * Math.min(1, t / 0.012) * 0.55 +
|
|
hit(t, 0.025, [330, 1800], 140, 0.3),
|
|
],
|
|
'brisca-card': [
|
|
0.38,
|
|
(t) =>
|
|
filtered().high * Math.sin((Math.PI * t) / 0.38) ** 2 * 0.55 +
|
|
hit(t, 0.26, [140, 720], 90, 0.5),
|
|
],
|
|
'coin-buy': [
|
|
1.1,
|
|
(t) =>
|
|
[1170, 2635, 4190, 6030].reduce(
|
|
(s, f, i) => s + (wave(f, t) * Math.exp(-t * (5 + i * 3))) / (i + 1),
|
|
0,
|
|
),
|
|
],
|
|
'coin-payment': [
|
|
0.8,
|
|
(t) =>
|
|
[0, 0.16, 0.28].reduce(
|
|
(s, at, i) => s + hit(t, at, [1380 + i * 190, 3230 + i * 117], 18, 0.6),
|
|
0,
|
|
),
|
|
],
|
|
build: [
|
|
0.46,
|
|
(t) => hit(t, 0, [105, 290], 32) + hit(t, 0.18, [290, 780], 55, 0.7),
|
|
],
|
|
'mystery-suggest': [
|
|
0.82,
|
|
(t) => {
|
|
const n = filtered();
|
|
return (
|
|
(wave(146.83, t) * 0.28 + wave(155.56, t) * 0.22 + n.low * 0.22) *
|
|
Math.sin((Math.PI * t) / 0.82) ** 2
|
|
);
|
|
},
|
|
],
|
|
'mystery-accuse': [
|
|
1.0,
|
|
(t) =>
|
|
pluck(t, 0, 196, 5) * 0.35 +
|
|
pluck(t, 0.12, 233.08, 5) * 0.3 +
|
|
pluck(t, 0.28, 277.18, 6) * 0.3,
|
|
],
|
|
'risk-reinforce': [
|
|
0.24,
|
|
(t) => hit(t, 0, [125, 650], 55) + hit(t, 0.065, [270, 1200], 100, 0.5),
|
|
],
|
|
'risk-attack': [
|
|
0.78,
|
|
(t) =>
|
|
[0, 0.2, 0.42].reduce(
|
|
(s, at, i) =>
|
|
s +
|
|
hit(
|
|
t,
|
|
at,
|
|
[72 + i * 17, 151 + i * 37, 630],
|
|
15 + i * 8,
|
|
0.9 - i * 0.12,
|
|
),
|
|
0,
|
|
) +
|
|
filtered().low * Math.exp(-t * 5) * 0.45,
|
|
],
|
|
check: [
|
|
0.66,
|
|
(t) => pluck(t, 0, 880, 12) * 0.55 + pluck(t, 0.28, 1174.66, 12) * 0.5,
|
|
],
|
|
promotion: [
|
|
1.15,
|
|
(t) =>
|
|
[523.25, 659.25, 783.99, 1046.5].reduce(
|
|
(s, f, i) => s + pluck(t, i * 0.09, f, 5) * 0.25,
|
|
0,
|
|
),
|
|
],
|
|
goal: [
|
|
1.2,
|
|
(t) =>
|
|
[659.25, 783.99, 987.77, 1318.51].reduce(
|
|
(s, f, i) => s + pluck(t, i * 0.12, f, 5) * 0.3,
|
|
0,
|
|
),
|
|
],
|
|
turn: [
|
|
0.16,
|
|
(t) =>
|
|
filtered().low * Math.exp(-t * 45) * 0.45 +
|
|
hit(t, 0, [240, 690], 130, 0.3),
|
|
],
|
|
start: [
|
|
1.05,
|
|
(t) =>
|
|
[329.63, 440, 554.37].reduce(
|
|
(s, f, i) => s + pluck(t, 0.055 * i, f, 5) * 0.3,
|
|
0,
|
|
),
|
|
],
|
|
win: [
|
|
2.1,
|
|
(t) =>
|
|
[392, 523.25, 659.25, 783.99].reduce(
|
|
(s, f, i) => s + pluck(t, i * 0.18, f, 3.5) * 0.32,
|
|
0,
|
|
) +
|
|
[523.25, 659.25, 783.99].reduce(
|
|
(s, f) => s + pluck(t, 0.8, f, 4) * 0.17,
|
|
0,
|
|
),
|
|
],
|
|
loss: [
|
|
1.1,
|
|
(t) =>
|
|
[293.66, 349.23, 440].reduce(
|
|
(s, f, i) => s + pluck(t, i * 0.1, f, 5) * 0.25,
|
|
0,
|
|
),
|
|
],
|
|
draw: [
|
|
0.9,
|
|
(t) => pluck(t, 0, 440, 6) * 0.35 + pluck(t, 0.25, 523.25, 7) * 0.3,
|
|
],
|
|
};
|
|
for (const [name, [duration, sample]] of Object.entries(effects)) {
|
|
seed = 1781;
|
|
low = 0;
|
|
const count = Math.ceil(rate * duration),
|
|
samples = new Float64Array(count);
|
|
let peak = 0;
|
|
for (let i = 0; i < count; i++) {
|
|
const t = i / rate,
|
|
fade = Math.max(0, Math.min(1, t / 0.0015, (duration - t) / 0.025));
|
|
samples[i] = sample(t) * fade;
|
|
peak = Math.max(peak, Math.abs(samples[i]));
|
|
}
|
|
const buffer = Buffer.alloc(44 + count * 2);
|
|
buffer.write('RIFF');
|
|
buffer.writeUInt32LE(buffer.length - 8, 4);
|
|
buffer.write('WAVEfmt ', 8);
|
|
buffer.writeUInt32LE(16, 16);
|
|
buffer.writeUInt16LE(1, 20);
|
|
buffer.writeUInt16LE(1, 22);
|
|
buffer.writeUInt32LE(rate, 24);
|
|
buffer.writeUInt32LE(rate * 2, 28);
|
|
buffer.writeUInt16LE(2, 32);
|
|
buffer.writeUInt16LE(16, 34);
|
|
buffer.write('data', 36);
|
|
buffer.writeUInt32LE(count * 2, 40);
|
|
for (let i = 0; i < count; i++)
|
|
buffer.writeInt16LE(
|
|
Math.round((samples[i] / Math.max(peak, 0.01)) * 25000),
|
|
44 + i * 2,
|
|
);
|
|
await writeFile(new URL(`${name}.wav`, directory), buffer);
|
|
}
|
|
console.log(
|
|
`${Object.keys(effects).length} efectos originales con materiales y ritmos diferentes, a 44,1 kHz.`,
|
|
);
|