Skip to content

Commit 32ccc45

Browse files
committed
feat: add microphone input selection
1 parent 89c3d4e commit 32ccc45

14 files changed

Lines changed: 378 additions & 13 deletions

File tree

electron/main/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# main/
22

3+
## Microphone Selection
4+
5+
The selected microphone is stored by `config-manager.ts` as part of ASR config.
6+
`audio/session-manager.ts` reads that value when a recording session starts and
7+
forwards the device ID to the hidden recording renderer. An empty value keeps the
8+
existing system-default microphone behavior.
9+
310
Electron 主进程目录,负责窗口管理、IPC、录音编排、ASR/润色调用与文本注入。
411

512
## 文件列表

electron/main/audio/README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ Main-process audio pipeline for recording sessions and chunked transcription.
55
## Files
66

77
- `index.ts` - Re-exports the audio module surface.
8-
- `session-manager.ts` - Owns the active recording session lifecycle, `sessionId`, and HUD state transitions, including the initial transcribing step after recording stops. Runs a 10s watchdog after SESSION_STOP that aborts the session if the final chunk never arrives, and exposes `handleBackgroundRendererGone` to fail the active session when the hidden recording renderer crashes or hangs.
8+
- `session-manager.ts` - Owns the active recording session lifecycle, `sessionId`, selected microphone device forwarding, and HUD state transitions, including the initial transcribing step after recording stops. Runs a 10s watchdog after SESSION_STOP that aborts the session if the final chunk never arrives, and exposes `handleBackgroundRendererGone` to fail the active session when the hidden recording renderer crashes or hangs.
99
- `processor.ts` - Accepts audio chunks, writes temp files, converts to MP3 for GLM or 16k mono WAV for local SenseVoice, calls the configured ASR provider, merges chunk text in order, promotes the HUD into the refine step when applicable, logs final line-break metadata, and runs the final refine/history/inject step once. Empty chunk buffers are recorded as empty transcripts (renderer placeholder markers); an empty final transcript completes the session without refine/history/injection. Exposes `hasReceivedFinalChunk`/`abortChunkSession` for the session watchdog.
1010
- `converter.ts` - Initializes FFmpeg and converts captured audio to MP3 or 16k mono WAV, with optional low-volume gain.
1111

1212
## Current Flow
1313

14-
1. The renderer records one session for up to 3 minutes and rotates internal chunks every 29 seconds.
15-
2. The main process tracks chunk work by `sessionId + chunkIndex` and can process chunk ASR requests out of order.
16-
3. Finalization only runs after the final chunk has been seen and every chunk from `0..finalChunkIndex` has produced text.
17-
4. Refinement, line-break-aware final text logging, history writes, and text injection happen once per session after the merged transcript is ready.
18-
5. Any chunk failure or session cancellation aborts the session and discards late results.
14+
1. The main process starts a session and sends the hidden recorder the `sessionId` plus the saved microphone device ID when one is selected.
15+
2. The renderer records one session for up to 3 minutes and rotates internal chunks every 29 seconds.
16+
3. The main process tracks chunk work by `sessionId + chunkIndex` and can process chunk ASR requests out of order.
17+
4. Finalization only runs after the final chunk has been seen and every chunk from `0..finalChunkIndex` has produced text.
18+
5. Refinement, line-break-aware final text logging, history writes, and text injection happen once per session after the merged transcript is ready.
19+
6. Any chunk failure or session cancellation aborts the session and discards late results.

electron/main/audio/session-manager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IPC_CHANNELS, type RecordingStartPayload, type VoiceSession } from '../
22
import { showOverlay, hideOverlay, updateOverlay, showErrorAndHide } from '../window/overlay'
33
import { getBackgroundWindow } from '../window/background'
44
import { t } from '../i18n'
5+
import { configManager } from '../config-manager'
56
import { abortChunkSession, hasReceivedFinalChunk } from './processor'
67

78
// How long after SESSION_STOP the final audio chunk may take to arrive before
@@ -102,7 +103,11 @@ export async function handleStartRecording(): Promise<void> {
102103
return
103104
}
104105

