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