Make voice grid visibly animate
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m6s

This commit is contained in:
Ruslan Bakiev
2026-05-13 16:58:42 +07:00
parent 2366587693
commit 73ed4c2614

View File

@@ -737,6 +737,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
Future<List<PlaceRecommendation>>? _nearbyPlacesFuture; Future<List<PlaceRecommendation>>? _nearbyPlacesFuture;
StreamSubscription<Amplitude>? _amplitudeSub; StreamSubscription<Amplitude>? _amplitudeSub;
Timer? _visualTimer;
var _step = 0; var _step = 0;
var _informationUnits = 0.0; var _informationUnits = 0.0;
var _recording = false; var _recording = false;
@@ -745,7 +746,9 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
var _noiseDb = -72.0; var _noiseDb = -72.0;
var _voicePeakDb = -34.0; var _voicePeakDb = -34.0;
var _liveLevel = 0.0; var _liveLevel = 0.0;
var _visualPhase = 0.0;
DateTime? _lastInformationAt; DateTime? _lastInformationAt;
DateTime? _lastAmplitudeAt;
@override @override
void initState() { void initState() {
@@ -755,6 +758,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
@override @override
void dispose() { void dispose() {
_amplitudeSub?.cancel(); _amplitudeSub?.cancel();
_visualTimer?.cancel();
_waveController.dispose(); _waveController.dispose();
super.dispose(); super.dispose();
} }
@@ -784,7 +788,9 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
await _waveController.startRecording(); await _waveController.startRecording();
await _amplitudeSub?.cancel(); await _amplitudeSub?.cancel();
_lastInformationAt = DateTime.now(); _lastInformationAt = DateTime.now();
_lastAmplitudeAt = null;
_amplitudeSub = _waveController.amplitudeStream.listen(_handleAmplitude); _amplitudeSub = _waveController.amplitudeStream.listen(_handleAmplitude);
_startVisualTick();
setState(() { setState(() {
_micAllowed = true; _micAllowed = true;
@@ -798,8 +804,11 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
Future<void> _stopRecording() async { Future<void> _stopRecording() async {
await _amplitudeSub?.cancel(); await _amplitudeSub?.cancel();
_amplitudeSub = null; _amplitudeSub = null;
_visualTimer?.cancel();
_visualTimer = null;
await _waveController.stopRecording(); await _waveController.stopRecording();
_lastInformationAt = null; _lastInformationAt = null;
_lastAmplitudeAt = null;
if (!mounted) { if (!mounted) {
return; return;
} }
@@ -813,12 +822,16 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
final currentDb = amplitude.current; final currentDb = amplitude.current;
final now = DateTime.now(); final now = DateTime.now();
final level = _normalizeDbLevel(currentDb); final level = _normalizeDbLevel(currentDb);
final informationDelta = _consumeInformationDelta(level, now); final informationLevel = currentDb > -64
? math.max(0.22, level)
: level * 0.35;
final informationDelta = _consumeInformationDelta(informationLevel, now);
_lastAmplitudeAt = now;
setState(() { setState(() {
_voiceSamples _voiceSamples
..removeAt(0) ..removeAt(0)
..add(level); ..add(math.max(0.04, level));
_liveLevel = _smoothLevel(_liveLevel, level); _liveLevel = _smoothLevel(_liveLevel, level);
_informationUnits = math.min( _informationUnits = math.min(
_minimumInformationUnits, _minimumInformationUnits,
@@ -830,6 +843,30 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
.setReviewDuration(_waveController.timeElapsed); .setReviewDuration(_waveController.timeElapsed);
} }
void _startVisualTick() {
_visualTimer?.cancel();
_visualTimer = Timer.periodic(const Duration(milliseconds: 70), (_) {
if (!mounted || !_recording) {
return;
}
_visualPhase += 0.34;
final hasFreshAmplitude =
_lastAmplitudeAt != null &&
DateTime.now().difference(_lastAmplitudeAt!).inMilliseconds < 160;
final baseLevel = hasFreshAmplitude ? _liveLevel : 0.10;
final wave =
(math.sin(_visualPhase) * 0.5 + 0.5) * (0.16 + baseLevel * 0.34);
final visualLevel = (0.06 + baseLevel * 0.70 + wave).clamp(0.0, 1.0);
setState(() {
_voiceSamples
..removeAt(0)
..add(visualLevel);
});
});
}
double _normalizeDbLevel(double currentDb) { double _normalizeDbLevel(double currentDb) {
final db = currentDb.clamp(-160.0, 0.0); final db = currentDb.clamp(-160.0, 0.0);
if (db < _noiseDb) { if (db < _noiseDb) {
@@ -1095,7 +1132,7 @@ class _VoiceStep extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final canFinish = canContinue && !isRecording; final canFinish = canContinue;
return Column( return Column(
children: [ children: [
@@ -1392,7 +1429,8 @@ class _VoiceInformationPainter extends CustomPainter {
final signal = samples.isEmpty final signal = samples.isEmpty
? 0.0 ? 0.0
: samples[sampleIndex].clamp(0.0, 1.0); : samples[sampleIndex].clamp(0.0, 1.0);
final filled = _cellOrder(cellIndex, totalCells) < activeCells; final fillOrder = (rows - 1 - row) * columns + column;
final filled = fillOrder < activeCells;
final x = startX + column * (cellSize + gap); final x = startX + column * (cellSize + gap);
final y = startY + row * (cellSize + gap); final y = startY + row * (cellSize + gap);
final rect = RRect.fromRectAndRadius( final rect = RRect.fromRectAndRadius(
@@ -1442,8 +1480,6 @@ class _VoiceInformationPainter extends CustomPainter {
} }
} }
int _cellOrder(int index, int total) => ((index + 11) * 73) % total;
double _hashUnit(int index) { double _hashUnit(int index) {
final value = math.sin(index * 12.9898 + 78.233) * 43758.5453; final value = math.sin(index * 12.9898 + 78.233) * 43758.5453;
return value - value.floorToDouble(); return value - value.floorToDouble();