From db0c581bdd3f0d1a8b5bf6bc062d678e7d314886 Mon Sep 17 00:00:00 2001 From: mssoftjp <115339791+mssoftjp@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:10:22 +0900 Subject: [PATCH] fix: harden audio promise handling --- src/core/audio/AudioRecorder.ts | 55 ++++++++++++++++++++++----------- src/core/audio/VADProcessor.ts | 29 ++++++++++------- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/core/audio/AudioRecorder.ts b/src/core/audio/AudioRecorder.ts index 96de892..6febc49 100644 --- a/src/core/audio/AudioRecorder.ts +++ b/src/core/audio/AudioRecorder.ts @@ -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 { + 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 { + 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); } } diff --git a/src/core/audio/VADProcessor.ts b/src/core/audio/VADProcessor.ts index 6f9b135..2b6939d 100644 --- a/src/core/audio/VADProcessor.ts +++ b/src/core/audio/VADProcessor.ts @@ -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 => { 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 { + 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 { + detectSpeechSegments(audioData: Float32Array): VADSegment[] { this.throwIfDisposed(); if (!this.fvadModule || !this.vadInstance || !this.bufferPtr) { throw new Error('VAD not initialized');