import 'dart:async'; import 'package:flutter/services.dart'; enum VoiceAudioEventType { microphone, playbackStarted, playbackIdle, error, } class VoiceAudioEvent { const VoiceAudioEvent._({ required this.type, this.pcm, this.rms = 0, this.message, }); final VoiceAudioEventType type; final Uint8List? pcm; final double rms; final String? message; factory VoiceAudioEvent.fromPlatform(dynamic raw) { final event = Map.from(raw as Map); switch (event['type']) { case 'microphone': return VoiceAudioEvent._( type: VoiceAudioEventType.microphone, pcm: event['pcm'] as Uint8List?, rms: (event['rms'] as num?)?.toDouble() ?? 0, ); case 'playbackStarted': return const VoiceAudioEvent._( type: VoiceAudioEventType.playbackStarted, ); case 'playbackIdle': return const VoiceAudioEvent._( type: VoiceAudioEventType.playbackIdle, ); default: return VoiceAudioEvent._( type: VoiceAudioEventType.error, message: event['message']?.toString() ?? 'Unknown audio error', ); } } } class VoiceAudioCapabilities { const VoiceAudioCapabilities({ required this.aecAvailable, required this.aecEnabled, required this.noiseSuppressionEnabled, required this.sampleRate, }); final bool aecAvailable; final bool aecEnabled; final bool noiseSuppressionEnabled; final int sampleRate; factory VoiceAudioCapabilities.fromPlatform(Map value) { return VoiceAudioCapabilities( aecAvailable: value['aecAvailable'] == true, aecEnabled: value['aecEnabled'] == true, noiseSuppressionEnabled: value['noiseSuppressionEnabled'] == true, sampleRate: (value['sampleRate'] as num?)?.toInt() ?? 24000, ); } } /// Thin Dart bridge to the single native full-duplex Android audio engine. /// /// The native side owns both AudioRecord and AudioTrack. This is important: /// Android's acoustic echo canceller receives a stable playback reference and /// the app no longer creates competing recorder/player audio sessions. class VoiceAudioEngine { static const MethodChannel _methods = MethodChannel('com.didvan.didvanapp/voice_audio/methods'); static const EventChannel _events = EventChannel('com.didvan.didvanapp/voice_audio/events'); Stream? _eventStream; Stream get events { return _eventStream ??= _events .receiveBroadcastStream() .map(VoiceAudioEvent.fromPlatform) .asBroadcastStream(); } Future start({required int sampleRate}) async { final result = await _methods.invokeMapMethod( 'start', {'sampleRate': sampleRate}, ); if (result == null) { throw StateError('Native voice audio engine returned no capabilities'); } return VoiceAudioCapabilities.fromPlatform(result); } Future enqueuePlayback( Uint8List pcm, { required int generation, }) { return _methods.invokeMethod( 'enqueuePlayback', { 'pcm': pcm, 'generation': generation, }, ); } Future stopPlayback({required int generation}) { return _methods.invokeMethod( 'stopPlayback', {'generation': generation}, ); } Future stop() => _methods.invokeMethod('stop'); }