From 6055a101e850d791aff2dbb13d1e64499f833895 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Sat, 9 May 2026 17:41:34 +0700 Subject: [PATCH] Use real PCM voice waveform --- lib/screens/mapflow_shell.dart | 194 +++++++++++++++++++++++---------- pubspec.lock | 8 -- pubspec.yaml | 1 - 3 files changed, 137 insertions(+), 66 deletions(-) diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index 308dab5..5c1ac09 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -7,7 +7,6 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart' hide Path; import 'package:record/record.dart'; -import 'package:siri_wave/siri_wave.dart'; import '../api/mapflow_api.dart'; import '../auth/telegram_login_button.dart'; @@ -725,7 +724,7 @@ class _AddExperienceFlowState extends ConsumerState { final _api = MapflowApi(); final _recorder = AudioRecorder(); - final _waveSamples = List.filled(64, 0.04); + final _waveSamples = List.filled(160, 0.0); Future>? _nearbyPlacesFuture; Timer? _timer; @@ -821,23 +820,35 @@ class _AddExperienceFlowState extends ConsumerState { } final bytes = ByteData.sublistView(chunk); - var sum = 0.0; - var count = 0; - for (var index = 0; index + 1 < chunk.length; index += 2) { - final sample = bytes.getInt16(index, Endian.little) / 32768.0; - sum += sample * sample; - count += 1; + final sampleCount = chunk.length ~/ 2; + final bucketCount = 10.clamp(1, sampleCount); + final bucketSize = (sampleCount / bucketCount).ceil(); + final peaks = []; + for (var bucket = 0; bucket < bucketCount; bucket++) { + final startSample = bucket * bucketSize; + final endSample = math.min(startSample + bucketSize, sampleCount); + var peak = 0.0; + for ( + var sampleIndex = startSample; + sampleIndex < endSample; + sampleIndex++ + ) { + final sample = bytes.getInt16(sampleIndex * 2, Endian.little) / 32768.0; + peak = math.max(peak, sample.abs()); + } + peaks.add((peak * 3.4).clamp(0.0, 1.0)); + } + if (peaks.isEmpty) { + return; } - final rms = count == 0 ? 0.0 : math.sqrt(sum / count); - final level = (rms * 7.5).clamp(0.03, 1.0).toDouble(); if (!mounted) { return; } setState(() { _waveSamples - ..removeAt(0) - ..add(level); + ..removeRange(0, peaks.length) + ..addAll(peaks); }); } @@ -1279,7 +1290,7 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton> } } -class _VoiceWave extends StatefulWidget { +class _VoiceWave extends StatelessWidget { const _VoiceWave({ required this.samples, required this.active, @@ -1290,42 +1301,6 @@ class _VoiceWave extends StatefulWidget { final bool active; final double progress; - @override - State<_VoiceWave> createState() => _VoiceWaveState(); -} - -class _VoiceWaveState extends State<_VoiceWave> { - late final IOS9SiriWaveformController _controller; - - @override - void initState() { - super.initState(); - _controller = IOS9SiriWaveformController( - amplitude: 0.12, - speed: 0.08, - color1: const Color(0xFFFF2D75), - color2: const Color(0xFF38F5D3), - color3: const Color(0xFF7C5CFF), - ); - _syncController(); - } - - @override - void didUpdateWidget(covariant _VoiceWave oldWidget) { - super.didUpdateWidget(oldWidget); - _syncController(); - } - - void _syncController() { - final lastLevel = widget.samples.isEmpty ? 0.04 : widget.samples.last; - _controller.amplitude = widget.active - ? (0.18 + lastLevel * 0.78).clamp(0.0, 1.0) - : (0.10 + widget.progress * 0.12).clamp(0.0, 1.0); - _controller.speed = widget.active - ? (0.10 + lastLevel * 0.36).clamp(0.0, 1.0) - : 0.035; - } - @override Widget build(BuildContext context) { return Stack( @@ -1341,14 +1316,14 @@ class _VoiceWaveState extends State<_VoiceWave> { BoxShadow( color: const Color( 0xFFFF2D75, - ).withValues(alpha: widget.active ? 0.18 : 0.08), + ).withValues(alpha: active ? 0.18 : 0.08), blurRadius: 90, spreadRadius: 18, ), BoxShadow( color: const Color( 0xFF38F5D3, - ).withValues(alpha: widget.active ? 0.12 : 0.05), + ).withValues(alpha: active ? 0.12 : 0.05), blurRadius: 120, spreadRadius: 14, ), @@ -1357,12 +1332,15 @@ class _VoiceWaveState extends State<_VoiceWave> { ), LayoutBuilder( builder: (context, constraints) { - return SiriWaveform.ios9( - controller: _controller, - options: IOS9SiriWaveformOptions( - width: constraints.maxWidth, - height: constraints.maxHeight.clamp(280.0, 520.0), - showSupportBar: false, + return CustomPaint( + size: Size( + constraints.maxWidth, + constraints.maxHeight.clamp(280.0, 520.0), + ), + painter: _VoiceWavePainter( + samples: samples, + active: active, + progress: progress, ), ); }, @@ -1372,6 +1350,108 @@ class _VoiceWaveState extends State<_VoiceWave> { } } +class _VoiceWavePainter extends CustomPainter { + const _VoiceWavePainter({ + required this.samples, + required this.active, + required this.progress, + }); + + final List samples; + final bool active; + final double progress; + + @override + void paint(Canvas canvas, Size size) { + final centerY = size.height / 2; + final widthStep = samples.length <= 1 + ? size.width + : size.width / (samples.length - 1); + final maxHeight = size.height * 0.46; + final idleHeight = maxHeight * (0.03 + progress * 0.06); + + final fillPath = Path(); + final topPath = Path(); + final bottomPath = Path(); + final denominator = math.max(samples.length - 1, 1); + + for (var index = 0; index < samples.length; index++) { + final x = index * widthStep; + final envelope = math.sin(index / denominator * math.pi); + final raw = samples[index].clamp(0.0, 1.0); + final height = active + ? math.max(raw * envelope * maxHeight, idleHeight) + : idleHeight * envelope; + final top = Offset(x, centerY - height); + final bottom = Offset(x, centerY + height); + + if (index == 0) { + topPath.moveTo(top.dx, top.dy); + bottomPath.moveTo(bottom.dx, bottom.dy); + fillPath.moveTo(top.dx, top.dy); + } else { + topPath.lineTo(top.dx, top.dy); + bottomPath.lineTo(bottom.dx, bottom.dy); + fillPath.lineTo(top.dx, top.dy); + } + } + + for (var index = samples.length - 1; index >= 0; index--) { + final x = index * widthStep; + final envelope = math.sin(index / denominator * math.pi); + final raw = samples[index].clamp(0.0, 1.0); + final height = active + ? math.max(raw * envelope * maxHeight, idleHeight) + : idleHeight * envelope; + fillPath.lineTo(x, centerY + height); + } + fillPath.close(); + + final glowPaint = Paint() + ..color = const Color(0xFFFF2D75).withValues(alpha: active ? 0.20 : 0.08) + ..strokeWidth = 16 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 14); + final strokePaint = Paint() + ..shader = const LinearGradient( + colors: [ + Color(0xFF38F5D3), + Color(0xFFFFFFFF), + Color(0xFFFF2D75), + Color(0xFF7C5CFF), + ], + ).createShader(Offset.zero & size) + ..strokeWidth = 4.8 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..style = PaintingStyle.stroke; + final fillPaint = Paint() + ..shader = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + const Color(0xFF38F5D3).withValues(alpha: active ? 0.12 : 0.04), + const Color(0xFFFF2D75).withValues(alpha: active ? 0.16 : 0.05), + ], + ).createShader(Offset.zero & size) + ..style = PaintingStyle.fill; + + canvas.drawPath(fillPath, fillPaint); + canvas.drawPath(topPath, glowPaint); + canvas.drawPath(bottomPath, glowPaint); + canvas.drawPath(topPath, strokePaint); + canvas.drawPath(bottomPath, strokePaint); + } + + @override + bool shouldRepaint(covariant _VoiceWavePainter oldDelegate) { + return oldDelegate.samples != samples || + oldDelegate.active != active || + oldDelegate.progress != progress; + } +} + class _StepLayout extends StatelessWidget { const _StepLayout({required this.body, this.action}); diff --git a/pubspec.lock b/pubspec.lock index 7b84a86..c5e2374 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -736,14 +736,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.4" - siri_wave: - dependency: "direct main" - description: - name: siri_wave - sha256: ea815d6627dc297f6be883bb0dd7a579a5f5f9729242d47c10e95850cccf169a - url: "https://pub.dev" - source: hosted - version: "2.3.1" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 644421b..8ec8757 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,6 @@ dependencies: web: ^1.1.1 geolocator: ^14.0.2 record: ^6.2.0 - siri_wave: ^2.3.1 dev_dependencies: flutter_test: