import { NativeModules, NativeEventEmitter, Platform } from 'react-native'; import { GoertzelDetector } from '../detectors/GoertzelDetector'; import { DualToneDetector } from '../detectors/DualToneDetector'; import { BridgeDetector } from '../detectors/BridgeDetector'; import { SequenceStateMachine, ConfigDefault } from '../detectors/SequenceStateMachine'; import { FrequencyTableLoader } from '../utils/FrequencyTableLoader'; import { ToneListenReactNativeConfig } from '../types/ToneTypes'; const AudioEmitter = NativeModules.ToneListenAudioModule; const audioEmitter = AudioEmitter ? new NativeEventEmitter(AudioEmitter) : null; export class NativeAudioPipeline { private cfg: ToneListenReactNativeConfig; private goertzel: GoertzelDetector | null = null; private dual: DualToneDetector | null = null; private bridge: BridgeDetector | null = null; private ssm: SequenceStateMachine | null = null; private sub: any = null; constructor(cfg: ToneListenReactNativeConfig) { this.cfg = cfg; } async init(onSequence: (seq: string) => void) { const table = await FrequencyTableLoader.loadFrequencyTables(); const freqs = FrequencyTableLoader.getFrequenciesToDetect(table); this.goertzel = new GoertzelDetector(this.cfg); this.dual = new DualToneDetector(this.cfg, table); this.bridge = new BridgeDetector(); // Create resolveTag function const resolveTag = (low: number, high: number): string | null => { const r = this.dual?.isDualTone(low, high); return r?.ok ? r.tag || null : null; }; this.ssm = new SequenceStateMachine(ConfigDefault, resolveTag); this.ssm.onSequence = (sequence: string, point: any) => onSequence(sequence); } async start(sampleRate = 44100, bufferSize = 4800) { console.log('🎯 NativeAudioPipeline: start called with sampleRate:', sampleRate, 'bufferSize:', bufferSize); if (!audioEmitter) { console.error('🎯 NativeAudioPipeline: Native audio emitter not linked'); throw new Error('Native audio emitter not linked'); } if (!this.goertzel || !this.ssm) { console.error('🎯 NativeAudioPipeline: Pipeline not initialized'); throw new Error('Pipeline not initialized'); } console.log('🎯 NativeAudioPipeline: AudioEmitter available:', !!AudioEmitter); console.log('🎯 NativeAudioPipeline: AudioEmitter methods:', AudioEmitter ? Object.keys(AudioEmitter) : 'null'); if (!this.sub) { console.log('🎯 NativeAudioPipeline: Adding listener for TLRN_AudioFrame'); let lastSrc: string | null = null; this.sub = audioEmitter.addListener('TLRN_AudioFrame', (payload: { sampleRate: number; buffer: number[]; source?: string }) => { if (payload.source && payload.source !== lastSrc) { lastSrc = payload.source; console.log('🎯 NativeAudioPipeline: AudioFrame source ->', payload.source); } // Debug logging removed to reduce console noise const data = new Float32Array(payload.buffer); const results = this.goertzel!.processAudioBuffer(data, payload.sampleRate); if (results.length === 0) return; const freqs = results.map(r => Math.round(r.frequency)); const powers = results.map(r => r.power); let max1: any = null, max2: any = null; let maxP = Number.MIN_VALUE, maxF = 0; let maxP2 = Number.MIN_VALUE, maxF2 = 0; for (let i = 0; i < freqs.length; i++) { const f = freqs[i]; const p = powers[i]; if (typeof f !== 'number' || typeof p !== 'number') continue; const gate = f >= 18000 ? 0.01 : 0.05; if (f >= 14000 && f <= 22000 && p > gate) { if (p > maxP) { maxP2 = maxP; maxF2 = maxF; maxP = p; maxF = f; } else if (p > maxP2) { maxP2 = p; maxF2 = f; } } } if (maxP > Number.MIN_VALUE) max1 = { f: maxF, p: maxP }; if (maxP2 > Number.MIN_VALUE) max2 = { f: maxF2, p: maxP2 }; const br = this.bridge?.detect(freqs, powers) || null; this.ssm!.feed(max1, max2, br); }); console.log('🎯 NativeAudioPipeline: Listener added successfully'); } else { console.log('🎯 NativeAudioPipeline: Using existing subscription'); } console.log('🎯 NativeAudioPipeline: Calling AudioEmitter.start...'); await AudioEmitter.start(sampleRate, bufferSize); console.log('🎯 NativeAudioPipeline: AudioEmitter.start completed successfully'); } async stop() { if (this.sub) { this.sub.remove(); this.sub = null; } if (AudioEmitter?.stop) { await AudioEmitter.stop(); } } /** * Switch audio session mode * @param mode - Audio mode: 'measurement', 'default', 'videoRecording', 'moviePlayback', 'videoChat', 'gameChat', 'spokenAudio' */ async setAudioMode(mode: string): Promise { if (!AudioEmitter?.setAudioMode) { console.warn('🎯 NativeAudioPipeline: setAudioMode not available on this platform'); return false; } try { const result = await AudioEmitter.setAudioMode(mode); console.log(`🎯 NativeAudioPipeline: Audio mode switched to: ${mode}`); return result; } catch (error) { console.error('🎯 NativeAudioPipeline: Failed to switch audio mode:', error); return false; } } /** * Get current audio session mode */ async getCurrentAudioMode(): Promise { if (!AudioEmitter?.getCurrentAudioMode) { console.warn('🎯 NativeAudioPipeline: getCurrentAudioMode not available on this platform'); return null; } try { const mode = await AudioEmitter.getCurrentAudioMode(); return mode; } catch (error) { console.error('🎯 NativeAudioPipeline: Failed to get current audio mode:', error); return null; } } /** * Check if other media is currently playing */ async isMediaPlaying(): Promise { if (!AudioEmitter?.isMediaPlaying) { console.warn('🎯 NativeAudioPipeline: isMediaPlaying not available on this platform'); return false; } try { const isPlaying = await AudioEmitter.isMediaPlaying(); return isPlaying; } catch (error) { console.error('🎯 NativeAudioPipeline: Failed to check media playing status:', error); return false; } } /** * Automatically switch to appropriate audio mode based on media playback */ async autoSwitchAudioMode(): Promise { try { const isPlaying = await this.isMediaPlaying(); const targetMode = isPlaying ? 'default' : 'measurement'; const currentMode = await this.getCurrentAudioMode(); if (currentMode !== targetMode) { console.log(`🎯 NativeAudioPipeline: Auto-switching from ${currentMode} to ${targetMode} (media playing: ${isPlaying})`); return await this.setAudioMode(targetMode); } return true; } catch (error) { console.error('🎯 NativeAudioPipeline: Failed to auto-switch audio mode:', error); return false; } } /** * Prepare audio session for media playback (iOS only) */ async prepareForMediaPlayback(): Promise { if (!AudioEmitter?.prepareForMediaPlayback) { console.warn('🎯 NativeAudioPipeline: prepareForMediaPlayback not available on this platform'); return false; } try { const result = await AudioEmitter.prepareForMediaPlayback(); console.log('🎯 NativeAudioPipeline: Audio session prepared for media playback'); return result; } catch (error) { console.error('🎯 NativeAudioPipeline: Failed to prepare for media playback:', error); return false; } } /** * Restore audio session after media playback (iOS only) */ async restoreAfterMediaPlayback(): Promise { if (!AudioEmitter?.restoreAfterMediaPlayback) { console.warn('🎯 NativeAudioPipeline: restoreAfterMediaPlayback not available on this platform'); return false; } try { const result = await AudioEmitter.restoreAfterMediaPlayback(); console.log('🎯 NativeAudioPipeline: Audio session restored after media playback'); return result; } catch (error) { console.error('🎯 NativeAudioPipeline: Failed to restore after media playback:', error); return false; } } /** * Start decoding playback audio for detection (iOS & Android) */ async startPlaybackDecode(url: string): Promise { if (!AudioEmitter?.startPlaybackDecode) { console.warn('🎯 NativeAudioPipeline: startPlaybackDecode not available on this platform'); return false; } try { const result = await AudioEmitter.startPlaybackDecode(url); console.log('🎯 NativeAudioPipeline: Playback decode started'); return result; } catch (error) { console.error('🎯 NativeAudioPipeline: Failed to start playback decode:', error); return false; } } /** * Stop decoding playback audio for detection (iOS & Android) */ async stopPlaybackDecode(): Promise { if (!AudioEmitter?.stopPlaybackDecode) { console.warn('🎯 NativeAudioPipeline: stopPlaybackDecode not available on this platform'); return false; } try { const result = await AudioEmitter.stopPlaybackDecode(); console.log('🎯 NativeAudioPipeline: Playback decode stopped'); return result; } catch (error) { console.error('🎯 NativeAudioPipeline: Failed to stop playback decode:', error); return false; } } }