Use recorder amplitude for web voice meter
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m53s
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m53s
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user