You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

190 lines
6.2 KiB

import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
import '../../core/utils/logger.dart';
/// 串行写入的 WAV 文件(16k / 16bit)。
///
/// ⚠️ **必须串行写**:Stream.listen 的回调即使标了 async 也不会被等待,
/// 音频每秒上百帧,多次 writeFrom 必然重叠;Dart 的 RandomAccessFile 同一时刻
/// 只允许一个异步操作,重叠调用会抛 "An async operation is currently pending",
/// 那一帧就丢了(实测等效采样率只剩 1587~5331Hz,播放又快又糊)。
/// 用 Future 链把写入排成队列:既不丢帧,也保证顺序。
class WavWriter {
WavWriter._(this._raf, this.path, this.channels, this.sampleRate);
static const String _tag = 'WavWriter';
final RandomAccessFile _raf;
final String path;
final int channels;
final int sampleRate;
int dataBytes = 0;
int writtenFrames = 0;
int droppedFrames = 0;
Future<void> _chain = Future<void>.value();
bool _closed = false;
static Future<WavWriter> open(String path,
{int channels = 1, int sampleRate = 16000}) async {
final raf = await File(path).open(mode: FileMode.write);
final w = WavWriter._(raf, path, channels, sampleRate);
await raf.writeFrom(w._header(0));
return w;
}
/// 入队写一块 PCM。
///
/// ⚠️ 这里刻意**没有**"写完回调"。它原本是给电平/波形用的,等于把 UI 挂在
/// 文件写入队列的尾巴上:队列一堵,界面跟着停;写失败时波形还会整段消失。
/// 显示所需的电平在调用方直接从这块 PCM 上算,与落盘无关。
void write(Uint8List pcm) {
if (_closed) return;
_chain = _chain.then((_) async {
if (_closed) return;
try {
await _raf.writeFrom(pcm);
dataBytes += pcm.length;
writtenFrames++;
} catch (e) {
droppedFrames++;
Logger.e(_tag, '写入 PCM 失败(已丢帧 $droppedFrames): $e');
}
});
}
/// 等队列里的帧全部落盘。
Future<void> drain() async {
try {
await _chain;
} catch (_) {}
}
/// 关闭;[save] = true 时回填 WAV 头的长度。
Future<void> close({required bool save}) async {
if (_closed) return;
await drain();
_closed = true;
try {
if (save) {
await _raf.setPosition(0);
await _raf.writeFrom(_header(dataBytes));
}
await _raf.close();
} catch (e) {
Logger.e(_tag, '关闭录音文件失败: $e');
}
}
Uint8List _header(int dataLength) {
const bits = 16;
final byteRate = sampleRate * channels * (bits ~/ 8);
final h = ByteData(44);
void ascii(int off, String s) {
for (var i = 0; i < s.length; i++) {
h.setUint8(off + i, s.codeUnitAt(i));
}
}
ascii(0, 'RIFF');
h.setUint32(4, 36 + dataLength, Endian.little);
ascii(8, 'WAVE');
ascii(12, 'fmt ');
h.setUint32(16, 16, Endian.little);
h.setUint16(20, 1, Endian.little);
h.setUint16(22, channels, Endian.little);
h.setUint32(24, sampleRate, Endian.little);
h.setUint32(28, byteRate, Endian.little);
h.setUint16(32, channels * (bits ~/ 8), Endian.little);
h.setUint16(34, bits, Endian.little);
ascii(36, 'data');
h.setUint32(40, dataLength, Endian.little);
return h.buffer.asUint8List();
}
/// PCM16 小端的 RMS 电平(0~1,开过根让波形更有跳动感)。
///
/// ⚠️ 不能用 `buffer.asInt16List(offsetInBytes, …)`:原生过来的 Uint8List 是
/// 带 offset 的视图,偏移是奇数时直接抛 RangeError。按字节手工拼。
static double rmsLevel(Uint8List pcm) {
final count = pcm.lengthInBytes ~/ 2;
if (count == 0) return 0;
var sum = 0.0;
for (var i = 0; i < count; i++) {
var v = (pcm[i * 2 + 1] << 8) | pcm[i * 2];
if (v >= 0x8000) v -= 0x10000;
final n = v / 32768.0;
sum += n * n;
}
final rms = math.sqrt(sum / count);
return math.sqrt(rms).clamp(0.0, 1.0);
}
/// 把两路单声道按字节交织成双声道(左=[left],右=[right])。
static Uint8List interleave(Uint8List left, Uint8List right) {
final samples = (left.length < right.length ? left.length : right.length) ~/ 2;
final out = Uint8List(samples * 4);
for (var i = 0; i < samples; i++) {
final si = i * 2, di = i * 4;
out[di] = left[si];
out[di + 1] = left[si + 1];
out[di + 2] = right[si];
out[di + 3] = right[si + 1];
}
return out;
}
}
/// 一对已对齐的左右声道 PCM(等长、偶数字节)。
class StereoPair {
StereoPair(this.left, this.right);
final Uint8List left;
final Uint8List right;
}
/// 把两路各自到达的 PCM 按字节对齐成对:两路都排队,谁都不丢,
/// 每次取出两边都已有的那一段(长度取较短者、对齐到整样本)。
///
/// 两路只在**开头**可能不齐(先到的一路先攒着),之后按各自到达速率自然对齐;
/// 一路长时间缺席时另一路会一直攒着(上限 [maxPending],超过就丢最旧的,
/// 避免对端根本没数据时本端无限堆积)。
class StereoPairer {
StereoPairer({this.maxPending = 16000 * 2 * 10}); // 10 秒 @16k/16bit
final int maxPending;
final BytesBuilder _left = BytesBuilder(copy: false);
final BytesBuilder _right = BytesBuilder(copy: false);
void pushLeft(Uint8List pcm) => _push(_left, pcm);
void pushRight(Uint8List pcm) => _push(_right, pcm);
void _push(BytesBuilder b, Uint8List pcm) {
b.add(pcm);
if (b.length > maxPending) {
final all = b.takeBytes();
b.add(Uint8List.sublistView(all, all.length - maxPending));
}
}
/// 两边都有数据才返回;否则 null。
StereoPair? take() {
final n = (_left.length < _right.length ? _left.length : _right.length) & ~1;
if (n <= 0) return null;
final l = _left.takeBytes();
final r = _right.takeBytes();
final pair = StereoPair(
Uint8List.sublistView(l, 0, n), Uint8List.sublistView(r, 0, n));
if (l.length > n) _left.add(Uint8List.sublistView(l, n));
if (r.length > n) _right.add(Uint8List.sublistView(r, n));
return pair;
}
void clear() {
_left.clear();
_right.clear();
}
}