didvan-app/lib/services/ai/voice_audio_engine.dart

127 lines
3.5 KiB
Dart

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<Object?, Object?>.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<Object?, Object?> 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<VoiceAudioEvent>? _eventStream;
Stream<VoiceAudioEvent> get events {
return _eventStream ??= _events
.receiveBroadcastStream()
.map(VoiceAudioEvent.fromPlatform)
.asBroadcastStream();
}
Future<VoiceAudioCapabilities> start({required int sampleRate}) async {
final result = await _methods.invokeMapMethod<Object?, Object?>(
'start',
<String, Object?>{'sampleRate': sampleRate},
);
if (result == null) {
throw StateError('Native voice audio engine returned no capabilities');
}
return VoiceAudioCapabilities.fromPlatform(result);
}
Future<void> enqueuePlayback(
Uint8List pcm, {
required int generation,
}) {
return _methods.invokeMethod<void>(
'enqueuePlayback',
<String, Object?>{
'pcm': pcm,
'generation': generation,
},
);
}
Future<void> stopPlayback({required int generation}) {
return _methods.invokeMethod<void>(
'stopPlayback',
<String, Object?>{'generation': generation},
);
}
Future<void> stop() => _methods.invokeMethod<void>('stop');
}