fix: harden audio promise handling

This commit is contained in:
mssoftjp 2025-11-09 02:10:22 +09:00
parent 6724316463
commit db0c581bdd
2 changed files with 54 additions and 30 deletions

View file

@ -261,12 +261,17 @@ export class AudioRecorder extends Disposable {
// Set up maximum recording time limit
const maxSeconds = this.options.maxRecordingSeconds || AUDIO_CONSTANTS.MAX_RECORDING_SECONDS;
this.maxRecordingTimer = setTimeout(async () => {
this.maxRecordingTimer = setTimeout(() => {
this.logger?.info(`Maximum recording time (${maxSeconds}s) reached, stopping recording`);
const audioBlob = await this.stopRecording();
if (audioBlob && this.options.onSpeechEnd) {
this.options.onSpeechEnd(audioBlob);
}
void this.stopRecording()
.then((audioBlob) => {
if (audioBlob && this.options.onSpeechEnd) {
this.options.onSpeechEnd(audioBlob);
}
})
.catch(error => {
this.logger?.error('Failed to stop recording after reaching max duration', error);
});
}, maxSeconds * 1000);
this.mediaRecorder.ondataavailable = (event) => {
@ -293,7 +298,7 @@ export class AudioRecorder extends Disposable {
}
}
private async startContinuousProcessing(): Promise<void> {
private startContinuousProcessing(): void {
if (!this.audioContext || !this.sourceNode) return;
const lowPassFilter = this.lowPassFilter;
if (!lowPassFilter) {
@ -320,9 +325,13 @@ export class AudioRecorder extends Disposable {
this.workletNode.port.postMessage({ type: 'start' });
// Handle audio data from worklet
this.workletNode.port.onmessage = async (event) => {
this.workletNode.port.onmessage = (event) => {
if (event.data.type === 'audio' && this.isRecording) {
await this.processAudioData(event.data.data);
try {
this.processAudioData(event.data.data);
} catch (error) {
this.logger?.error('Failed to process audio data from AudioWorklet', error);
}
}
};
@ -353,16 +362,20 @@ export class AudioRecorder extends Disposable {
lowPassFilter.connect(scriptProcessor);
scriptProcessor.connect(this.audioContext.destination);
scriptProcessor.onaudioprocess = async (event) => {
scriptProcessor.onaudioprocess = (event) => {
if (!this.isRecording) return;
const inputData = event.inputBuffer.getChannelData(0);
const audioData = new Float32Array(inputData);
await this.processAudioData(audioData);
try {
this.processAudioData(audioData);
} catch (error) {
this.logger?.error('Failed to process audio data from ScriptProcessor', error);
}
};
}
private async processAudioData(audioData: Float32Array): Promise<void> {
private processAudioData(audioData: Float32Array): void {
// Early return if not recording to prevent unnecessary processing
if (!this.isRecording) {
return;
@ -401,7 +414,7 @@ export class AudioRecorder extends Disposable {
// Process with VAD
const segments = this.vadProcessor ? await this.vadProcessor.detectSpeechSegments(combinedData) : [];
const segments = this.vadProcessor ? this.vadProcessor.detectSpeechSegments(combinedData) : [];
if (segments.length > 0) {
this.handleSpeechDetected();
@ -465,13 +478,19 @@ export class AudioRecorder extends Disposable {
// Check if we should auto-stop due to silence (only in VAD mode)
if (this.options.useVAD && this.lastSpeechTime > 0 && !this.silenceTimer) {
const autoStopDuration = this.options.autoStopSilenceDuration ?? DEFAULT_AUDIO_SETTINGS.autoStopSilenceDuration;
this.silenceTimer = setTimeout(async () => {
if (this.isRecording) {
const audioBlob = await this.stopRecording();
if (audioBlob && this.options.onSpeechEnd) {
this.options.onSpeechEnd(audioBlob);
}
this.silenceTimer = setTimeout(() => {
if (!this.isRecording) {
return;
}
void this.stopRecording()
.then((audioBlob) => {
if (audioBlob && this.options.onSpeechEnd) {
this.options.onSpeechEnd(audioBlob);
}
})
.catch(error => {
this.logger?.error('Failed to stop recording after silence detection', error);
});
}, this.options.useVAD ? autoStopDuration : 0);
}
}

View file

@ -89,7 +89,7 @@ export class VADProcessor extends Disposable {
await this.initializeFvadModule(wasmBuffer);
// VAD インスタンスを作成して設定
await this.createAndConfigureVAD();
this.createAndConfigureVAD();
this.logger.info('WebRTC VAD processor initialized successfully');
@ -142,17 +142,20 @@ export class VADProcessor extends Disposable {
document.head.appendChild(script);
// モジュールが読み込まれるのを待つ
setTimeout(async () => {
try {
setTimeout(() => {
const loadModule = async (): Promise<void> => {
if (!hasFvadModule(globalWindow)) {
throw new Error('fvad module not found in global scope');
}
const createModule = globalWindow.__fvadModule;
if (!createModule) {
throw new Error('fvad module factory is undefined');
}
this.logger.debug('fvad module loaded from global scope');
// WebAssembly モジュールを初期化
this.fvadModule = await createModule!({
this.fvadModule = await createModule({
wasmBinary: new Uint8Array(wasmBuffer),
instantiateWasm: (
imports: WebAssembly.Imports,
@ -164,7 +167,7 @@ export class VADProcessor extends Disposable {
})
.catch((error) => {
this.logger.error('WebRTC VAD WASM instantiation error', error);
reject(error);
reject(error instanceof Error ? error : new Error(String(error)));
});
return {};
}
@ -173,11 +176,13 @@ export class VADProcessor extends Disposable {
// クリーンアップ
document.head.removeChild(script);
delete globalWindow.__fvadModule;
resolve();
} catch (error) {
reject(error);
}
};
loadModule()
.then(() => resolve())
.catch((error) => {
reject(error instanceof Error ? error : new Error(String(error)));
});
}, 100); // モジュール読み込みを待つ
});
}
@ -185,7 +190,7 @@ export class VADProcessor extends Disposable {
/**
* VAD
*/
private async createAndConfigureVAD(): Promise<void> {
private createAndConfigureVAD(): void {
if (!this.fvadModule) {
throw new Error('fvad module not initialized');
}
@ -313,7 +318,7 @@ export class VADProcessor extends Disposable {
/**
*
*/
async detectSpeechSegments(audioData: Float32Array): Promise<VADSegment[]> {
detectSpeechSegments(audioData: Float32Array): VADSegment[] {
this.throwIfDisposed();
if (!this.fvadModule || !this.vadInstance || !this.bufferPtr) {
throw new Error('VAD not initialized');