This repository has been archived on 2025-08-26. You can view files and clone it, but cannot push or open issues or pull requests.
Files
enginex-mr_series-sherpa-onnx/sherpa-onnx/flutter/example/lib/vad.dart

256 lines
6.6 KiB
Dart
Raw Normal View History

// Copyright (c) 2024 Xiaomi Corporation
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
import 'package:sherpa_onnx/sherpa_onnx.dart' as sherpa_onnx;
import './utils.dart';
2024-05-30 12:21:09 +08:00
class VadScreen extends StatefulWidget {
const VadScreen({super.key});
@override
2024-05-30 12:21:09 +08:00
State<VadScreen> createState() => _VadScreenState();
}
2024-05-30 12:21:09 +08:00
class _VadScreenState extends State<VadScreen> {
late final AudioRecorder _audioRecorder;
bool _printed = false;
var _color = Colors.black;
bool _isInitialized = false;
sherpa_onnx.VoiceActivityDetector? _vad;
sherpa_onnx.CircularBuffer? _buffer;
StreamSubscription<RecordState>? _recordSub;
RecordState _recordState = RecordState.stop;
@override
void initState() {
_audioRecorder = AudioRecorder();
_recordSub = _audioRecorder.onStateChanged().listen((recordState) {
_updateRecordState(recordState);
});
super.initState();
}
Future<void> _start() async {
if (!_isInitialized) {
sherpa_onnx.initBindings();
final src = 'assets/silero_vad.onnx';
final modelPath = await copyAssetFile(src: src, dst: 'silero_vad.onnx');
final sileroVadConfig = sherpa_onnx.SileroVadModelConfig(
model: modelPath,
minSpeechDuration: 0.25,
minSilenceDuration: 0.5,
);
final config = sherpa_onnx.VadModelConfig(
sileroVad: sileroVadConfig,
numThreads: 1,
debug: true,
);
_vad = sherpa_onnx.VoiceActivityDetector(
config: config, bufferSizeInSeconds: 30);
_buffer = sherpa_onnx.CircularBuffer(capacity: 16000 * 30);
print(_buffer!.ptr);
_isInitialized = true;
}
try {
if (await _audioRecorder.hasPermission()) {
const encoder = AudioEncoder.pcm16bits;
if (!await _isEncoderSupported(encoder)) {
return;
}
final devs = await _audioRecorder.listInputDevices();
debugPrint(devs.toString());
const config = RecordConfig(
encoder: encoder,
sampleRate: 16000,
numChannels: 1,
);
final stream = await _audioRecorder.startStream(config);
final dir = await getApplicationDocumentsDirectory();
stream.listen(
(data) {
final samplesFloat32 =
convertBytesToFloat32(Uint8List.fromList(data));
_buffer!.push(samplesFloat32);
final windowSize = _vad!.config.sileroVad.windowSize;
while (_buffer!.size > windowSize) {
final samples =
_buffer!.get(startIndex: _buffer!.head, n: windowSize);
_buffer!.pop(windowSize);
_vad!.acceptWaveform(samples);
if (_vad!.isDetected() && !_printed) {
print('detected');
_printed = true;
setState(() => _color = Colors.red);
}
if (!_vad!.isDetected()) {
_printed = false;
setState(() => _color = Colors.black);
}
while (!_vad!.isEmpty()) {
final segment = _vad!.front();
final duration = segment.samples.length / 16000;
final d = DateTime.now();
final filename = p.join(dir.path,
'${d.year}-${d.month}-${d.day}-${d.hour}-${d.minute}-${d.second}-duration-${duration.toStringAsPrecision(3)}s.wav');
bool ok = sherpa_onnx.writeWave(
filename: filename,
samples: segment.samples,
sampleRate: 16000);
if (!ok) {
print('Failed to write $filename');
} else {
print('Saved to write $filename');
}
_vad!.pop();
}
}
},
onDone: () {
print('stream stopped.');
},
);
}
} catch (e) {
print(e);
}
}
Future<void> _stop() async {
_buffer!.reset();
_vad!.clear();
await _audioRecorder.stop();
}
Future<void> _pause() => _audioRecorder.pause();
Future<void> _resume() => _audioRecorder.resume();
void _updateRecordState(RecordState recordState) {
setState(() => _recordState = recordState);
}
Future<bool> _isEncoderSupported(AudioEncoder encoder) async {
final isSupported = await _audioRecorder.isEncoderSupported(
encoder,
);
if (!isSupported) {
debugPrint('${encoder.name} is not supported on this platform.');
debugPrint('Supported encoders are:');
for (final e in AudioEncoder.values) {
if (await _audioRecorder.isEncoderSupported(e)) {
debugPrint('- ${encoder.name}');
}
}
}
return isSupported;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _color,
),
),
const SizedBox(height: 50),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildRecordStopControl(),
const SizedBox(width: 20),
_buildText(),
],
),
],
),
),
);
}
@override
void dispose() {
_recordSub?.cancel();
_audioRecorder.dispose();
_vad?.free();
_buffer?.free();
super.dispose();
}
Widget _buildRecordStopControl() {
late Icon icon;
late Color color;
if (_recordState != RecordState.stop) {
icon = const Icon(Icons.stop, color: Colors.red, size: 30);
color = Colors.red.withOpacity(0.1);
} else {
final theme = Theme.of(context);
icon = Icon(Icons.mic, color: theme.primaryColor, size: 30);
color = theme.primaryColor.withOpacity(0.1);
}
return ClipOval(
child: Material(
color: color,
child: InkWell(
child: SizedBox(width: 56, height: 56, child: icon),
onTap: () {
(_recordState != RecordState.stop) ? _stop() : _start();
},
),
),
);
}
Widget _buildText() {
if (_recordState == RecordState.stop) {
return const Text("Start");
} else {
return const Text("Stop");
}
}
}