start method
Implementation
Future<void> start({
required void Function(Float32List chunk, bool isFinal) onChunk,
}) async {
await stop();
_recorder = AudioRecorder();
final recordStream = await _recorder!.startStream(
// We explicitly disable ALL hardware processing to ensure the "ه"
// breathy sound isn't filtered out as background noise by Android.
const RecordConfig(
encoder: AudioEncoder.pcm16bits,
sampleRate: recordSampleRate, // 16000
numChannels: numChannels,
autoGain: false,
echoCancel: false,
noiseSuppress: false,
),
);
_subscription = recordStream.listen((Uint8List rawData) {
// Ensure 16-bit alignment for downstream operations
if (rawData.offsetInBytes % 2 != 0) {
rawData = Uint8List.fromList(rawData);
}
Uint8List allBytes;
if (_frameBuffer.isEmpty) {
allBytes = rawData;
} else {
allBytes = Uint8List(_frameBuffer.length + rawData.length);
allBytes.setAll(0, _frameBuffer);
allBytes.setAll(_frameBuffer.length, rawData);
}
int offset = 0;
// Process in exact 480ms blocks
while (allBytes.length - offset >= recordChunkBytes) {
final byteView = Uint8List.view(
allBytes.buffer,
allBytes.offsetInBytes + offset,
recordChunkBytes,
);
offset += recordChunkBytes;
final int16samples = Int16List.view(
byteView.buffer,
byteView.offsetInBytes,
recordChunkBytes ~/ bytesPerSample,
);
final float32Samples = Float32List(int16samples.length);
// ── Direct Linear Conversion ──
// Exactly matches the Python training pipeline: `wav.astype(np.float32) / 32768.0`
for (int i = 0; i < int16samples.length; i++) {
float32Samples[i] = int16samples[i] / 32768.0;
}
// Stream all audio directly to Sherpa ASR!
onChunk(float32Samples, false);
}
// Keep the remainder for the next stream event
if (offset < allBytes.length) {
_frameBuffer = Uint8List.fromList(
Uint8List.view(allBytes.buffer, allBytes.offsetInBytes + offset),
);
} else {
_frameBuffer = Uint8List(0);
}
});
}