Skip to content

Commit 252d746

Browse files
feat: added csv playback for soundmeter instrument. (#2879)
1 parent 6aa049b commit 252d746

File tree

7 files changed

+272
-9
lines changed

7 files changed

+272
-9
lines changed

lib/l10n/app_en.arb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,5 +477,10 @@
477477
"distance" : "Distance",
478478
"distanceUnitLabel" : "mm",
479479
"legacyFirmwareAlertTitle": "Legacy Firmware Detected",
480-
"legacyFirmwareAlertMessage": "We have detected that your PSLab device is running legacy firmware. Please note that support for this firmware has ended. For the best experience and continued support, please update your device to the latest firmware version."
480+
"legacyFirmwareAlertMessage": "We have detected that your PSLab device is running legacy firmware. Please note that support for this firmware has ended. For the best experience and continued support, please update your device to the latest firmware version.",
481+
"playbackStarted" : "Playback started",
482+
"playback" : "Playback",
483+
"stopPlayback" : "Stop Playback",
484+
"resumePlayback" : "Resume Playback",
485+
"pausePlayback" : "Pause Playback"
481486
}

lib/l10n/app_localizations.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2993,6 +2993,36 @@ abstract class AppLocalizations {
29932993
/// In en, this message translates to:
29942994
/// **'We have detected that your PSLab device is running legacy firmware. Please note that support for this firmware has ended. For the best experience and continued support, please update your device to the latest firmware version.'**
29952995
String get legacyFirmwareAlertMessage;
2996+
2997+
/// No description provided for @playbackStarted.
2998+
///
2999+
/// In en, this message translates to:
3000+
/// **'Playback started'**
3001+
String get playbackStarted;
3002+
3003+
/// No description provided for @playback.
3004+
///
3005+
/// In en, this message translates to:
3006+
/// **'Playback'**
3007+
String get playback;
3008+
3009+
/// No description provided for @stopPlayback.
3010+
///
3011+
/// In en, this message translates to:
3012+
/// **'Stop Playback'**
3013+
String get stopPlayback;
3014+
3015+
/// No description provided for @resumePlayback.
3016+
///
3017+
/// In en, this message translates to:
3018+
/// **'Resume Playback'**
3019+
String get resumePlayback;
3020+
3021+
/// No description provided for @pausePlayback.
3022+
///
3023+
/// In en, this message translates to:
3024+
/// **'Pause Playback'**
3025+
String get pausePlayback;
29963026
}
29973027

29983028
class _AppLocalizationsDelegate

lib/l10n/app_localizations_en.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,4 +1552,19 @@ class AppLocalizationsEn extends AppLocalizations {
15521552
@override
15531553
String get legacyFirmwareAlertMessage =>
15541554
'We have detected that your PSLab device is running legacy firmware. Please note that support for this firmware has ended. For the best experience and continued support, please update your device to the latest firmware version.';
1555+
1556+
@override
1557+
String get playbackStarted => 'Playback started';
1558+
1559+
@override
1560+
String get playback => 'Playback';
1561+
1562+
@override
1563+
String get stopPlayback => 'Stop Playback';
1564+
1565+
@override
1566+
String get resumePlayback => 'Resume Playback';
1567+
1568+
@override
1569+
String get pausePlayback => 'Pause Playback';
15551570
}

lib/providers/soundmeter_state_provider.dart

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:pslab/others/logger_service.dart';
77
import 'package:flutter/foundation.dart';
88
import 'package:pslab/providers/locator.dart';
99
import 'package:pslab/others/audio_jack.dart';
10+
import 'package:permission_handler/permission_handler.dart';
1011

