d3ro-voice/apps/desktop/scripts/generate-sounds.js
yunchan8804 45a580878a feat(V2-1a): Monorepo 구조 전환 — apps/desktop으로 V1 이동
- npm workspaces 루트 (apps/*, packages/*) 세팅
- V1 전체를 apps/desktop/으로 git mv (src, resources, tests, sidecar,
  scripts, electron.vite.config.ts, electron-builder.yml, vitest.config.ts,
  tsconfig.node.json, tsconfig.web.json)
- apps/desktop/package.json 신규 (name=@d3ro/desktop)
- productName: 'd3ro-voice' 명시 — app.getName()을 고정하여 userData 경로
  %APPDATA%\d3ro-voice\ 그대로 유지 (기존 DB/설정 연속성 보장)
- 루트 package.json을 workspace 루트로 재구성, 공통 devDep만 유지
  (typescript, eslint, prettier)
- turbo.json, tsconfig.base.json 추가 (Turborepo 자체 설치는 별도 sub-phase)
- memory/project_status.md 생성 (규칙 13)

검증:
- npm run typecheck 통과
- npm run build 통과 (electron-vite main+preload+renderer)
- npm run dev 실제 실행 → DB/핫키/Ollama 자동 실행 모두 정상
2026-04-08 14:04:41 +09:00

147 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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.');