web-asr-core
Version:
WebASR Core - Browser-based speech processing with VAD, WakeWord and Whisper - Unified all-in-one version
386 lines (384 loc) • 12.7 kB
JavaScript
import { ConfigManager } from '../utils/config-manager';
import { AudioResampler, ResamplingAlgorithm } from './audio-resampler';
/**
* AudioCapture - 麥克風音訊擷取服務
*
* 提供完整的麥克風管理功能:
* - 列舉所有音訊輸入裝置
* - 選擇特定麥克風
* - 擷取原始音訊資料
* - 自動重採樣到目標採樣率
* - 支援 AudioWorklet 和 ScriptProcessor
*/
export class AudioCapture {
config;
audioContext;
mediaStream;
sourceNode;
processorNode;
resampler;
callbacks = new Set();
state = {
isCapturing: false,
totalSamples: 0
};
audioWorkletLoaded = false;
constructor(config = ConfigManager.getInstance()) {
this.config = config;
this.resampler = new AudioResampler(config);
}
/**
* 獲取所有音訊輸入裝置列表
* @returns 音訊裝置資訊陣列
*/
async getAudioDevices() {
// 先請求權限(必要,否則 label 會是空的)
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
}
catch (e) {
console.warn('無法獲取麥克風權限:', e);
}
const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputs = devices
.filter(device => device.kind === 'audioinput')
.map(device => ({
deviceId: device.deviceId,
label: device.label || `麥克風 ${device.deviceId.substring(0, 8)}`,
kind: device.kind,
groupId: device.groupId,
isDefault: device.deviceId === 'default'
}));
return audioInputs;
}
/**
* 獲取預設麥克風
*/
async getDefaultDevice() {
const devices = await this.getAudioDevices();
return devices.find(d => d.isDefault || d.deviceId === 'default') || devices[0] || null;
}
/**
* 開始音訊擷取
* @param options 擷取選項
*/
async startCapture(options = {}) {
if (this.state.isCapturing) {
console.warn('音訊擷取已在進行中');
return;
}
// 設定預設選項
const finalOptions = {
sampleRate: 16000,
channelCount: 1,
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
bufferSize: 2048,
useAudioWorklet: true,
...options
};
try {
// 建立音訊約束
const constraints = {
audio: {
deviceId: finalOptions.deviceId ? { exact: finalOptions.deviceId } : undefined,
sampleRate: finalOptions.sampleRate,
channelCount: finalOptions.channelCount,
echoCancellation: finalOptions.echoCancellation,
noiseSuppression: finalOptions.noiseSuppression,
autoGainControl: finalOptions.autoGainControl
}
};
// 獲取媒體串流
this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
// 創建或重用 AudioContext
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
// 如果 AudioContext 被暫停,恢復它
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
}
// 創建音訊源節點
this.sourceNode = this.audioContext.createMediaStreamSource(this.mediaStream);
// 獲取實際的音訊設定
const audioTrack = this.mediaStream.getAudioTracks()[0];
const actualSettings = audioTrack.getSettings();
// 更新狀態
this.state = {
isCapturing: true,
currentDeviceId: actualSettings.deviceId || finalOptions.deviceId,
actualSampleRate: actualSettings.sampleRate || this.audioContext.sampleRate,
actualChannelCount: actualSettings.channelCount || 1,
totalSamples: 0,
startTime: Date.now()
};
// 創建處理節點
if (finalOptions.useAudioWorklet && this.audioContext.audioWorklet) {
await this.createAudioWorkletProcessor(finalOptions);
}
else {
this.createScriptProcessor(finalOptions);
}
// 連接節點
this.sourceNode.connect(this.processorNode);
// 注意:不連接到 destination,避免回音
// this.processorNode!.connect(this.audioContext.destination);
}
catch (error) {
this.state.isCapturing = false;
throw new Error(`無法開始音訊擷取: ${error}`);
}
}
/**
* 創建 AudioWorklet 處理器
*/
async createAudioWorkletProcessor(options) {
if (!this.audioContext)
return;
// 載入 AudioWorklet(如果尚未載入)
if (!this.audioWorkletLoaded) {
const workletCode = `
class AudioCaptureProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.bufferSize = 128; // AudioWorklet 固定大小
}
process(inputs, outputs, parameters) {
const input = inputs[0];
if (input && input.length > 0) {
const channelData = input[0];
// 發送資料到主線程
this.port.postMessage({
type: 'audio-data',
data: channelData,
timestamp: currentTime
});
}
return true; // 保持處理器活躍
}
}
registerProcessor('audio-capture-processor', AudioCaptureProcessor);
`;
const blob = new Blob([workletCode], { type: 'application/javascript' });
const workletUrl = URL.createObjectURL(blob);
await this.audioContext.audioWorklet.addModule(workletUrl);
URL.revokeObjectURL(workletUrl);
this.audioWorkletLoaded = true;
}
// 創建 AudioWorkletNode
const workletNode = new AudioWorkletNode(this.audioContext, 'audio-capture-processor');
// 設定訊息處理
workletNode.port.onmessage = (event) => {
if (event.data.type === 'audio-data') {
this.processAudioData(new Float32Array(event.data.data), event.data.timestamp);
}
};
this.processorNode = workletNode;
}
/**
* 創建 ScriptProcessor(降級方案)
*/
createScriptProcessor(options) {
if (!this.audioContext)
return;
const processor = this.audioContext.createScriptProcessor(options.bufferSize || 2048, 1, // 輸入聲道
1 // 輸出聲道
);
processor.onaudioprocess = (event) => {
const inputData = event.inputBuffer.getChannelData(0);
const timestamp = event.timeStamp;
this.processAudioData(inputData, timestamp);
};
this.processorNode = processor;
}
/**
* 處理音訊資料
*/
async processAudioData(audioData, timestamp) {
if (!this.state.isCapturing)
return;
// 更新統計
this.state.totalSamples += audioData.length;
// 重採樣到目標採樣率(如果需要)
let processedData = audioData;
if (this.state.actualSampleRate && this.state.actualSampleRate !== 16000) {
processedData = this.resampler.resampleTo16kHz(audioData, this.state.actualSampleRate);
}
// 觸發所有回調
this.callbacks.forEach(callback => {
try {
callback(processedData, timestamp);
}
catch (error) {
console.error('音訊回調錯誤:', error);
}
});
}
/**
* 停止音訊擷取
*/
stopCapture() {
if (!this.state.isCapturing)
return;
// 斷開連接
if (this.processorNode) {
this.processorNode.disconnect();
this.processorNode = undefined;
}
if (this.sourceNode) {
this.sourceNode.disconnect();
this.sourceNode = undefined;
}
// 停止媒體串流
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
this.mediaStream = undefined;
}
// 更新狀態
this.state.isCapturing = false;
}
/**
* 暫停音訊擷取(保持連接但停止處理)
*/
pause() {
if (!this.state.isCapturing)
return;
if (this.sourceNode && this.processorNode) {
try {
this.sourceNode.disconnect(this.processorNode);
}
catch (e) {
// 忽略斷開錯誤
}
}
this.state.isCapturing = false;
}
/**
* 恢復音訊擷取
*/
resume() {
if (this.state.isCapturing)
return;
if (this.sourceNode && this.processorNode) {
this.sourceNode.connect(this.processorNode);
this.state.isCapturing = true;
}
}
/**
* 註冊音訊資料回調
* @param callback 回調函數
*/
onAudioData(callback) {
this.callbacks.add(callback);
}
/**
* 移除音訊資料回調
* @param callback 回調函數
*/
offAudioData(callback) {
this.callbacks.delete(callback);
}
/**
* 清除所有回調
*/
clearCallbacks() {
this.callbacks.clear();
}
/**
* 獲取當前擷取狀態
*/
getState() {
return { ...this.state };
}
/**
* 獲取擷取統計資訊
*/
getStats() {
const duration = this.state.startTime
? (Date.now() - this.state.startTime) / 1000
: 0;
return {
isCapturing: this.state.isCapturing,
duration,
totalSamples: this.state.totalSamples,
sampleRate: this.state.actualSampleRate,
deviceLabel: this.state.currentDeviceId
};
}
/**
* 切換麥克風裝置
* @param deviceId 新裝置 ID
*/
async switchDevice(deviceId) {
const wasCapturing = this.state.isCapturing;
if (wasCapturing) {
this.stopCapture();
}
await this.startCapture({ deviceId });
}
/**
* 監聽裝置變更
*/
onDeviceChange(callback) {
navigator.mediaDevices.addEventListener('devicechange', async () => {
const devices = await this.getAudioDevices();
callback(devices);
});
}
/**
* 釋放所有資源
*/
dispose() {
this.stopCapture();
this.clearCallbacks();
if (this.audioContext) {
this.audioContext.close();
this.audioContext = undefined;
}
this.audioWorkletLoaded = false;
}
/**
* 檢查瀏覽器相容性
*/
static checkBrowserSupport() {
return {
getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
audioContext: !!(window.AudioContext || window.webkitAudioContext),
audioWorklet: !!(window.AudioContext && AudioContext.prototype.audioWorklet),
mediaRecorder: !!(window.MediaRecorder)
};
}
}
/**
* 單例實例
*/
let captureInstance = null;
/**
* 獲取全域音訊擷取實例
*/
export function getAudioCapture() {
if (!captureInstance) {
captureInstance = new AudioCapture();
}
return captureInstance;
}
/**
* 便利函數:快速開始擷取
*/
export async function startAudioCapture(callback, options) {
const capture = getAudioCapture();
capture.onAudioData(callback);
await capture.startCapture(options);
return capture;
}
/**
* 便利函數:列出所有麥克風
*/
export async function listMicrophones() {
const capture = getAudioCapture();
return capture.getAudioDevices();
}
export default AudioCapture;
//# sourceMappingURL=audio-capture.js.map