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;
|
Future<List<PlaceRecommendation>>? _nearbyPlacesFuture;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
Timer? _visualTimer;
|
Timer? _visualTimer;
|
||||||
|
Timer? _amplitudeTimer;
|
||||||
StreamSubscription<Uint8List>? _audioStreamSub;
|
StreamSubscription<Uint8List>? _audioStreamSub;
|
||||||
var _step = 0;
|
var _step = 0;
|
||||||
var _seconds = 0;
|
var _seconds = 0;
|
||||||
@@ -741,6 +742,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
|||||||
var _voiceCeiling = 0.045;
|
var _voiceCeiling = 0.045;
|
||||||
var _visualPhase = 0.0;
|
var _visualPhase = 0.0;
|
||||||
DateTime? _lastAudioChunkAt;
|
DateTime? _lastAudioChunkAt;
|
||||||
|
DateTime? _lastInformationAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -751,6 +753,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_visualTimer?.cancel();
|
_visualTimer?.cancel();
|
||||||
|
_amplitudeTimer?.cancel();
|
||||||
_audioStreamSub?.cancel();
|
_audioStreamSub?.cancel();
|
||||||
_recorder.dispose();
|
_recorder.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -796,7 +799,9 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
|||||||
);
|
);
|
||||||
_audioStreamSub = stream.listen(_handleAudioChunk);
|
_audioStreamSub = stream.listen(_handleAudioChunk);
|
||||||
_lastAudioChunkAt = DateTime.now();
|
_lastAudioChunkAt = DateTime.now();
|
||||||
|
_lastInformationAt = _lastAudioChunkAt;
|
||||||
_startIdleWave();
|
_startIdleWave();
|
||||||
|
_startAmplitudePolling();
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -816,10 +821,12 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
|||||||
Future<void> _stopRecording() async {
|
Future<void> _stopRecording() async {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_visualTimer?.cancel();
|
_visualTimer?.cancel();
|
||||||
|
_amplitudeTimer?.cancel();
|
||||||
await _audioStreamSub?.cancel();
|
await _audioStreamSub?.cancel();
|
||||||
_audioStreamSub = null;
|
_audioStreamSub = null;
|
||||||
await _recorder.stop();
|
await _recorder.stop();
|
||||||
_lastAudioChunkAt = null;
|
_lastAudioChunkAt = null;
|
||||||
|
_lastInformationAt = null;
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
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) {
|
void _handleAudioChunk(Uint8List chunk) {
|
||||||
if (chunk.length < 2) {
|
if (chunk.length < 2) {
|
||||||
return;
|
return;
|
||||||
@@ -896,8 +954,6 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
|||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final previousChunkAt = _lastAudioChunkAt ?? now;
|
final previousChunkAt = _lastAudioChunkAt ?? now;
|
||||||
_lastAudioChunkAt = now;
|
_lastAudioChunkAt = now;
|
||||||
final deltaSeconds =
|
|
||||||
now.difference(previousChunkAt).inMilliseconds.clamp(20, 300) / 1000;
|
|
||||||
final currentRms = totalRms / peaks.length;
|
final currentRms = totalRms / peaks.length;
|
||||||
_ambientLevel = currentRms < _ambientLevel
|
_ambientLevel = currentRms < _ambientLevel
|
||||||
? (_ambientLevel * 0.86 + currentRms * 0.14)
|
? (_ambientLevel * 0.86 + currentRms * 0.14)
|
||||||
@@ -913,7 +969,11 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
|||||||
final voicedAmount =
|
final voicedAmount =
|
||||||
normalizedPeaks.fold<double>(0, (sum, value) => sum + value) /
|
normalizedPeaks.fold<double>(0, (sum, value) => sum + value) /
|
||||||
normalizedPeaks.length;
|
normalizedPeaks.length;
|
||||||
final informationDelta = voicedAmount * deltaSeconds;
|
final hasRecentAmplitude =
|
||||||
|
now.difference(previousChunkAt).inMilliseconds < 120;
|
||||||
|
final informationDelta = hasRecentAmplitude
|
||||||
|
? 0.0
|
||||||
|
: _consumeInformationDelta(voicedAmount, now);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -1465,6 +1525,24 @@ class _VoiceInformationPainter extends CustomPainter {
|
|||||||
canvas.drawRRect(rect, backgroundPaint);
|
canvas.drawRRect(rect, backgroundPaint);
|
||||||
canvas.drawRRect(rect, borderPaint);
|
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) {
|
if (filled) {
|
||||||
final warmth = (0.38 + signal * 0.62).clamp(0.0, 1.0);
|
final warmth = (0.38 + signal * 0.62).clamp(0.0, 1.0);
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
|
|||||||
Reference in New Issue
Block a user