127 lines
3.5 KiB
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');
|
|
}
|