1112
class SoundMeterStateProvider extends ChangeNotifier {
1213
AppLocalizations appLocalizations = getIt.get<AppLocalizations>();
@@ -26,14 +27,38 @@ class SoundMeterStateProvider extends ChangeNotifier {
2627
int _dataCount = 0;
2728
bool _isRecording = false;
2829
List<List<dynamic>> _recordedData = [];
30+
bool _isPlayingBack = false;
31+
List<List<dynamic>>? _playbackData;
32+
int _playbackIndex = 0;
33+
Timer? _playbackTimer;
34+
bool _isPlaybackPaused = false;
2935
bool get isRecording => _isRecording;
36+
bool get isPlayingBack => _isPlayingBack;
37+
bool get isPlaybackPaused => _isPlaybackPaused;
3038

3139
Function(String)? onSensorError;
40+
Function? onPlaybackEnd;
3241

3342
void initializeSensors({Function(String)? onError}) async {
3443
onSensorError = onError;
3544

3645
try {
46+
PermissionStatus microphonePermission =
47+
await Permission.microphone.status;
48+
49+
if (microphonePermission != PermissionStatus.granted) {
50+
microphonePermission = await Permission.microphone.request();
51+
}
52+
if (microphonePermission != PermissionStatus.granted) {
53+
if (microphonePermission == PermissionStatus.permanentlyDenied) {
54+
await openAppSettings();
55+
_handleSensorError("Microphone permission is permanently denied.");
56+
return;
57+
} else {
58+
return;
59+
}
60+
}
61+
3762
_audioJack = AudioJack();
3863
await _audioJack!.initialize();
3964
await _audioJack!.start();
@@ -92,8 +117,116 @@ class SoundMeterStateProvider extends ChangeNotifier {
92117
_audioJack = null;
93118
}
94119

120+
void startPlayback(List<List<dynamic>> data) {
121+
if (data.length <= 1) return;
122+
123+
_isPlayingBack = true;
124+
_isPlaybackPaused = false;
125+
_playbackData = data;
126+
_playbackIndex = 1;
127+
128+
_timeTimer?.cancel();
129+
_audioTimer?.cancel();
130+
131+
_dbData.clear();
132+
dbChartData.clear();
133+
_timeData.clear();
134+
_startTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
135+
_currentTime = 0;
136+
_dbSum = 0;
137+
_dataCount = 0;
138+
139+
_startPlaybackTimer();
140+
notifyListeners();
141+
}
142+
143+
void _startPlaybackTimer() {
144+
if (_playbackIndex >= _playbackData!.length) {
145+
stopPlayback();
146+
return;
147+
}
148+
149+
final currentRow = _playbackData![_playbackIndex];
150+
if (currentRow.length > 2) {
151+
_currentDb = double.tryParse(currentRow[2].toString()) ?? 0.0;
152+
_currentTime = (_playbackIndex - 1).toDouble();
153+
_updateData();
154+
_playbackIndex++;
155+
notifyListeners();
156+
} else {
157+
logger.e(
158+
'Skipping playback row at index $_playbackIndex due to insufficient columns (found ${currentRow.length}, expected at least 3');
159+
_playbackIndex++;
160+
notifyListeners();
161+
}
162+
163+
Duration interval = const Duration(seconds: 1);
164+
165+
if (_playbackIndex < _playbackData!.length && _playbackIndex > 1) {
166+
try {
167+
final currentTimestamp =
168+
int.tryParse(_playbackData![_playbackIndex - 1][0].toString());
169+
final nextTimestamp =
170+
int.tryParse(_playbackData![_playbackIndex][0].toString());
171+
172+
if (currentTimestamp != null && nextTimestamp != null) {
173+
final timeDiff = nextTimestamp - currentTimestamp;
174+
interval = Duration(milliseconds: timeDiff);
175+
if (interval.inMilliseconds < 100) {
176+
interval = const Duration(milliseconds: 100);
177+
} else if (interval.inMilliseconds > 10000) {
178+
interval = const Duration(seconds: 10);
179+
}
180+
}
181+
} catch (e) {
182+
interval = const Duration(seconds: 1);
183+
}
184+
}
185+
186+
_playbackTimer = Timer(interval, () {
187+
if (_isPlayingBack && !_isPlaybackPaused) {
188+
_startPlaybackTimer();
189+
}
190+
});
191+
}
192+
193+
Future<void> stopPlayback() async {
194+
_isPlayingBack = false;
195+
_isPlaybackPaused = false;
196+
_playbackTimer?.cancel();
197+
_playbackData = null;
198+
_playbackIndex = 0;
199+
200+
_dbData.clear();
201+
dbChartData.clear();
202+
_timeData.clear();
203+
_dbSum = 0;
204+
_dataCount = 0;
205+
_currentDb = 0.0;
206+
_currentTime = 0;
207+
notifyListeners();
208+
onPlaybackEnd?.call();
209+
}
210+
211+
void pausePlayback() {
212+
if (_isPlayingBack) {
213+
_isPlaybackPaused = true;
214+
_playbackTimer?.cancel();
215+
notifyListeners();
216+
}
217+
}
218+
219+
void resumePlayback() {
220+
if (_isPlayingBack && _isPlaybackPaused) {
221+
_isPlaybackPaused = false;
222+
_startPlaybackTimer();
223+
notifyListeners();
224+
}
225+
}
226+
95227
@override
96228
void dispose() {
229+
_playbackTimer?.cancel();
97230
disposeSensors();
98231
super.dispose();
99232
}

lib/view/logged_data_screen.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'package:intl/intl.dart';
44
import 'package:pslab/others/csv_service.dart';
55
import 'package:pslab/theme/colors.dart';
66
import 'package:pslab/view/logged_data_chart_screen.dart';
7-
7+
import 'package:pslab/view/soundmeter_screen.dart';
88
import '../l10n/app_localizations.dart';
99
import '../providers/locator.dart';
1010

@@ -164,6 +164,22 @@ class _LoggedDataScreenState extends State<LoggedDataScreen> {
164164
}
165165
}
166166

167+
Future<void> _playFile(File file) async {
168+
final data = await _csvService.readCsvFromFile(file);
169+
if (data.isNotEmpty && mounted) {
170+
switch (widget.instrumentName) {
171+
case 'soundmeter':
172+
Navigator.push(
173+
context,
174+
MaterialPageRoute(
175+
builder: (context) => SoundMeterScreen(playbackData: data),
176+
),
177+
);
178+
break;
179+
}
180+
}
181+
}
182+
167183
Future<void> _pickAndImportFile() async {
168184
final data = await _csvService.pickAndReadCsvFile();
169185
if (data != null && mounted) {
@@ -288,6 +304,12 @@ class _LoggedDataScreenState extends State<LoggedDataScreen> {
288304
mainAxisAlignment: MainAxisAlignment.center,
289305
mainAxisSize: MainAxisSize.min,
290306
children: [
307+
if (widget.instrumentName == "soundmeter")
308+
IconButton(
309+
icon:
310+
Icon(Icons.play_arrow, color: primaryRed),
311+
onPressed: () => _playFile(file),
312+
),
291313
IconButton(
292314
icon: Icon(Icons.share, color: primaryRed),
293315
onPressed: () =>

lib/view/soundmeter_screen.dart

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import '../constants.dart';
1515
import '../theme/colors.dart';
1616

1717
class SoundMeterScreen extends StatefulWidget {
18-
const SoundMeterScreen({super.key});
18+
final List<List<dynamic>>? playbackData;
19+
const SoundMeterScreen({super.key, this.playbackData});
20+
1921
@override
2022
State<StatefulWidget> createState() => _SoundMeterScreenState();
2123
}
@@ -105,7 +107,7 @@ class _SoundMeterScreenState extends State<SoundMeterScreen> {
105107
MaterialPageRoute(
106108
builder: (context) => LoggedDataScreen(
107109
instrumentName: 'soundmeter',
108-
appBarName: 'Sound Meter',
110+
appBarName: appLocalizations.soundMeter,
109111
instrumentIcon: instrumentIcons[15],
110112
),
111113
),
@@ -196,9 +198,18 @@ class _SoundMeterScreenState extends State<SoundMeterScreen> {
196198
void initState() {
197199
super.initState();
198200
_provider = SoundMeterStateProvider();
201+
_provider.onPlaybackEnd = () {
202+
if (mounted && Navigator.canPop(context)) {
203+
Navigator.pop(context);
204+
}
205+
};
199206
WidgetsBinding.instance.addPostFrameCallback((_) {
200207
if (mounted) {
201-
_provider.initializeSensors(onError: _showSensorErrorSnackbar);
208+
if (widget.playbackData != null) {
209+
_provider.startPlayback(widget.playbackData!);
210+
} else {
211+
_provider.initializeSensors(onError: _showSensorErrorSnackbar);
212+
}
202213
}
203214
});
204215
}
@@ -234,11 +245,27 @@ class _SoundMeterScreenState extends State<SoundMeterScreen> {
234245
Consumer<SoundMeterStateProvider>(
235246
builder: (context, provider, child) {
236247
return CommonScaffold(
237-
title: appLocalizations.soundMeterTitle,
248+
title: provider.isPlayingBack
249+
? '${appLocalizations.soundMeter} - ${appLocalizations.playback}'
250+
: appLocalizations.soundMeterTitle,
238251
onGuidePressed: _showInstrumentGuide,
239-
onOptionsPressed: _showOptionsMenu,
240-
onRecordPressed: _toggleRecording,
252+
onOptionsPressed:
253+
provider.isPlayingBack ? null : _showOptionsMenu,
254+
onRecordPressed:
255+
provider.isPlayingBack ? null : _toggleRecording,
241256
isRecording: provider.isRecording,
257+
isPlayingBack: provider.isPlayingBack,
258+
isPlaybackPaused: provider.isPlaybackPaused,
259+
onPlaybackPauseResume: provider.isPlayingBack
260+
? (provider.isPlaybackPaused
261+
? _provider.resumePlayback
262+
: _provider.pausePlayback)
263+
: null,
264+
onPlaybackStop: provider.isPlayingBack
265+
? () async {
266+
await _provider.stopPlayback();
267+
}
268+
: null,
242269
body: SafeArea(
243270
child: LayoutBuilder(
244271
builder: (context, constraints) {

0 commit comments

Comments
 (0)