105-
const payload: RecordingStartPayload = { sessionId: currentSession.id }
106+
const asrConfig = configManager.getASRConfig()
107+
const payload: RecordingStartPayload = {
108+
sessionId: currentSession.id,
109+
microphoneDeviceId: asrConfig.microphoneDeviceId || undefined,
110+
}
106111
bgWindow.webContents.send(IPC_CHANNELS.SESSION_START, payload)
107112
const duration = Date.now() - startTimestamp
108113
console.log(`[Audio:Session] Recording start completed in ${duration}ms`)

electron/main/config-manager.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
TranslationConfig,
1111
} from '../shared/types'
1212
import { normalizeRefineBaseUrl } from '../shared/refine-url'
13-
import { DEFAULT_HOTKEYS, LLM_REFINE, TRANSLATION } from '../shared/constants'
13+
import { DEFAULT_HOTKEYS, LLM_REFINE, MICROPHONE_INPUT, TRANSLATION } from '../shared/constants'
1414

1515
const ENCRYPTED_PREFIX = 'enc:'
1616

@@ -69,6 +69,8 @@ const defaultConfig: AppConfig = {
6969
intl: '',
7070
},
7171
lowVolumeMode: true,
72+
microphoneDeviceId: '',
73+
microphoneDeviceLabel: '',
7274
endpoint: '',
7375
language: 'auto',
7476
},
@@ -156,6 +158,10 @@ function normalizeASRProvider(provider: unknown): ASRProviderType {
156158
return provider === 'local-sensevoice' ? 'local-sensevoice' : 'glm'
157159
}
158160

