diff --git a/crates/model/src/session.rs b/crates/model/src/session.rs index 59567966..9c7e8827 100644 --- a/crates/model/src/session.rs +++ b/crates/model/src/session.rs @@ -570,6 +570,10 @@ impl Session { if !self.edit.zoom.samples_per_px.is_finite() || self.edit.zoom.samples_per_px <= 0.0 { self.edit.zoom.samples_per_px = 1024.0; } + if SampleRate::new(self.sample_rate.hz()).is_err() { + self.sample_rate = SampleRate::default(); + } + self.tempo.sanitize(); } } @@ -641,6 +645,15 @@ mod tests { assert_eq!(c.source_offset(), 580); } + #[test] + fn a_bad_sample_rate_or_tempo_is_repaired_on_load() { + let mut v = serde_json::to_value(Session::default()).unwrap(); + v["sample_rate"] = serde_json::json!(0); + v["tempo"]["tempos"] = serde_json::json!([{"tick": 0, "bpm": 4e9}]); + let s = Session::from_json(&v.to_string()).unwrap(); + assert_eq!((s.sample_rate, s.tempo), (SampleRate::HZ_48000, TempoMap::default())); + } + #[test] fn rejects_garbage_and_future_versions() { assert!(Session::from_json("not json").is_err()); diff --git a/crates/time/src/tempo.rs b/crates/time/src/tempo.rs index 5b87a3a7..72936d80 100644 --- a/crates/time/src/tempo.rs +++ b/crates/time/src/tempo.rs @@ -55,6 +55,10 @@ fn valid_bpm(bpm: f64) -> Result { if bpm.is_finite() && (5.0..=1000.0).contains(&bpm) { Ok(bpm) } else { Err(TimeError::Tempo(bpm)) } } +fn valid_meter(numerator: u32, denominator: u32) -> bool { + (1..=64).contains(&numerator) && [1, 2, 4, 8, 16, 32, 64].contains(&denominator) +} + impl TempoMap { pub fn new(bpm: f64, numerator: u32, denominator: u32) -> Result { let mut m = TempoMap::default(); @@ -70,6 +74,25 @@ impl TempoMap { &self.meters } + /// Repair a map read from a file: only valid tempos and meters, in tick order, with one of each + /// at tick 0. + pub fn sanitize(&mut self) { + self.tempos.retain(|e| e.tick >= 0 && valid_bpm(e.bpm).is_ok()); + self.tempos.sort_by_key(|e| e.tick); + self.tempos.dedup_by_key(|e| e.tick); + if self.tempos.first().is_none_or(|e| e.tick > 0) { + let bpm = self.tempos.first().map_or(120.0, |e| e.bpm); + self.tempos.insert(0, TempoEvent { tick: 0, bpm }); + } + self.meters.retain(|m| m.tick >= 0 && valid_meter(m.numerator, m.denominator)); + self.meters.sort_by_key(|m| m.tick); + self.meters.dedup_by_key(|m| m.tick); + if self.meters.first().is_none_or(|m| m.tick > 0) { + let (numerator, denominator) = self.meters.first().map_or((4, 4), |m| (m.numerator, m.denominator)); + self.meters.insert(0, MeterEvent { tick: 0, numerator, denominator }); + } + } + /// Insert or replace the tempo at `tick`. pub fn set_tempo(&mut self, tick: i64, bpm: f64) -> Result<(), TimeError> { let bpm = valid_bpm(bpm)?; @@ -97,7 +120,7 @@ impl TempoMap { /// Insert or replace the meter at `tick` (snapped to the bar line at or before it). pub fn set_meter(&mut self, tick: i64, numerator: u32, denominator: u32) -> Result<(), TimeError> { - if !(1..=64).contains(&numerator) || ![1, 2, 4, 8, 16, 32, 64].contains(&denominator) { + if !valid_meter(numerator, denominator) { return Err(TimeError::Meter(numerator, denominator)); } let tick = if tick <= 0 { 0 } else { self.bar_start_tick(self.bar_beat_at_tick(tick).bar) }; @@ -317,6 +340,22 @@ mod tests { assert_eq!(m.bar_beat_at_tick(TICKS_PER_QUARTER / 2), BarBeat { bar: 1, beat: 2, tick: 0 }); } + #[test] + fn sanitize_keeps_what_the_setters_accept() { + let tempo = |tick, bpm| TempoEvent { tick, bpm }; + let meter = |tick, numerator, denominator| MeterEvent { tick, numerator, denominator }; + let mut m = TempoMap { + tempos: vec![tempo(960, 4e9), tempo(3840, 90.0), tempo(-5, 100.0), tempo(1920, 60.0), tempo(1920, 70.0)], + meters: vec![meter(0, 0, 4), meter(3840, 3, 4)], + }; + m.sanitize(); + assert_eq!(m.tempos(), [tempo(0, 60.0), tempo(1920, 60.0), tempo(3840, 90.0)]); + assert_eq!(m.meters(), [meter(0, 3, 4), meter(3840, 3, 4)]); + let mut empty = TempoMap { tempos: Vec::new(), meters: Vec::new() }; + empty.sanitize(); + assert_eq!(empty, TempoMap::default()); + } + #[test] fn rejects_bad_values() { let mut m = TempoMap::default();