Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion lib/features/voice_to_text/view/voice_to_text_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,36 @@ abstract class VoiceToTextModel extends Listenable {
Stream<List<double>> get waveformStream;
Duration get elapsedDuration;
bool get isTimerRunning;
RecordingState get recordingState;

void setActiveWord(int index);
void toggleCursorVisibility(bool visible);
void updateWaveform(List<double> amplitudes);
void startTimer();
void pauseTimer();
void resetTimer();
void startRecording();
void pauseRecording();
void resumeRecording();
void stopRecording();
void restartRecording();
void discardRecording();

// Other state already planned (timer, waveform, recording commands) lives here too.
}

enum RecordingState { idle, recording, paused, stopped }

class VoiceToTextModelState extends ChangeNotifier implements VoiceToTextModel {
VoiceToTextModelState({
required List<String> initialTranscript,
int initialActiveWordIndex = 0,
bool initialCursorVisible = true,
RecordingState initialRecordingState = RecordingState.idle,
}) : _transcript = List.unmodifiable(initialTranscript),
_activeWordIndex = initialActiveWordIndex,
_isCursorVisible = initialCursorVisible;
_isCursorVisible = initialCursorVisible,
_recordingState = initialRecordingState;

final List<String> _transcript;
int _activeWordIndex;
Expand All @@ -39,6 +50,7 @@ class VoiceToTextModelState extends ChangeNotifier implements VoiceToTextModel {
Duration _elapsedDuration = Duration.zero;
bool _isTimerRunning = false;
Timer? _timer;
RecordingState _recordingState;

@override
List<String> get transcript => _transcript;
Expand All @@ -61,6 +73,9 @@ class VoiceToTextModelState extends ChangeNotifier implements VoiceToTextModel {
@override
bool get isTimerRunning => _isTimerRunning;

@override
RecordingState get recordingState => _recordingState;

@override
void setActiveWord(int index) {
if (index == _activeWordIndex) return;
Expand Down Expand Up @@ -120,6 +135,57 @@ class VoiceToTextModelState extends ChangeNotifier implements VoiceToTextModel {
}
}

@override
void startRecording() {
if (_recordingState == RecordingState.recording) return;
_recordingState = RecordingState.recording;
startTimer();
notifyListeners();
}

@override
void pauseRecording() {
if (_recordingState != RecordingState.recording) return;
_recordingState = RecordingState.paused;
pauseTimer();
notifyListeners();
}

@override
void resumeRecording() {
if (_recordingState != RecordingState.paused) return;
_recordingState = RecordingState.recording;
startTimer();
notifyListeners();
}

@override
void stopRecording() {
if (_recordingState == RecordingState.stopped) return;
_recordingState = RecordingState.stopped;
resetTimer();
notifyListeners();
}

@override
void restartRecording() {
_recordingState = RecordingState.recording;
resetTimer();
startTimer();
notifyListeners();
}

@override
void discardRecording() {
_recordingState = RecordingState.idle;
_waveformData = const [];
if (!_waveformController.isClosed) {
_waveformController.add(_waveformData);
}
resetTimer();
notifyListeners();
}

@override
void dispose() {
_timer?.cancel();
Expand Down
62 changes: 49 additions & 13 deletions lib/features/voice_to_text/view/voice_to_text_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../widget/control_buttons.dart';
import '../widget/text_display.dart';
import '../widget/timer_display.dart';
import '../widget/waveform.dart';
Expand Down Expand Up @@ -64,7 +65,6 @@ class _VoiceToTextViewState extends State<_VoiceToTextView> {
_waveformTick,
(_) => _pushWaveformSample(model),
);
Comment on lines 65 to 67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Waveform continues updating while recording is paused or discarded

The new recording state logic clears waveformData and resets the timer when a session is paused, stopped, or discarded, but _VoiceToTextViewState still schedules _waveformTimer in initState and never cancels or gates it based on recordingState. As a result, _pushWaveformSample keeps publishing random samples even after discardRecording() has emptied the waveform or when the state is paused/stopped, so the UI immediately repopulates with a live waveform and the “clear” action has no visible effect. Consider starting the periodic sampler only while RecordingState.recording or checking the state before pushing samples so the waveform can actually freeze or clear when recording is inactive.

Useful? React with 👍 / 👎.

model.startTimer();
});
}

Expand All @@ -75,11 +75,20 @@ class _VoiceToTextViewState extends State<_VoiceToTextView> {
}

void _pushWaveformSample(VoiceToTextModel model) {
final sample = List<double>.generate(_waveformSampleCount, (index) {
final variance = math.sin(index / 4) * 0.3 + 0.5;
final noise = (_random.nextDouble() - 0.5) * 0.2;
return (variance + noise).clamp(0.0, 1.0);
final halfCount = _waveformSampleCount ~/ 2;
final leading = List<double>.generate(halfCount, (index) {
final phase = index / halfCount * math.pi;
final base = (math.sin(phase) * 0.5) + 0.5;
final noise = (_random.nextDouble() - 0.5) * 0.15;
return (base + noise).clamp(0.0, 1.0);
});
final trailing = List<double>.from(leading.reversed);
final sample = [...leading, ...trailing];
if (sample.length < _waveformSampleCount) {
sample.addAll(
List<double>.filled(_waveformSampleCount - sample.length, 0.3),
);
}
model.updateWaveform(sample);
}

Expand All @@ -105,22 +114,49 @@ class _VoiceToTextViewState extends State<_VoiceToTextView> {
flex: 3,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: WaveformStream(
stream: model.waveformStream,
initialAmplitudes: model.waveformData,
height: 220,
barColor: Colors.black87,
barWidth: 3,
spacing: 6,
backgroundColor: Colors.transparent,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: WaveformStream(
stream: model.waveformStream,
initialAmplitudes: model.waveformData,
height: 220,
barColor: Colors.black87,
barWidth: 3,
spacing: 6,
backgroundColor: Colors.transparent,
),
),
),
),
),
TimerDisplay(duration: model.elapsedDuration),
ControlButtonsRow(
state: model.recordingState,
onMicTap: () => _handleMicTap(model),
onPause: model.pauseRecording,
onResume: model.resumeRecording,
onDiscard: model.discardRecording,
),
const SizedBox(height: 24),
],
),
),
);
}

void _handleMicTap(VoiceToTextModelState model) {
switch (model.recordingState) {
case RecordingState.idle:
case RecordingState.stopped:
model.startRecording();
break;
case RecordingState.recording:
model.stopRecording();
break;
case RecordingState.paused:
model.stopRecording();
break;
}
}
}
Loading