Use real PCM voice waveform
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m57s
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m57s
This commit is contained in:
@@ -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<AddExperienceFlow> {
|
||||
|
||||
final _api = MapflowApi();
|
||||
final _recorder = AudioRecorder();
|
||||
final _waveSamples = List<double>.filled(64, 0.04);
|
||||
final _waveSamples = List<double>.filled(160, 0.0);
|
||||
|
||||
Future<List<PlaceRecommendation>>? _nearbyPlacesFuture;
|
||||
Timer? _timer;
|
||||
@@ -821,23 +820,35 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
}
|
||||
|
||||
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 = <double>[];
|
||||
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<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 {
|
||||
const _StepLayout({required this.body, this.action});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user