fix: 오디오 테스트를 실시간 레벨 미터로 개선
단발성 평균값 → 100ms 간격 실시간 RMS 스트리밍. AudioCaptureService 직접 시작/5초 자동 중지. 테스트 버튼 누르면 레벨 바가 왔다갔다 반응.
This commit is contained in:
parent
7a925bdeb9
commit
6197ceb132
3 changed files with 70 additions and 15 deletions
|
|
@ -3,10 +3,37 @@
|
||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import { IPC_CHANNELS } from '@shared/ipc-channels'
|
import { IPC_CHANNELS } from '@shared/ipc-channels'
|
||||||
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
|
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
|
||||||
import { getAudioCaptureService } from '../services/AudioCaptureService'
|
import { getAudioCaptureService, calculateRMS } from '../services/AudioCaptureService'
|
||||||
|
import { getMainWindow } from '../windows/WindowManager'
|
||||||
import { configGet, configSet } from '../services/ConfigService'
|
import { configGet, configSet } from '../services/ConfigService'
|
||||||
|
import { getLogger } from '../services/LoggerService'
|
||||||
import type { SetDeviceParams } from '@shared/types'
|
import type { SetDeviceParams } from '@shared/types'
|
||||||
|
|
||||||
|
const logger = getLogger('audio-handlers')
|
||||||
|
|
||||||
|
let testTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let testAudioHandler: ((payload: { buffer: Buffer; timestamp: number }) => void) | null = null
|
||||||
|
let testLevelInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
let testLastRms = 0
|
||||||
|
|
||||||
|
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
|
||||||
|
service.stop().catch(() => { /* ignore */ })
|
||||||
|
}
|
||||||
|
|
||||||
export function registerAudioHandlers(): void {
|
export function registerAudioHandlers(): void {
|
||||||
ipcMain.handle(IPC_CHANNELS.AUDIO.GET_DEVICES, async () => {
|
ipcMain.handle(IPC_CHANNELS.AUDIO.GET_DEVICES, async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -32,10 +59,39 @@ export function registerAudioHandlers(): void {
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.AUDIO.TEST_DEVICE, async () => {
|
ipcMain.handle(IPC_CHANNELS.AUDIO.TEST_DEVICE, async () => {
|
||||||
try {
|
try {
|
||||||
|
// 이전 테스트가 진행 중이면 정리
|
||||||
|
stopAudioTest()
|
||||||
|
|
||||||
const service = getAudioCaptureService()
|
const service = getAudioCaptureService()
|
||||||
const result = await service.testCapture(2000)
|
await service.start()
|
||||||
return ipcSuccess(result)
|
|
||||||
|
// 오디오 데이터에서 RMS 추적
|
||||||
|
testAudioHandler = (payload: { buffer: Buffer; timestamp: number }) => {
|
||||||
|
testLastRms = calculateRMS(payload.buffer)
|
||||||
|
}
|
||||||
|
service.on('audio-data', testAudioHandler)
|
||||||
|
|
||||||
|
// 100ms 간격으로 레벨을 렌더러에 전송
|
||||||
|
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('audio:testLevel', { level })
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
// 5초 후 자동 중지
|
||||||
|
testTimer = setTimeout(() => {
|
||||||
|
stopAudioTest()
|
||||||
|
const win = getMainWindow()
|
||||||
|
if (win && !win.isDestroyed()) {
|
||||||
|
win.webContents.send('audio:testLevel', { level: 0 })
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
return ipcSuccess({ averageLevel: 0, peakLevel: 0, hasAudio: true })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
stopAudioTest()
|
||||||
return ipcError(
|
return ipcError(
|
||||||
ErrorCode.AudioDeviceNotFound,
|
ErrorCode.AudioDeviceNotFound,
|
||||||
`Audio test failed: ${err instanceof Error ? err.message : String(err)}`,
|
`Audio test failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,9 @@ const electronAPI = {
|
||||||
testDevice: (params: TestDeviceParams) =>
|
testDevice: (params: TestDeviceParams) =>
|
||||||
invoke<TestDeviceResult>(IPC_CHANNELS.AUDIO.TEST_DEVICE, params),
|
invoke<TestDeviceResult>(IPC_CHANNELS.AUDIO.TEST_DEVICE, params),
|
||||||
onDeviceChanged: (cb: (e: AudioDeviceChangedEvent) => void): Unsubscribe =>
|
onDeviceChanged: (cb: (e: AudioDeviceChangedEvent) => void): Unsubscribe =>
|
||||||
on(IPC_CHANNELS.AUDIO.DEVICE_CHANGED, cb)
|
on(IPC_CHANNELS.AUDIO.DEVICE_CHANGED, cb),
|
||||||
|
onTestLevel: (cb: (e: { level: number }) => void): Unsubscribe =>
|
||||||
|
on('audio:testLevel', cb),
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Config ─────────────────────────────────────────────
|
// ── Config ─────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -539,18 +539,15 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
||||||
setMicLevel(0)
|
setMicLevel(0)
|
||||||
} else {
|
} else {
|
||||||
setMicTesting(true)
|
setMicTesting(true)
|
||||||
setMicLevel(0.05)
|
|
||||||
const resp = await window.electronAPI.audio.testDevice({ deviceId: 'default' })
|
|
||||||
if (resp.success) {
|
|
||||||
setMicLevel(resp.data.averageLevel)
|
|
||||||
setTimeout(() => {
|
|
||||||
setMicTesting(false)
|
|
||||||
setMicLevel(0)
|
setMicLevel(0)
|
||||||
}, 3000)
|
const unsub = window.electronAPI.audio.onTestLevel((e) => {
|
||||||
} else {
|
setMicLevel(e.level)
|
||||||
|
if (e.level === 0) {
|
||||||
setMicTesting(false)
|
setMicTesting(false)
|
||||||
setMicLevel(0)
|
unsub()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
await window.electronAPI.audio.testDevice({ deviceId: 'default' })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
sx={{ fontFamily: d3roFontMono, fontSize: '11px', minWidth: 80 }}
|
sx={{ fontFamily: d3roFontMono, fontSize: '11px', minWidth: 80 }}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue