Files
StackChan/app/lib/util/audio_engine_manager.dart
T
2026-04-27 12:16:53 +08:00

158 lines
4.4 KiB
Dart

/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:opus_codec/opus_codec.dart' as opus_flutter;
import 'package:opus_codec_dart/opus_codec_dart.dart';
import 'package:permission_handler/permission_handler.dart';
import 'native_bridge.dart';
class AudioEngineManager {
static final AudioEngineManager shared = AudioEngineManager._internal();
AudioEngineManager._internal();
late SimpleOpusEncoder simpleOpusEncoder;
late SimpleOpusDecoder simpleOpusDecoder;
bool _isInitialized = false;
Function(Uint8List)? onAudioData;
Function(double)? onDecibel;
//🔥 Fixed Opus standard parameters (DO NOT CHANGE)
static const int sampleRate = 16000;
static const int channels = 1;
static const int frameSamples = 320;
// autoBufferqueue(fixcore)
final Int16List _pcmBuffer = Int16List(frameSamples * 10);
int _bufferWriteIndex = 0;
Future<void> init() async {
try {
WidgetsFlutterBinding.ensureInitialized();
await opus_flutter.load();
initOpus(await opus_flutter.load());
// StandardencodeController
simpleOpusEncoder = SimpleOpusEncoder(
sampleRate: sampleRate,
channels: channels,
application: Application.voip,
);
simpleOpusDecoder = SimpleOpusDecoder(
sampleRate: sampleRate,
channels: channels,
);
_isInitialized = true;
debugPrint(" Audio引擎InitializeSuccess");
//listenoriginal生Recorddata
// NativeBridge.shared.recordChannel.receiveBroadcastStream().listen((data) {
// if (data is Uint8List) _processPcm(data);
// });
} catch (e) {
debugPrint(" InitializeFailed: $e");
rethrow;
}
}
//======================== 🔥 Core fix: Auto frame assembly ========================
void _processPcm(Uint8List pcm8) {
try {
final pcm16 = Int16List.view(pcm8.buffer);
//writeBuffer
for (int sample in pcm16) {
if (_bufferWriteIndex < _pcmBuffer.length) {
_pcmBuffer[_bufferWriteIndex++] = sample;
}
//Accumulate 320 samples → encode one frame
if (_bufferWriteIndex == frameSamples) {
final frame = _pcmBuffer.sublist(0, frameSamples);
final opusData = simpleOpusEncoder.encode(input: frame);
//output
if (onAudioData != null) onAudioData!(opusData);
if (onDecibel != null) onDecibel!(_getDecibel(frame));
//resetBuffer
_bufferWriteIndex = 0;
}
}
} catch (e) {
debugPrint(" EncodeFailed: $e");
}
}
//decibelcalculate
double _getDecibel(Int16List pcm) {
num sum = 0;
for (int s in pcm) {
sum += s * s;
}
final rms = sqrt(sum / pcm.length);
final db = 20 * log(rms / 32768.0) / ln10;
return db.isFinite ? db : -60;
}
//======================== play ========================
Future<void> playOpus(Uint8List opusData) async {
try {
if (!_isInitialized) return;
final pcm16 = simpleOpusDecoder.decode(input: opusData);
final byteData = ByteData(pcm16.length * 2);
for (int i = 0; i < pcm16.length; i++) {
byteData.setInt16(i * 2, pcm16[i], Endian.little);
}
NativeBridge.shared.sendAudioStream(byteData);
} catch (e) {
debugPrint(" PlayFailed: $e");
}
}
Future<void> stopPlayOpus() async {
NativeBridge.shared.sendMessage(.stopPlayPCM);
}
//====================== startRecord ======================
Future<bool> startRecording() async {
if (!_isInitialized) return false;
//requestMicrophonepermission
final perm = await Permission.microphone.request();
if (!perm.isGranted) {
debugPrint("MicrophonePermissionNot授权");
return false;
}
debugPrint("🎙️ RecordStart (实WhenOpusEncode Int16)");
NativeBridge.shared.sendMessage(.startRecording);
return true;
}
//====================== stopRecord ======================
Future<void> stopRecording() async {
NativeBridge.shared.sendMessage(.stopRecording);
debugPrint("🛑 RecordStop");
}
Future<void> dispose() async {
if (_isInitialized) {
simpleOpusEncoder.destroy();
simpleOpusDecoder.destroy();
_isInitialized = false;
}
debugPrint("♻️ Asset / Resource源ReleaseComplete / Done");
}
}