Skip to content

Commit 08d7f67

Browse files
authored
freeplay(recorder): Replace string recorder status with enum (#402)
1 parent d757317 commit 08d7f67

4 files changed

Lines changed: 87 additions & 46 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

neothesia/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ fluid-synth = ["synth", "cpal", "fluidlite", "oxisynth"]
1414
oxi-synth = ["synth", "cpal", "oxisynth"]
1515

1616
[dependencies]
17+
thiserror.workspace = true
1718
pollster.workspace = true
1819
log.workspace = true
1920
env_logger.workspace = true

neothesia/src/scene/freeplay/mod.rs

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
icons,
1616
scene::{
1717
MouseToMidiEventState, NuonRenderer, Scene,
18-
freeplay::recorder::{FreeplayRecorder, PreviewState},
18+
freeplay::recorder::{FreeplayRecorder, PreviewState, RecorderError, RecorderStatus},
1919
playing_scene::{Keyboard, midi_player::MidiPlayer},
2020
},
2121
song::Song,
@@ -56,7 +56,7 @@ pub struct FreeplayScene {
5656
mouse_to_midi_state: MouseToMidiEventState,
5757
deduced_chord_name: String,
5858
recorder: FreeplayRecorder,
59-
recorder_status: String,
59+
recorder_status: RecorderStatus,
6060
preview_state: Option<PreviewState>,
6161

6262
context: std::task::Context<'static>,
@@ -102,7 +102,7 @@ impl FreeplayScene {
102102
mouse_to_midi_state: MouseToMidiEventState::default(),
103103
deduced_chord_name: String::new(),
104104
recorder: FreeplayRecorder::default(),
105-
recorder_status: String::new(),
105+
recorder_status: RecorderStatus::default(),
106106
preview_state: None,
107107

108108
context: std::task::Context::from_waker(noop_waker_ref()),
@@ -197,7 +197,7 @@ impl FreeplayScene {
197197
self.keyboard.reset_notes();
198198
}
199199

200-
fn rebuild_preview(&mut self, ctx: &Context) -> Result<(), String> {
200+
fn rebuild_preview(&mut self, ctx: &Context) -> Result<(), RecorderError> {
201201
self.clear_preview();
202202

203203
let song = self.recorder.to_song()?;
@@ -269,15 +269,7 @@ impl FreeplayScene {
269269
self.keyboard.reset_notes();
270270
}
271271

272-
fn preview_status_label(&self) -> String {
273-
if self.recorder.is_recording() {
274-
return format!("Recording {:.1}s", self.recorder.duration().as_secs_f32(),);
275-
}
276-
277-
self.recorder_status.clone()
278-
}
279-
280-
fn stop_recording(&mut self, ctx: &Context) -> Result<(), String> {
272+
fn stop_recording(&mut self, ctx: &Context) -> Result<(), RecorderError> {
281273
self.recorder.stop();
282274
self.rebuild_preview(ctx)
283275
}
@@ -287,33 +279,30 @@ impl FreeplayScene {
287279
match self.stop_recording(ctx) {
288280
Ok(()) => {
289281
self.recorder_status =
290-
format!("Recorded {:.1}s", self.recorder.duration().as_secs_f32());
282+
RecorderStatus::RecordingFinished(self.recorder.duration());
291283
}
292284
Err(err) => {
293-
self.recorder_status = err;
285+
self.recorder_status = RecorderStatus::Error(err);
294286
}
295287
}
296288

297289
return;
298290
}
299291

300292
self.clear_preview();
293+
self.recorder_status = RecorderStatus::default();
301294
self.recorder.start();
302-
self.recorder_status = "Recording in progress".to_string();
303295
}
304296

305297
fn handle_save_click(&mut self, ctx: &Context) {
306298
if self.recorder.is_recording()
307299
&& let Err(err) = self.stop_recording(ctx)
308300
{
309-
self.recorder_status = err;
301+
self.recorder_status = RecorderStatus::Error(err);
310302
return;
311303
}
312304

313-
if !self.recorder.has_note_events() {
314-
self.recorder_status = "Nothing recorded yet".to_string();
315-
return;
316-
}
305+
debug_assert!(self.recorder.has_note_events());
317306

318307
let mut dialog = rfd::AsyncFileDialog::new()
319308
.add_filter("midi", &["mid", "midi"])
@@ -326,28 +315,26 @@ impl FreeplayScene {
326315
let smf = match self.recorder.to_smf() {
327316
Ok(smf) => smf,
328317
Err(err) => {
329-
self.recorder_status = err;
318+
self.recorder_status = RecorderStatus::Error(err);
330319
return;
331320
}
332321
};
333322

334-
self.futures.push(on_async(
335-
dialog.save_file(),
336-
|path, state, _ctx| match path {
337-
Some(file) => match FreeplayRecorder::save_to_path(smf, file.path()) {
323+
self.futures
324+
.push(on_async(dialog.save_file(), |file, state, _ctx| {
325+
let Some(file) = file else {
326+
return;
327+
};
328+
329+
match FreeplayRecorder::save_to_path(smf, file.path()) {
338330
Ok(()) => {
339-
state.recorder_status =
340-
format!("Saved recording to {}", file.path().display());
331+
state.recorder_status = RecorderStatus::Saved(file.path().to_owned());
341332
}
342333
Err(err) => {
343-
state.recorder_status = err;
334+
state.recorder_status = RecorderStatus::Error(err);
344335
}
345-
},
346-
None => {
347-
state.recorder_status = "Save canceled".to_string();
348336
}
349-
},
350-
));
337+
}));
351338
}
352339

353340
fn update_preview(&mut self, ctx: &Context, delta: Duration) -> Option<f32> {
@@ -376,7 +363,11 @@ impl FreeplayScene {
376363
.map(|s| s.player.is_paused())
377364
.unwrap_or(true);
378365

379-
let status_label = self.preview_status_label();
366+
let status_label = if self.recorder.is_recording() {
367+
format!("Recording {:.1}s", self.recorder.duration().as_secs_f32())
368+
} else {
369+
self.recorder_status.to_string()
370+
};
380371

381372
enum Msg {
382373
TogglePlay,

neothesia/src/scene/freeplay/recorder.rs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{
22
collections::HashSet,
3-
path::Path,
3+
fmt,
4+
path::{Path, PathBuf},
45
time::{Duration, Instant},
56
};
67

@@ -11,6 +12,44 @@ use neothesia_core::render::{NoteLabels, WaterfallRenderer};
1112

1213
use crate::{scene::playing_scene::midi_player::MidiPlayer, song::Song};
1314

15+
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
16+
pub enum RecorderError {
17+
#[error("No note events recorded")]
18+
NoNotesFound,
19+
#[error("Failed to write MIDI file")]
20+
Write,
21+
#[error("{0}")]
22+
MidiFileParse(String),
23+
}
24+
25+
#[derive(Default, Debug)]
26+
pub enum RecorderStatus {
27+
#[default]
28+
Idle,
29+
RecordingFinished(Duration),
30+
Saved(PathBuf),
31+
Error(RecorderError),
32+
}
33+
34+
impl fmt::Display for RecorderStatus {
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
match self {
37+
Self::Idle => {}
38+
Self::RecordingFinished(duration) => {
39+
write!(f, "Recorded {:.1}s", duration.as_secs_f32())?;
40+
}
41+
Self::Error(err) => {
42+
write!(f, "{err}")?;
43+
}
44+
Self::Saved(path) => {
45+
write!(f, "Saved recording to {}", path.display())?;
46+
}
47+
}
48+
49+
Ok(())
50+
}
51+
}
52+
1453
#[derive(Clone, Copy)]
1554
pub struct RecordedMidiEvent {
1655
timestamp: Duration,
@@ -122,15 +161,24 @@ impl FreeplayRecorder {
122161
}
123162
}
124163

125-
pub fn save_to_path(smf: Smf<'static>, path: &Path) -> Result<(), String> {
164+
pub fn save_to_path(smf: Smf<'static>, path: &Path) -> Result<(), RecorderError> {
126165
let mut bytes = Vec::new();
127-
smf.write_std(&mut bytes)
128-
.map_err(|err| format!("Failed to encode MIDI: {err}"))?;
129-
std::fs::write(path, bytes).map_err(|err| format!("Failed to write MIDI file: {err}"))
166+
167+
if smf.write_std(&mut bytes).is_err() {
168+
return Err(RecorderError::Write);
169+
}
170+
171+
if std::fs::write(path, bytes).is_err() {
172+
return Err(RecorderError::Write);
173+
}
174+
175+
Ok(())
130176
}
131177

132-
pub fn to_song(&self) -> Result<Song, String> {
133-
let midi = midi_file::MidiFile::from_smf("freeplay-recording.mid", self.to_smf()?)?;
178+
pub fn to_song(&self) -> Result<Song, RecorderError> {
179+
let smf = self.to_smf()?;
180+
let midi = midi_file::MidiFile::from_smf("freeplay-recording.mid", smf)
181+
.map_err(RecorderError::MidiFileParse)?;
134182
Ok(Song::new(midi))
135183
}
136184

@@ -150,9 +198,9 @@ impl FreeplayRecorder {
150198
}
151199
}
152200

153-
pub fn to_smf(&self) -> Result<Smf<'static>, String> {
201+
pub fn to_smf(&self) -> Result<Smf<'static>, RecorderError> {
154202
if !self.has_note_events() {
155-
return Err("No note events recorded yet".to_string());
203+
return Err(RecorderError::NoNotesFound);
156204
}
157205

158206
let events = match &self.state {
@@ -256,7 +304,7 @@ mod freeplay_recorder_tests {
256304
let error = recorder
257305
.to_song()
258306
.expect_err("pedal-only recordings should not create preview songs");
259-
assert_eq!(error, "No note events recorded yet");
307+
assert_eq!(error, RecorderError::NoNotesFound);
260308
}
261309

262310
#[test]
@@ -277,6 +325,6 @@ mod freeplay_recorder_tests {
277325
let error = recorder
278326
.to_song()
279327
.expect_err("note-off-only recordings should not create preview songs");
280-
assert_eq!(error, "No note events recorded yet");
328+
assert_eq!(error, RecorderError::NoNotesFound);
281329
}
282330
}

0 commit comments

Comments
 (0)