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 () => {
|
ipcMain.handle(IPC_CHANNELS.AUDIO.TEST_DEVICE, async () => {
|
||||||
// Phase 1: 스텁
|
try {
|
||||||
return ipcSuccess({ averageLevel: 0, peakLevel: 0, hasAudio: false })
|
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')
|
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한다.
|
* 수신된 오디오 청크를 60ms 프레임(1920 bytes) 단위로 분할하여 emit한다.
|
||||||
* 잔여 바이트는 다음 청크와 결합한다.
|
* 잔여 바이트는 다음 청크와 결합한다.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue