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

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.`,
);

Powered by TurnKey Linux.