Fix adaptive voice information meter
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m41s
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m41s
This commit is contained in:
@@ -719,15 +719,17 @@ class AddExperienceFlow extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
static const _minimumInformationUnits = 18.0;
|
||||
static const _minimumInformationUnits = 9.0;
|
||||
static const _nearbyPlaceRadiusMeters = 200;
|
||||
|
||||
final _api = MapflowApi();
|
||||
final _recorder = AudioRecorder();
|
||||
final _waveSamples = List<double>.filled(160, 0.0);
|
||||
final _visualRandom = math.Random();
|
||||
|
||||
Future<List<PlaceRecommendation>>? _nearbyPlacesFuture;
|
||||
Timer? _timer;
|
||||
Timer? _visualTimer;
|
||||
StreamSubscription<Uint8List>? _audioStreamSub;
|
||||
var _step = 0;
|
||||
var _seconds = 0;
|
||||
@@ -735,6 +737,9 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
var _recording = false;
|
||||
var _submitting = false;
|
||||
var _micAllowed = true;
|
||||
var _ambientLevel = 0.008;
|
||||
var _voiceCeiling = 0.045;
|
||||
var _visualPhase = 0.0;
|
||||
DateTime? _lastAudioChunkAt;
|
||||
|
||||
@override
|
||||
@@ -745,6 +750,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_visualTimer?.cancel();
|
||||
_audioStreamSub?.cancel();
|
||||
_recorder.dispose();
|
||||
super.dispose();
|
||||
@@ -790,6 +796,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
);
|
||||
_audioStreamSub = stream.listen(_handleAudioChunk);
|
||||
_lastAudioChunkAt = DateTime.now();
|
||||
_startIdleWave();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@@ -808,6 +815,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
|
||||
Future<void> _stopRecording() async {
|
||||
_timer?.cancel();
|
||||
_visualTimer?.cancel();
|
||||
await _audioStreamSub?.cancel();
|
||||
_audioStreamSub = null;
|
||||
await _recorder.stop();
|
||||
@@ -818,6 +826,35 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
setState(() => _recording = false);
|
||||
}
|
||||
|
||||
void _startIdleWave() {
|
||||
_visualTimer?.cancel();
|
||||
_visualTimer = Timer.periodic(const Duration(milliseconds: 70), (_) {
|
||||
if (!mounted || !_recording) {
|
||||
return;
|
||||
}
|
||||
|
||||
final lastSignalAt = _lastAudioChunkAt;
|
||||
final hasFreshSignal =
|
||||
lastSignalAt != null &&
|
||||
DateTime.now().difference(lastSignalAt).inMilliseconds < 220;
|
||||
if (hasFreshSignal) {
|
||||
return;
|
||||
}
|
||||
|
||||
_visualPhase += 0.24;
|
||||
final idleSamples = List<double>.generate(8, (index) {
|
||||
final wave = math.sin(_visualPhase + index * 0.72) * 0.5 + 0.5;
|
||||
final noise = _visualRandom.nextDouble() * 0.16;
|
||||
return (0.10 + wave * 0.18 + noise).clamp(0.0, 1.0);
|
||||
});
|
||||
setState(() {
|
||||
_waveSamples
|
||||
..removeRange(0, idleSamples.length)
|
||||
..addAll(idleSamples);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _handleAudioChunk(Uint8List chunk) {
|
||||
if (chunk.length < 2) {
|
||||
return;
|
||||
@@ -825,22 +862,32 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
|
||||
final bytes = ByteData.sublistView(chunk);
|
||||
final sampleCount = chunk.length ~/ 2;
|
||||
final bucketCount = 10.clamp(1, sampleCount);
|
||||
final bucketCount = 12.clamp(1, sampleCount);
|
||||
final bucketSize = (sampleCount / bucketCount).ceil();
|
||||
final peaks = <double>[];
|
||||
var totalRms = 0.0;
|
||||
for (var bucket = 0; bucket < bucketCount; bucket++) {
|
||||
final startSample = bucket * bucketSize;
|
||||
final endSample = math.min(startSample + bucketSize, sampleCount);
|
||||
var peak = 0.0;
|
||||
var sumSquares = 0.0;
|
||||
var bucketSamples = 0;
|
||||
for (
|
||||
var sampleIndex = startSample;
|
||||
sampleIndex < endSample;
|
||||
sampleIndex++
|
||||
) {
|
||||
final sample = bytes.getInt16(sampleIndex * 2, Endian.little) / 32768.0;
|
||||
peak = math.max(peak, sample.abs());
|
||||
final absoluteSample = sample.abs();
|
||||
peak = math.max(peak, absoluteSample);
|
||||
sumSquares += absoluteSample * absoluteSample;
|
||||
bucketSamples += 1;
|
||||
}
|
||||
peaks.add((peak * 3.4).clamp(0.0, 1.0));
|
||||
final rms = bucketSamples == 0
|
||||
? 0.0
|
||||
: math.sqrt(sumSquares / bucketSamples);
|
||||
totalRms += rms;
|
||||
peaks.add(math.max(peak, rms * 2.6));
|
||||
}
|
||||
if (peaks.isEmpty) {
|
||||
return;
|
||||
@@ -851,15 +898,21 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
_lastAudioChunkAt = now;
|
||||
final deltaSeconds =
|
||||
now.difference(previousChunkAt).inMilliseconds.clamp(20, 300) / 1000;
|
||||
const noiseFloor = 0.08;
|
||||
final currentRms = totalRms / peaks.length;
|
||||
_ambientLevel = currentRms < _ambientLevel
|
||||
? (_ambientLevel * 0.86 + currentRms * 0.14)
|
||||
: (_ambientLevel * 0.985 + currentRms * 0.015);
|
||||
_voiceCeiling = currentRms > _voiceCeiling
|
||||
? (_voiceCeiling * 0.68 + currentRms * 0.32)
|
||||
: math.max(_ambientLevel + 0.018, _voiceCeiling * 0.992);
|
||||
final dynamicRange = math.max(0.012, _voiceCeiling - _ambientLevel);
|
||||
final normalizedPeaks = peaks
|
||||
.map((peak) => ((peak - _ambientLevel) / dynamicRange).clamp(0.0, 1.0))
|
||||
.map((peak) => math.pow(peak, 0.62).toDouble())
|
||||
.toList();
|
||||
final voicedAmount =
|
||||
peaks
|
||||
.map(
|
||||
(peak) =>
|
||||
((peak - noiseFloor) / (1 - noiseFloor)).clamp(0.0, 1.0),
|
||||
)
|
||||
.fold<double>(0, (sum, value) => sum + value) /
|
||||
peaks.length;
|
||||
normalizedPeaks.fold<double>(0, (sum, value) => sum + value) /
|
||||
normalizedPeaks.length;
|
||||
final informationDelta = voicedAmount * deltaSeconds;
|
||||
|
||||
if (!mounted) {
|
||||
@@ -867,8 +920,13 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
}
|
||||
setState(() {
|
||||
_waveSamples
|
||||
..removeRange(0, peaks.length)
|
||||
..addAll(peaks);
|
||||
..removeRange(0, normalizedPeaks.length)
|
||||
..addAll(
|
||||
normalizedPeaks.map((peak) {
|
||||
final visibleSignal = 0.12 + peak * 0.88;
|
||||
return visibleSignal.clamp(0.0, 1.0);
|
||||
}),
|
||||
);
|
||||
_informationUnits = math.min(
|
||||
_minimumInformationUnits,
|
||||
_informationUnits + informationDelta,
|
||||
|
||||
Reference in New Issue
Block a user