// src/main/ipc/audio-handlers.ts import { ipcMain } from 'electron' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { ipcSuccess, ipcError, ErrorCode } from '@d3ro/core/errors' import { getAudioCaptureService, calculateRMS, type CaptureLease } from '../services/AudioCaptureService' import { getMainWindow } from '../windows/WindowManager' import { configGet, configSet } from '../services/ConfigService' import type { SetDeviceParams } from '@d3ro/core/types' let testTimer: ReturnType | null = null let testAudioHandler: ((payload: { buffer: Buffer; timestamp: number }) => void) | null = null let testLevelInterval: ReturnType | null = null let testLastRms = 0 /** * 테스트가 실제로 잡은 캡처 참조. 테스트가 돌고 있지 않을 때 stop() 을 부르면 회의·자막·받아쓰기가 * 잡고 있는 참조를 빼앗아 그쪽 마이크가 조용히 끊긴다 — 테스트는 자기 임대만 놓는다. */ let testLease: CaptureLease | null = null function stopAudioTest(): void { const service = getAudioCaptureService() if (testAudioHandler) { service.off('audio-data', testAudioHandler) testAudioHandler = null } if (testLevelInterval) { clearInterval(testLevelInterval) testLevelInterval = null } if (testTimer) { clearTimeout(testTimer) testTimer = null } testLastRms = 0 const lease = testLease testLease = null lease?.release().catch(() => { /* ignore */ }) } export function registerAudioHandlers(): void { ipcMain.handle(IPC_CHANNELS.AUDIO.GET_DEVICES, async () => { try { const devices = await getAudioCaptureService().getDevices() return ipcSuccess(devices) } catch (error) { const message = error instanceof Error ? error.message : String(error) return ipcError(ErrorCode.AudioDeviceNotFound, `Failed to enumerate audio devices: ${message}`) } }) ipcMain.handle(IPC_CHANNELS.AUDIO.GET_SELECTED_DEVICE, async () => { return ipcSuccess(configGet('selectedDeviceId')) }) ipcMain.handle(IPC_CHANNELS.AUDIO.SET_SELECTED_DEVICE, async (_event, params: SetDeviceParams) => { try { configSet('selectedDeviceId', params.deviceId) return ipcSuccess(undefined) } catch (error) { const message = error instanceof Error ? error.message : String(error) return ipcError(ErrorCode.AudioDeviceNotFound, `Failed to set device: ${message}`) } }) ipcMain.handle(IPC_CHANNELS.AUDIO.TEST_DEVICE, async () => { try { // 이전 테스트가 진행 중이면 정리 stopAudioTest() const service = getAudioCaptureService() testLease = await service.acquire() // 오디오 데이터에서 RMS 추적 testAudioHandler = (payload: { buffer: Buffer; timestamp: number }) => { testLastRms = calculateRMS(payload.buffer) } service.on('audio-data', testAudioHandler) // 100ms 간격으로 레벨을 렌더러에 전송. // level 0은 "무음/초기화 중"일 뿐 종료가 아님 — 종료는 done:true로만 알린다. testLevelInterval = setInterval(() => { const level = testLastRms > 0 ? Math.min(1.0, Math.pow(testLastRms, 0.28)) : 0 const win = getMainWindow() if (win && !win.isDestroyed()) { win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level, done: false }) } }, 100) // 5초 후 자동 중지 testTimer = setTimeout(() => { stopAudioTest() const win = getMainWindow() if (win && !win.isDestroyed()) { win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level: 0, done: true }) } }, 5000) return ipcSuccess({ averageLevel: 0, peakLevel: 0, hasAudio: true }) } catch (err) { stopAudioTest() return ipcError( ErrorCode.AudioDeviceNotFound, `Audio test failed: ${err instanceof Error ? err.message : String(err)}`, ) } }) // 테스트 수동 중지 (STOP 버튼) ipcMain.handle(IPC_CHANNELS.AUDIO.STOP_TEST, async () => { stopAudioTest() const win = getMainWindow() if (win && !win.isDestroyed()) { win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level: 0, done: true }) } return ipcSuccess(undefined) }) }