77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
// ============================================================
|
||
// MicManager — 麦克风音频采集
|
||
// 职责:获取麦克风 MediaStream,供 VAD 和音频录制使用
|
||
// ============================================================
|
||
|
||
import { useCallback, useRef, useState } from "react";
|
||
import { useI18n } from "../../lib/i18n";
|
||
|
||
// Electron API 类型
|
||
interface ElectronAPI {
|
||
requestMicAccess: () => Promise<boolean>;
|
||
isElectron: boolean;
|
||
}
|
||
|
||
declare global {
|
||
interface Window {
|
||
electronAPI?: ElectronAPI;
|
||
}
|
||
}
|
||
|
||
export function useMicrophone() {
|
||
const { t } = useI18n();
|
||
const [stream, setStream] = useState<MediaStream | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const audioContextRef = useRef<AudioContext | null>(null);
|
||
|
||
const startMic = useCallback(async () => {
|
||
try {
|
||
// Electron 环境:先通过 IPC 请求系统级权限
|
||
if (window.electronAPI?.isElectron) {
|
||
const granted = await window.electronAPI.requestMicAccess();
|
||
if (!granted) {
|
||
const msg = t("error.micAccess");
|
||
setError(msg);
|
||
console.warn("[Mic] macOS 系统麦克风权限被拒绝");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
||
audio: {
|
||
sampleRate: 16000,
|
||
channelCount: 1,
|
||
echoCancellation: true,
|
||
noiseSuppression: true,
|
||
},
|
||
video: false,
|
||
});
|
||
setStream(mediaStream);
|
||
setError(null);
|
||
return mediaStream;
|
||
} catch (err) {
|
||
const message = err instanceof Error ? err.message : t("error.micAccess");
|
||
setError(message);
|
||
console.error("[Mic] 获取麦克风失败:", err);
|
||
return null;
|
||
}
|
||
}, []);
|
||
|
||
const stopMic = useCallback(() => {
|
||
stream?.getTracks().forEach((track) => track.stop());
|
||
setStream(null);
|
||
audioContextRef.current?.close();
|
||
audioContextRef.current = null;
|
||
}, [stream]);
|
||
|
||
/** 获取或创建 AudioContext */
|
||
const getAudioContext = useCallback((): AudioContext => {
|
||
if (!audioContextRef.current) {
|
||
audioContextRef.current = new AudioContext({ sampleRate: 16000 });
|
||
}
|
||
return audioContextRef.current;
|
||
}, []);
|
||
|
||
return { stream, error, startMic, stopMic, getAudioContext };
|
||
}
|