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: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});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user