Use recorder amplitude for web voice meter
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m53s

This commit is contained in:
Ruslan Bakiev
2026-05-09 23:08:30 +07:00
parent 2c9bcad0cc
commit 906c23366f

View File

@@ -730,6 +730,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
Future<List<PlaceRecommendation>>? _nearbyPlacesFuture;
Timer? _timer;
Timer? _visualTimer;
Timer? _amplitudeTimer;
StreamSubscription<Uint8List>? _audioStreamSub;
var _step = 0;
var _seconds = 0;
@@ -741,6 +742,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
var _voiceCeiling = 0.045;
var _visualPhase = 0.0;
DateTime? _lastAudioChunkAt;
DateTime? _lastInformationAt;
@override
void initState() {
@@ -751,6 +753,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
void dispose() {
_timer?.cancel();
_visualTimer?.cancel();
_amplitudeTimer?.cancel();
_audioStreamSub?.cancel();
_recorder.dispose();
super.dispose();
@@ -796,7 +799,9 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
);
_audioStreamSub = stream.listen(_handleAudioChunk);
_lastAudioChunkAt = DateTime.now();
_lastInformationAt = _lastAudioChunkAt;
_startIdleWave();
_startAmplitudePolling();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) {
return;
@@ -816,10 +821,12 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
Future<void> _stopRecording() async {
_timer?.cancel();
_visualTimer?.cancel();
_amplitudeTimer?.cancel();
await _audioStreamSub?.cancel();
_audioStreamSub = null;
await _recorder.stop();
_lastAudioChunkAt = null;
_lastInformationAt = null;
if (!mounted) {
return;
}
@@ -855,6 +862,57 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
});
}
void _startAmplitudePolling() {
_amplitudeTimer?.cancel();
_amplitudeTimer = Timer.periodic(const Duration(milliseconds: 90), (
_,
) async {
if (!mounted || !_recording) {
return;
}
final amplitude = await _recorder.getAmplitude();
if (!mounted || !_recording) {
return;
}
_handleAmplitude(amplitude.current);
});
}
void _handleAmplitude(double currentDb) {
final now = DateTime.now();
final normalized = ((currentDb + 54) / 54).clamp(0.0, 1.0);
final voicedAmount = math.pow(normalized, 0.72).toDouble();
final informationDelta = _consumeInformationDelta(voicedAmount, now);
_visualPhase += 0.38;
final samples = List<double>.generate(8, (index) {
final wave = math.sin(_visualPhase + index * 0.68) * 0.5 + 0.5;
return (0.10 + voicedAmount * 0.68 + wave * voicedAmount * 0.22).clamp(
0.0,
1.0,
);
});
setState(() {
_waveSamples
..removeRange(0, samples.length)
..addAll(samples);
_informationUnits = math.min(
_minimumInformationUnits,
_informationUnits + informationDelta,
);
});
}
double _consumeInformationDelta(double voicedAmount, DateTime now) {
final previous = _lastInformationAt ?? now;
_lastInformationAt = now;
final deltaSeconds =
now.difference(previous).inMilliseconds.clamp(20, 180) / 1000;
return voicedAmount.clamp(0.0, 1.0) * deltaSeconds;
}
void _handleAudioChunk(Uint8List chunk) {
if (chunk.length < 2) {
return;
@@ -896,8 +954,6 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
final now = DateTime.now();
final previousChunkAt = _lastAudioChunkAt ?? now;
_lastAudioChunkAt = now;
final deltaSeconds =
now.difference(previousChunkAt).inMilliseconds.clamp(20, 300) / 1000;
final currentRms = totalRms / peaks.length;
_ambientLevel = currentRms < _ambientLevel
? (_ambientLevel * 0.86 + currentRms * 0.14)
@@ -913,7 +969,11 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
final voicedAmount =
normalizedPeaks.fold<double>(0, (sum, value) => sum + value) /
normalizedPeaks.length;
final informationDelta = voicedAmount * deltaSeconds;
final hasRecentAmplitude =
now.difference(previousChunkAt).inMilliseconds < 120;
final informationDelta = hasRecentAmplitude
? 0.0
: _consumeInformationDelta(voicedAmount, now);
if (!mounted) {
return;
@@ -1465,6 +1525,24 @@ class _VoiceInformationPainter extends CustomPainter {
canvas.drawRRect(rect, backgroundPaint);
canvas.drawRRect(rect, borderPaint);
final centerDistance = (row - (rows - 1) / 2).abs();
final waveReach = 0.55 + signal * rows * 0.52;
final inWave = centerDistance <= waveReach;
if (inWave) {
final waveAlpha = active
? 0.14 + signal * 0.26
: 0.07 + signal * 0.08;
final wavePaint = Paint()
..shader = LinearGradient(
colors: [
const Color(0xFF38F5D3).withValues(alpha: waveAlpha),
const Color(0xFFFF2D75).withValues(alpha: waveAlpha * 0.92),
],
).createShader(rect.outerRect)
..style = PaintingStyle.fill;
canvas.drawRRect(rect, wavePaint);
}
if (filled) {
final warmth = (0.38 + signal * 0.62).clamp(0.0, 1.0);
final paint = Paint()