fix: 오디오 테스트 버튼 구현 — 스텁 제거, 실제 SoX 캡처
2초간 마이크 캡처 후 평균/피크 RMS 레벨 반환. 설정 오디오 탭에서 테스트 버튼 정상 작동.
This commit is contained in:
parent
2a465f22e9
commit
fa73ad3bb2
2 changed files with 65 additions and 2 deletions
|
|
@ -31,7 +31,15 @@ export function registerAudioHandlers(): void {
|
|||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.AUDIO.TEST_DEVICE, async () => {
|
||||
// Phase 1: 스텁
|
||||
return ipcSuccess({ averageLevel: 0, peakLevel: 0, hasAudio: false })
|
||||
try {
|
||||
const service = getAudioCaptureService()
|
||||
const result = await service.testCapture(2000)
|
||||
return ipcSuccess(result)
|
||||
} catch (err) {
|
||||
return ipcError(
|
||||
ErrorCode.AudioDeviceNotFound,
|
||||
`Audio test failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -278,6 +278,61 @@ class AudioCaptureService extends EventEmitter {
|
|||
logger.info('AudioCaptureService disposed')
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정 시간(ms) 동안 마이크 캡처 후 평균/피크 레벨을 반환한다.
|
||||
*/
|
||||
async testCapture(durationMs: number): Promise<{ averageLevel: number; peakLevel: number; hasAudio: boolean }> {
|
||||
const soxExe = getSoxPath()
|
||||
const selectedDeviceId = configGet('selectedDeviceId')
|
||||
const deviceArg = selectedDeviceId && selectedDeviceId !== 'default'
|
||||
? selectedDeviceId
|
||||
: 'default'
|
||||
|
||||
const soxArgs = process.platform === 'win32'
|
||||
? ['-t', 'waveaudio', deviceArg, '--no-show-progress',
|
||||
'-r', String(AUDIO_FORMAT.SAMPLE_RATE), '-c', String(AUDIO_FORMAT.CHANNELS),
|
||||
'-b', '16', '-e', 'signed-integer', '-t', 'raw', '-']
|
||||
: ['-d', '--no-show-progress',
|
||||
'-r', String(AUDIO_FORMAT.SAMPLE_RATE), '-c', String(AUDIO_FORMAT.CHANNELS),
|
||||
'-b', '16', '-e', 'signed-integer', '-t', 'raw', '-']
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(soxExe, soxArgs, { stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
const buffers: Buffer[] = []
|
||||
let peakRms = 0
|
||||
|
||||
proc.stdout?.on('data', (chunk: Buffer) => {
|
||||
buffers.push(chunk)
|
||||
const rms = calculateRMS(chunk)
|
||||
if (rms > peakRms) peakRms = rms
|
||||
})
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(new D3ROError(ErrorCode.AudioCaptureStartFailed, `SoX 실행 실패: ${err.message}`))
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
proc.kill()
|
||||
}, durationMs)
|
||||
|
||||
proc.on('close', () => {
|
||||
if (buffers.length === 0) {
|
||||
resolve({ averageLevel: 0, peakLevel: 0, hasAudio: false })
|
||||
return
|
||||
}
|
||||
const combined = Buffer.concat(buffers)
|
||||
const avgRms = calculateRMS(combined)
|
||||
const avgLevel = avgRms > 0 ? Math.min(1.0, Math.pow(avgRms, 0.28)) : 0
|
||||
const peakLevel = peakRms > 0 ? Math.min(1.0, Math.pow(peakRms, 0.28)) : 0
|
||||
resolve({
|
||||
averageLevel: avgLevel,
|
||||
peakLevel: peakLevel,
|
||||
hasAudio: avgRms > 0.001,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 수신된 오디오 청크를 60ms 프레임(1920 bytes) 단위로 분할하여 emit한다.
|
||||
* 잔여 바이트는 다음 청크와 결합한다.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue