/** * D3RO-VOICE 효과음 WAV 파일 생성 스크립트 * * 생성 파일: * resources/sounds/recording-start.wav — 상승 톤 (440→880Hz, 150ms) * resources/sounds/recording-stop.wav — 하강 톤 (880→440Hz, 150ms) * resources/sounds/error.wav — 저음 비프 2회 (220Hz, 200ms×2) * * WAV 포맷: 16kHz, mono, 16bit PCM */ const fs = require('fs'); const path = require('path'); const SAMPLE_RATE = 16000; const BIT_DEPTH = 16; const NUM_CHANNELS = 1; const BYTES_PER_SAMPLE = BIT_DEPTH / 8; /** * 사인파 샘플 생성 (주파수 선형 스윕 지원) * @param {number} durationMs - 길이 (ms) * @param {number} freqStart - 시작 주파수 (Hz) * @param {number} freqEnd - 끝 주파수 (Hz) * @param {number} volume - 볼륨 (0.0~1.0) * @returns {Int16Array} */ function generateTone(durationMs, freqStart, freqEnd, volume = 0.6) { const numSamples = Math.floor((SAMPLE_RATE * durationMs) / 1000); const samples = new Int16Array(numSamples); const maxVal = 32767 * volume; // 페이드 인/아웃 길이 (클릭 방지) const fadeSamples = Math.min(Math.floor(numSamples * 0.05), 80); let phase = 0; for (let i = 0; i < numSamples; i++) { const t = i / numSamples; const freq = freqStart + (freqEnd - freqStart) * t; // 페이드 인/아웃 엔벨로프 let envelope = 1.0; if (i < fadeSamples) { envelope = i / fadeSamples; } else if (i > numSamples - fadeSamples) { envelope = (numSamples - i) / fadeSamples; } samples[i] = Math.round(Math.sin(phase) * maxVal * envelope); phase += (2 * Math.PI * freq) / SAMPLE_RATE; } return samples; } /** * 무음 생성 * @param {number} durationMs * @returns {Int16Array} */ function generateSilence(durationMs) { const numSamples = Math.floor((SAMPLE_RATE * durationMs) / 1000); return new Int16Array(numSamples); } /** * 여러 샘플 배열을 이어붙임 * @param {Int16Array[]} arrays * @returns {Int16Array} */ function concatenate(arrays) { const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); const result = new Int16Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; } /** * PCM 데이터를 WAV 파일 버퍼로 변환 * @param {Int16Array} samples * @returns {Buffer} */ function createWavBuffer(samples) { const dataSize = samples.length * BYTES_PER_SAMPLE; const headerSize = 44; const buffer = Buffer.alloc(headerSize + dataSize); // RIFF header buffer.write('RIFF', 0); buffer.writeUInt32LE(headerSize - 8 + dataSize, 4); buffer.write('WAVE', 8); // fmt chunk buffer.write('fmt ', 12); buffer.writeUInt32LE(16, 16); // chunk size buffer.writeUInt16LE(1, 20); // PCM format buffer.writeUInt16LE(NUM_CHANNELS, 22); buffer.writeUInt32LE(SAMPLE_RATE, 24); buffer.writeUInt32LE(SAMPLE_RATE * NUM_CHANNELS * BYTES_PER_SAMPLE, 28); // byte rate buffer.writeUInt16LE(NUM_CHANNELS * BYTES_PER_SAMPLE, 32); // block align buffer.writeUInt16LE(BIT_DEPTH, 34); // data chunk buffer.write('data', 36); buffer.writeUInt32LE(dataSize, 40); // PCM data (Int16 little-endian) const pcmBuffer = Buffer.from(samples.buffer, samples.byteOffset, samples.byteLength); pcmBuffer.copy(buffer, headerSize); return buffer; } // --- 효과음 생성 --- const outputDir = path.resolve(__dirname, '..', 'resources', 'sounds'); fs.mkdirSync(outputDir, { recursive: true }); // 1. recording-start.wav: 상승 톤 440→880Hz, 150ms const startSamples = generateTone(150, 440, 880, 0.5); const startWav = createWavBuffer(startSamples); const startPath = path.join(outputDir, 'recording-start.wav'); fs.writeFileSync(startPath, startWav); console.log(`Created: ${startPath} (${startWav.length} bytes)`); // 2. recording-stop.wav: 하강 톤 880→440Hz, 150ms const stopSamples = generateTone(150, 880, 440, 0.5); const stopWav = createWavBuffer(stopSamples); const stopPath = path.join(outputDir, 'recording-stop.wav'); fs.writeFileSync(stopPath, stopWav); console.log(`Created: ${stopPath} (${stopWav.length} bytes)`); // 3. error.wav: 220Hz 비프 200ms × 2회, 중간 100ms 무음 const beep1 = generateTone(200, 220, 220, 0.5); const gap = generateSilence(100); const beep2 = generateTone(200, 220, 220, 0.5); const errorSamples = concatenate([beep1, gap, beep2]); const errorWav = createWavBuffer(errorSamples); const errorPath = path.join(outputDir, 'error.wav'); fs.writeFileSync(errorPath, errorWav); console.log(`Created: ${errorPath} (${errorWav.length} bytes)`); console.log('\nDone! All 3 sound files generated.');