fix(desktop): AudioCaptureService.getDevices Mac 분기 — system_profiler CoreAudio 열거 (U7, 빅뱅 Phase 5 Part 6)
getDevices()가 powershell -NoProfile을 플랫폼 불문 호출해 Mac에서 "Failed to enumerate audio devices via PowerShell" 경고 + 1 device fallback이 발생하던 문제. process.platform 분기로 _getDevicesWindows / _getDevicesMac helper로 분리하고, Mac은 system_profiler SPAudioDataType -json 을 파싱해 coreaudio_device_input > 0 인 항목만 input 디바이스로 추출. macOS SoX 빌드는 실제 캡처 경로에서 -d(default) 고정이므로 deviceId는 'default'로 통일하고 label만 사용자 구분용으로 노출한다. Linux는 default 1개만 반환(pulseaudio/alsa 열거는 sox -d 로 충분). Electron 재기동 후 로그: "Found 2 audio device(s) on darwin" 확인. V2-5 플랫폼 감사 누락 항목 해소.
This commit is contained in:
parent
412a2e71f9
commit
dd591d06e7
2 changed files with 118 additions and 28 deletions
|
|
@ -222,7 +222,7 @@ class AudioCaptureService extends EventEmitter {
|
|||
}
|
||||
|
||||
async getDevices(): Promise<AudioDevice[]> {
|
||||
// 캐싱: PowerShell 호출이 느리므로 한번만 실행
|
||||
// 캐싱: 네이티브 프로세스 호출이 느리므로 한번만 실행
|
||||
if (this._cachedDevices) return this._cachedDevices
|
||||
|
||||
const devices: AudioDevice[] = [
|
||||
|
|
@ -230,9 +230,28 @@ class AudioCaptureService extends EventEmitter {
|
|||
]
|
||||
|
||||
try {
|
||||
// PowerShell로 Windows 오디오 입력(마이크) 엔드포인트 열거
|
||||
// Get-PnpDevice -Class AudioEndpoint: 실제 오디오 엔드포인트 (마이크/스피커)
|
||||
// MediaCategory가 'Capture' 또는 FriendlyName에 마이크 관련 키워드 포함
|
||||
if (process.platform === 'win32') {
|
||||
devices.push(...(await this._getDevicesWindows()))
|
||||
} else if (process.platform === 'darwin') {
|
||||
devices.push(...(await this._getDevicesMac()))
|
||||
}
|
||||
// linux: default 1개만 (pulseaudio/alsa 열거는 sox fallback -d 로 충분)
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Failed to enumerate audio devices: ${err instanceof Error ? err.message : String(err)}`,
|
||||
)
|
||||
}
|
||||
|
||||
this._cachedDevices = devices
|
||||
logger.debug(`Found ${devices.length} audio device(s) on ${process.platform}`)
|
||||
return devices
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows: PowerShell로 AudioEndpoint 열거.
|
||||
* SoX -t waveaudio는 이름으로 매칭하므로 FriendlyName을 deviceId로 사용.
|
||||
*/
|
||||
private async _getDevicesWindows(): Promise<AudioDevice[]> {
|
||||
const { execSync } = await import('child_process')
|
||||
const psCommand = `[Console]::OutputEncoding = [Text.Encoding]::UTF8; Get-PnpDevice -Class AudioEndpoint -Status OK | Select-Object InstanceId, FriendlyName | ConvertTo-Json -Compress`
|
||||
const output = execSync(`powershell -NoProfile -Command "${psCommand}"`, {
|
||||
|
|
@ -241,28 +260,85 @@ class AudioCaptureService extends EventEmitter {
|
|||
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
||||
}).trim()
|
||||
|
||||
if (output) {
|
||||
if (!output) return []
|
||||
|
||||
const parsed: unknown = JSON.parse(output.startsWith('[') ? output : `[${output}]`)
|
||||
if (Array.isArray(parsed)) {
|
||||
if (!Array.isArray(parsed)) return []
|
||||
|
||||
const result: AudioDevice[] = []
|
||||
for (const dev of parsed) {
|
||||
const d = dev as { InstanceId?: string; FriendlyName?: string }
|
||||
if (d.InstanceId && d.FriendlyName) {
|
||||
devices.push({
|
||||
deviceId: d.FriendlyName, // SoX -t waveaudio는 이름으로 매칭
|
||||
result.push({
|
||||
deviceId: d.FriendlyName,
|
||||
label: d.FriendlyName,
|
||||
isDefault: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to enumerate audio devices via PowerShell: ${err instanceof Error ? err.message : String(err)}`)
|
||||
return result
|
||||
}
|
||||
|
||||
this._cachedDevices = devices
|
||||
logger.debug(`Found ${devices.length} audio device(s)`)
|
||||
return devices
|
||||
/**
|
||||
* macOS: system_profiler SPAudioDataType -json 으로 CoreAudio 디바이스 열거.
|
||||
* input 전용 디바이스만 필터 (coreaudio_device_input > 0).
|
||||
* macOS SoX 빌드는 실제 디바이스 선택에 -d(default)만 지원하는 경우가 대부분이므로
|
||||
* label은 UI 표시 용도, deviceId는 'default'로 통일해 실제 캡처 경로를 단순화한다.
|
||||
*/
|
||||
private async _getDevicesMac(): Promise<AudioDevice[]> {
|
||||
const { execFile } = await import('child_process')
|
||||
const output: string = await new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
'/usr/sbin/system_profiler',
|
||||
['SPAudioDataType', '-json'],
|
||||
{ encoding: 'utf8', timeout: 5000, maxBuffer: 1024 * 1024 },
|
||||
(err, stdout) => {
|
||||
if (err) reject(err)
|
||||
else resolve(stdout)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
if (!output) return []
|
||||
|
||||
interface MacAudioItem {
|
||||
_name?: string
|
||||
coreaudio_device_input?: number
|
||||
coreaudio_default_audio_input_device?: string
|
||||
coreaudio_input_source?: string
|
||||
}
|
||||
interface MacAudioGroup {
|
||||
_items?: MacAudioItem[]
|
||||
}
|
||||
interface MacAudioJson {
|
||||
SPAudioDataType?: MacAudioGroup[]
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(output) as MacAudioJson
|
||||
const groups = parsed.SPAudioDataType ?? []
|
||||
const result: AudioDevice[] = []
|
||||
|
||||
for (const group of groups) {
|
||||
const items = group._items ?? []
|
||||
for (const item of items) {
|
||||
const hasInput =
|
||||
typeof item.coreaudio_device_input === 'number' && item.coreaudio_device_input > 0
|
||||
const name = item._name ?? item.coreaudio_input_source
|
||||
if (!hasInput || !name) continue
|
||||
|
||||
// 중복 방지 (name 기준)
|
||||
if (result.some((d) => d.label === name)) continue
|
||||
|
||||
result.push({
|
||||
// macOS SoX record 경로는 -d(default) 고정이므로 선택 시 default로 매핑.
|
||||
// 라벨만 사용자 구분용으로 노출.
|
||||
deviceId: 'default',
|
||||
label: name,
|
||||
isDefault: item.coreaudio_default_audio_input_device === 'spaudio_yes',
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getCurrentDevice(): AudioDevice | null {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,20 @@
|
|||
> 마지막 갱신: 2026-04-11 (빅뱅 Phase 5 Part 6 — Voice Conversation 몰입 패널 구현)
|
||||
> 규칙 13: 작업 완료 즉시 이 파일 갱신 의무
|
||||
|
||||
## 빅뱅 Phase 5 Part 6+ (2026-04-11) — U7 AudioCaptureService Mac 분기 ✅
|
||||
|
||||
`AudioCaptureService.getDevices()`가 `powershell -NoProfile`만 호출하던 것을
|
||||
`process.platform` 분기로 교체. Mac에서 `system_profiler SPAudioDataType -json`으로
|
||||
CoreAudio input 디바이스 열거(`coreaudio_device_input > 0` 필터). macOS SoX는
|
||||
실제 캡처 경로에서 `-d`(default) 고정이므로 `deviceId='default'`, `label`만 사용자
|
||||
구분용으로 노출. 기존 "Failed to enumerate audio devices via PowerShell" 경고
|
||||
+ 1 device fallback 해소.
|
||||
|
||||
**변경 파일 (1)**: `apps/desktop/src/main/services/AudioCaptureService.ts` —
|
||||
`_getDevicesWindows` / `_getDevicesMac` helper 추출 + 분기 디스패처.
|
||||
|
||||
**검증**: Electron 재기동 후 로그 `Found 2 audio device(s) on darwin` 확인 (경고 없음).
|
||||
|
||||
## 빅뱅 Phase 5 Part 6 (2026-04-11) — Voice Conversation UX 몰입 패널 ✅
|
||||
|
||||
Part 5-C 설계를 구현으로 완결. 사용자가 listening 상태에서 풀 몰입 계측기 모드 + 사운드 피드백 + 상태별 UI 분기를 실제로 경험할 수 있는 상태.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue