import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; const sampleRate = 44100; const outputDir = join('src', 'assets', 'audio', 'sfx'); function envelope(time, start, duration) { const local = time - start; if (local < 0 || local > duration) { return 0; } const attack = 0.018; const release = Math.min(0.24, duration * 0.42); if (local < attack) { return local / attack; } if (local > duration - release) { return Math.max(0, (duration - local) / release); } return 1; } function tone(time, frequency, start, duration, gain, wave = 'sine') { const amp = envelope(time, start, duration) * gain; if (amp <= 0) { return 0; } const phase = Math.PI * 2 * frequency * (time - start); if (wave === 'triangle') { return amp * (2 / Math.PI) * Math.asin(Math.sin(phase)); } if (wave === 'square') { return amp * Math.sign(Math.sin(phase)); } return amp * Math.sin(phase); } function clamp(sample) { return Math.max(-0.92, Math.min(0.92, sample)); } function levelUpSample(time) { const notes = [ { start: 0, frequency: 523.25, gain: 0.22, duration: 0.52 }, { start: 0.09, frequency: 659.25, gain: 0.2, duration: 0.56 }, { start: 0.18, frequency: 783.99, gain: 0.18, duration: 0.62 }, { start: 0.31, frequency: 1046.5, gain: 0.16, duration: 0.7 } ]; const chord = notes.reduce((sum, note) => sum + tone(time, note.frequency, note.start, note.duration, note.gain), 0); const shimmer = tone(time, 1318.51, 0.42, 0.36, 0.055, 'triangle') + tone(time, 1567.98, 0.52, 0.28, 0.042, 'triangle'); return clamp(chord + shimmer); } function bondResonanceSample(time) { const pulse = tone(time, 392, 0, 0.34, 0.1, 'triangle') + tone(time, 587.33, 0.04, 0.42, 0.13, 'triangle') + tone(time, 783.99, 0.1, 0.42, 0.11, 'sine') + tone(time, 1174.66, 0.2, 0.38, 0.065, 'sine'); const sparkle = tone(time, 1567.98, 0.34, 0.24, 0.045, 'triangle') + tone(time, 1760, 0.42, 0.2, 0.035, 'triangle'); const tremolo = 1 + Math.sin(time * Math.PI * 18) * 0.08; return clamp((pulse + sparkle) * tremolo); } function victoryFanfareSample(time) { const brass = tone(time, 392, 0, 0.26, 0.14, 'triangle') + tone(time, 493.88, 0.16, 0.26, 0.14, 'triangle') + tone(time, 587.33, 0.32, 0.28, 0.14, 'triangle') + tone(time, 783.99, 0.5, 0.58, 0.16, 'triangle'); const harmony = tone(time, 196, 0, 0.92, 0.07, 'sine') + tone(time, 293.66, 0.5, 0.64, 0.06, 'sine') + tone(time, 987.77, 0.72, 0.34, 0.055, 'sine'); return clamp(brass + harmony); } function defeatStingSample(time) { const low = tone(time, 174.61, 0, 0.46, 0.16, 'triangle') + tone(time, 146.83, 0.24, 0.52, 0.15, 'triangle') + tone(time, 110, 0.48, 0.56, 0.15, 'sine'); const strike = tone(time, 87.31, 0, 0.18, 0.13, 'square') + tone(time, 261.63, 0.02, 0.16, 0.06, 'triangle'); return clamp(low + strike); } function wavBuffer(samples) { const bytesPerSample = 2; const dataSize = samples.length * bytesPerSample; const buffer = Buffer.alloc(44 + dataSize); buffer.write('RIFF', 0); buffer.writeUInt32LE(36 + dataSize, 4); buffer.write('WAVE', 8); buffer.write('fmt ', 12); buffer.writeUInt32LE(16, 16); buffer.writeUInt16LE(1, 20); buffer.writeUInt16LE(1, 22); buffer.writeUInt32LE(sampleRate, 24); buffer.writeUInt32LE(sampleRate * bytesPerSample, 28); buffer.writeUInt16LE(bytesPerSample, 32); buffer.writeUInt16LE(16, 34); buffer.write('data', 36); buffer.writeUInt32LE(dataSize, 40); samples.forEach((sample, index) => { buffer.writeInt16LE(Math.round(sample * 32767), 44 + index * bytesPerSample); }); return buffer; } const clips = [ { name: 'level-up', duration: 1.12, sampleAt: levelUpSample }, { name: 'bond-resonance', duration: 0.82, sampleAt: bondResonanceSample }, { name: 'victory-fanfare', duration: 1.2, sampleAt: victoryFanfareSample }, { name: 'defeat-sting', duration: 1.08, sampleAt: defeatStingSample } ]; await mkdir(outputDir, { recursive: true }); for (const clip of clips) { const samples = Array.from({ length: Math.floor(sampleRate * clip.duration) }, (_, index) => clip.sampleAt(index / sampleRate)); const outputPath = join(outputDir, `${clip.name}.wav`); await writeFile(outputPath, wavBuffer(samples)); console.log(`Generated ${outputPath}`); }