161+
function normalizeConfigString(value: unknown, maxLength: number): string {
162+
return typeof value === 'string' ? value.trim().slice(0, maxLength) : ''
163+
}
164+
159165
export class ConfigManager {
160166
private store: Store<ConfigSchema>
161167

@@ -275,6 +281,14 @@ export class ConfigManager {
275281
...(storedConfig.apiKeys ?? {}),
276282
},
277283
}
284+
config.microphoneDeviceId = normalizeConfigString(
285+
config.microphoneDeviceId,
286+
MICROPHONE_INPUT.DEVICE_ID_MAX_LENGTH,
287+
)
288+
config.microphoneDeviceLabel = normalizeConfigString(
289+
config.microphoneDeviceLabel,
290+
MICROPHONE_INPUT.DEVICE_LABEL_MAX_LENGTH,
291+
)
278292
config.apiKeys = {
279293
cn: this.decryptKey(config.apiKeys.cn),
280294
intl: this.decryptKey(config.apiKeys.intl),
@@ -294,6 +308,14 @@ export class ConfigManager {
294308
...current,
295309
...config,
296310
provider: normalizeASRProvider(config.provider ?? current.provider),
311+
microphoneDeviceId: normalizeConfigString(
312+
config.microphoneDeviceId ?? current.microphoneDeviceId,
313+
MICROPHONE_INPUT.DEVICE_ID_MAX_LENGTH,
314+
),
315+
microphoneDeviceLabel: normalizeConfigString(
316+
config.microphoneDeviceLabel ?? current.microphoneDeviceLabel,
317+
MICROPHONE_INPUT.DEVICE_LABEL_MAX_LENGTH,
318+
),
297319
apiKeys: {
298320
...current.apiKeys,
299321
...(config.apiKeys ?? {}),

electron/preload/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# preload/
22

3+
## Microphone Selection
4+
5+
`RecordingStartPayload` may include `microphoneDeviceId`; the hidden recorder
6+
uses it as a capture hint while preload keeps the same listener API.
7+
38
Electron 预加载脚本目录,作为主进程与渲染进程之间的安全桥梁。
49

510
## 文件列表

electron/shared/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# shared/
22

3+
## Microphone Selection
4+
5+
`ASRConfig` stores the selected microphone device ID and last known label.
6+
`RecordingStartPayload` carries the device ID from main to the hidden recorder,
7+
and `MICROPHONE_INPUT` defines the renderer select sentinel plus config string
8+
length caps.
9+
310
主进程与渲染进程共享的类型、常量与多语言资源。
411

512
## 文件列表

electron/shared/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,12 @@ export const AUDIO_CONFIG = {
320320
BIT_DEPTH: 16,
321321
} as const
322322

323+
export const MICROPHONE_INPUT = {
324+
SYSTEM_DEFAULT_ID: '__system-default__',
325+
DEVICE_ID_MAX_LENGTH: 256,
326+
DEVICE_LABEL_MAX_LENGTH: 128,
327+
} as const
328+
323329
export const LOW_VOLUME_GAIN_DB = 10
324330

325331
export const HISTORY_RETENTION_DAYS = 90

electron/shared/locales/en.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,22 @@
142142
"durationWarning": "Note: Maximum duration for single voice input is 3 minutes.",
143143
"lowVolumeMode": "Low-volume mode",
144144
"lowVolumeModeHelp": "Boost quiet speech in software before ASR (may also amplify background noise).",
145+
"microphone": {
146+
"label": "Microphone",
147+
"placeholder": "Select microphone",
148+
"systemDefault": "System default microphone",
149+
"refresh": "Detect",
150+
"detecting": "Detecting...",
151+
"help": "Choose which input device Voice Key records from.",
152+
"noDevices": "No microphones detected",
153+
"deviceFallback": "Microphone {{index}}",
154+
"unknownDevice": "Selected microphone",
155+
"unavailableOption": "{{label}} (unavailable)",
156+
"unavailableHelp": "The selected microphone is unavailable. Recording will fall back to the system default until it returns or you choose another device.",
157+
"permissionHelp": "Microphone permission is needed to show device names.",
158+
"unsupported": "This environment cannot detect microphone devices.",
159+
"detectFailed": "Couldn't detect microphones."
160+
},
145161
"asrProvider": "Recognition engine",
146162
"asrProviderLocal": "Local model",
147163
"asrProviderGlm": "Zhipu GLM",

electron/shared/locales/zh.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,22 @@
142142
"durationWarning": "注意:单次语音输入最长为 3 分钟。",
143143
"lowVolumeMode": "低音量模式",
144144
"lowVolumeModeHelp": "在调用 ASR 前对小声语音进行软件增益(可能同时放大环境噪声)。",
145+
"microphone": {
146+
"label": "麦克风",
147+
"placeholder": "选择麦克风",
148+
"systemDefault": "系统默认麦克风",
149+
"refresh": "检测",
150+
"detecting": "检测中...",
151+
"help": "选择 Voice Key 录音时使用的输入设备。",
152+
"noDevices": "未检测到麦克风",
153+
"deviceFallback": "麦克风 {{index}}",
154+
"unknownDevice": "已选择的麦克风",
155+
"unavailableOption": "{{label}}(不可用)",
156+
"unavailableHelp": "已选择的麦克风当前不可用。录音会暂时回退到系统默认麦克风,直到设备恢复或你选择其它设备。",
157+
"permissionHelp": "需要麦克风权限才能显示设备名称。",
158+
"unsupported": "当前环境无法检测麦克风设备。",
159+
"detectFailed": "无法检测麦克风。"
160+
},
145161
"asrProvider": "识别引擎",
146162
"asrProviderLocal": "本地模型",
147163
"asrProviderGlm": "智谱 GLM",

electron/shared/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface ASRConfig {
2020
intl: string
2121
}
2222
lowVolumeMode?: boolean
23+
microphoneDeviceId?: string
24+
microphoneDeviceLabel?: string
2325

2426
// Deprecated: for backward compatibility during migration
2527
apiKey?: string
@@ -125,6 +127,7 @@ export interface RefineConnectionResult {
125127

126128
export interface RecordingStartPayload {
127129
sessionId: string
130+
microphoneDeviceId?: string
128131
}
129132

130133
export interface AudioChunkPayload {

0 commit comments

Comments
 (0)