diff --git a/lib/providers/voice_provider.dart b/lib/providers/voice_provider.dart index 6c533b4..1887ae1 100644 --- a/lib/providers/voice_provider.dart +++ b/lib/providers/voice_provider.dart @@ -99,10 +99,23 @@ class VoiceNotifier extends Notifier { StreamSubscription? _amplitudeSubscription; // Recording / silence detection + // + // Silence threshold is dynamic: we track the session peak dBFS and treat + // "silent" as "current level is at least _dropFromPeakDb below peak." + // This auto-calibrates to whatever mic + room the user is on rather than + // assuming a fixed ambient level. Until the peak climbs above + // _dynamicArmDb we fall back to a conservative static threshold so a + // quiet room doesn't spin forever. A grace period at the start of the + // recording gives the user time to begin speaking before silence checks + // arm. int _recordingStartMs = 0; int _silenceMs = 0; - static const _silenceThresholdDb = -40.0; - static const _silenceDurationMs = 1500; + double _peakDb = -100.0; + static const _fallbackThresholdDb = -35.0; + static const _dropFromPeakDb = 15.0; + static const _dynamicArmDb = -20.0; + static const _graceMs = 1500; + static const _silenceDurationMs = 2000; static const _minRecordingMs = 300; // Voice mode callbacks @@ -236,6 +249,7 @@ class VoiceNotifier extends Notifier { if (!state.voiceModeActive) return; _silenceMs = 0; + _peakDb = -100.0; _recordingStartMs = DateTime.now().millisecondsSinceEpoch; state = state.copyWith(mode: VoiceMode.recording); @@ -285,13 +299,22 @@ class VoiceNotifier extends Notifier { if ((norm - state.amplitude).abs() > 0.02) { state = state.copyWith(amplitude: norm); } + if (db > _peakDb) _peakDb = db; } final elapsed = DateTime.now().millisecondsSinceEpoch - _recordingStartMs; + // Grace period at the start — let the user begin speaking before we + // start silence-counting. Also suppresses any false triggers while + // the native recorder is still warming up. + if (elapsed < _graceMs) return; if (elapsed < _minRecordingMs) return; - final isSilent = !validDb || db < _silenceThresholdDb; + // Dynamic threshold once we've seen real speech; static fallback + // before that so a dead-silent session doesn't spin forever. + final threshold = + _peakDb > _dynamicArmDb ? _peakDb - _dropFromPeakDb : _fallbackThresholdDb; + final isSilent = !validDb || db < threshold; if (isSilent) { _silenceMs += 200;