Use real PCM voice waveform
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m57s

This commit is contained in:
Ruslan Bakiev
2026-05-09 17:41:34 +07:00
parent f9d6e4fa5b
commit 6055a101e8
3 changed files with 137 additions and 66 deletions

View File

@@ -7,7 +7,6 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:latlong2/latlong.dart' hide Path; import 'package:latlong2/latlong.dart' hide Path;
import 'package:record/record.dart'; import 'package:record/record.dart';
import 'package:siri_wave/siri_wave.dart';
import '../api/mapflow_api.dart'; import '../api/mapflow_api.dart';
import '../auth/telegram_login_button.dart'; import '../auth/telegram_login_button.dart';
@@ -725,7 +724,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
final _api = MapflowApi(); final _api = MapflowApi();
final _recorder = AudioRecorder(); final _recorder = AudioRecorder();
final _waveSamples = List<double>.filled(64, 0.04); final _waveSamples = List<double>.filled(160, 0.0);
Future<List<PlaceRecommendation>>? _nearbyPlacesFuture; Future<List<PlaceRecommendation>>? _nearbyPlacesFuture;
Timer? _timer; Timer? _timer;
@@ -821,23 +820,35 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
} }
final bytes = ByteData.sublistView(chunk); final bytes = ByteData.sublistView(chunk);
var sum = 0.0; final sampleCount = chunk.length ~/ 2;
var count = 0; final bucketCount = 10.clamp(1, sampleCount);
for (var index = 0; index + 1 < chunk.length; index += 2) { final bucketSize = (sampleCount / bucketCount).ceil();
final sample = bytes.getInt16(index, Endian.little) / 32768.0; final peaks = <double>[];
sum += sample * sample; for (var bucket = 0; bucket < bucketCount; bucket++) {
count += 1; 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) { if (!mounted) {
return; return;
} }
setState(() { setState(() {
_waveSamples _waveSamples
..removeAt(0) ..removeRange(0, peaks.length)
..add(level); ..addAll(peaks);
}); });
} }
@@ -1279,7 +1290,7 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton>
} }
} }
class _VoiceWave extends StatefulWidget { class _VoiceWave extends StatelessWidget {
const _VoiceWave({ const _VoiceWave({
required this.samples, required this.samples,
required this.active, required this.active,
@@ -1290,42 +1301,6 @@ class _VoiceWave extends StatefulWidget {
final bool active; final bool active;
final double progress; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
@@ -1341,14 +1316,14 @@ class _VoiceWaveState extends State<_VoiceWave> {
BoxShadow( BoxShadow(
color: const Color( color: const Color(
0xFFFF2D75, 0xFFFF2D75,
).withValues(alpha: widget.active ? 0.18 : 0.08), ).withValues(alpha: active ? 0.18 : 0.08),
blurRadius: 90, blurRadius: 90,
spreadRadius: 18, spreadRadius: 18,
), ),
BoxShadow( BoxShadow(
color: const Color( color: const Color(
0xFF38F5D3, 0xFF38F5D3,
).withValues(alpha: widget.active ? 0.12 : 0.05), ).withValues(alpha: active ? 0.12 : 0.05),
blurRadius: 120, blurRadius: 120,
spreadRadius: 14, spreadRadius: 14,
), ),
@@ -1357,12 +1332,15 @@ class _VoiceWaveState extends State<_VoiceWave> {
), ),
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return SiriWaveform.ios9( return CustomPaint(
controller: _controller, size: Size(
options: IOS9SiriWaveformOptions( constraints.maxWidth,
width: constraints.maxWidth, constraints.maxHeight.clamp(280.0, 520.0),
height: constraints.maxHeight.clamp(280.0, 520.0), ),
showSupportBar: false, 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<double> 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 { class _StepLayout extends StatelessWidget {
const _StepLayout({required this.body, this.action}); const _StepLayout({required this.body, this.action});

View File

@@ -736,14 +736,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.4" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@@ -41,7 +41,6 @@ dependencies:
web: ^1.1.1 web: ^1.1.1
geolocator: ^14.0.2 geolocator: ^14.0.2
record: ^6.2.0 record: ^6.2.0
siri_wave: ^2.3.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: