import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_sound/flutter_sound.dart'; /// 麦克风输入可视化 class MicrophoneWave extends StatefulWidget { final FlutterSoundRecorder recorder; final VoidCallback onAutoDetectPass; final double width; final double height; final bool isRecording; MicrophoneWave({ required this.recorder, required this.isRecording, required this.onAutoDetectPass, this.width = 250.0, this.height = 40.0, }); @override _MicrophoneWaveState createState() => _MicrophoneWaveState(); } class _MicrophoneWaveState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; StreamSubscription? _recorderSubscription; List _loudnessValues = []; int autoDetectCount = 0; static const int autoDetectThreshold = 10; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: Duration(milliseconds: 50), ); _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) ..addListener(() { setState(() {}); }); _recorderSubscription = widget.recorder.onProgress!.listen((RecordingDisposition e) { /// 输出响度,响度范围是0-120 /// 0表示没有声音,120表示最大声音 final loudness = ((e.decibels ?? 0) + 5) / 120; if (loudness > 0.2) { autoDetectCount++; } if (autoDetectCount > autoDetectThreshold) { autoDetectCount = 0; widget.onAutoDetectPass.call(); } setState(() { _loudnessValues.add(loudness); }); if (_loudnessValues.length > widget.width) { _loudnessValues.removeAt(0); } _controller.forward(from: 0.0); }, onError: (err) { print('onError: $err'); }); } @override void didUpdateWidget(covariant MicrophoneWave oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.isRecording != widget.isRecording) { if (widget.isRecording) { _loudnessValues.clear(); } } } @override void dispose() { super.dispose(); _controller.dispose(); _recorderSubscription?.cancel(); _recorderSubscription = null; } @override Widget build(BuildContext context) { return Container( width: widget.width, height: widget.height, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( "麦克风输入:", style: TextStyle( height: 1, color: Colors.black87, ), ), Expanded( child: ClipPath( child: CustomPaint( painter: _MicrophoneVisualizerPainter( loudnessList: _loudnessValues, animationValue: _animation.value, isRecording: widget.isRecording, ), size: Size(widget.width, widget.height), ), )), ], ), ); } } class _MicrophoneVisualizerPainter extends CustomPainter { final List loudnessList; final double animationValue; final bool isRecording; _MicrophoneVisualizerPainter({ required this.loudnessList, required this.animationValue, this.isRecording = false, }); @override void paint(Canvas canvas, Size size) { final double sampleStep = 5.0; final offsetX = sampleStep * animationValue; final double centerY = size.height / 2; Paint linePaint = Paint() ..color = Colors.grey ..strokeWidth = 1.0; /// 绘制中轴线 canvas.drawLine(Offset(0, centerY), Offset(size.width, centerY), linePaint); if (!isRecording) return; linePaint = Paint() ..color = Colors.blue ..strokeWidth = 2.0; for (int i = 0; i < loudnessList.length; i++) { final loudness = loudnessList[loudnessList.length - i - 1]; final x = i * sampleStep + offsetX; final y = centerY + loudness * centerY; final y2 = centerY - loudness * centerY; canvas.drawLine(Offset(x, y), Offset(x, y2), linePaint); } } @override bool shouldRepaint(_MicrophoneVisualizerPainter oldDelegate) { return oldDelegate.animationValue != animationValue; } }