diff --git a/crates/engine/src/clip_group_file.rs b/crates/engine/src/clip_group_file.rs index 66735f9b..e60beedd 100644 --- a/crates/engine/src/clip_group_file.rs +++ b/crates/engine/src/clip_group_file.rs @@ -144,6 +144,7 @@ pub fn import(e: &mut Engine, path: &str, first: Option, at: Samples) - continue; } c.id = s.new_clip_id(); + soundcraft_model::truncate_chars(&mut c.name, soundcraft_model::MAX_NAME_CHARS); c.start = at.saturating_add(scale(c.start.max(0))); c.length = scale(c.length).max(1); c.group = Some(group_id); diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index f1bf6a49..a99f7cc6 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -203,7 +203,8 @@ pub fn import_midi_bytes(e: &mut Engine, bytes: &[u8], at: Samples, tempo_map: b if tr.sequence.notes.is_empty() { continue; } - let name = if tr.name.trim().is_empty() { format!("MIDI {}", i + 1) } else { tr.name.clone() }; + let mut name = if tr.name.trim().is_empty() { format!("MIDI {}", i + 1) } else { tr.name.clone() }; + soundcraft_model::truncate_chars(&mut name, soundcraft_model::MAX_NAME_CHARS); let id = s.add_track(TrackKind::Instrument, ChannelFormat::Stereo, Some(&name)); let drums = tr.sequence.notes.iter().all(|n| n.channel == 9); if let Some(t) = s.track_mut(id) { @@ -1265,6 +1266,17 @@ mod rate_tests { mod name_tests { use super::*; + #[test] + fn a_midi_track_name_is_cut_to_a_length_that_can_be_drawn() { + let note = soundcraft_midi::Note { pitch: 60, velocity: 100, release_velocity: 64, channel: 0, start: 0, length: 960 }; + let sequence = soundcraft_midi::Sequence { notes: vec![note], ..Default::default() }; + let smf = soundcraft_midi::Smf { tracks: vec![soundcraft_midi::SmfTrack { name: "n".repeat(1_000_000), sequence }], ..Default::default() }; + let mut e = Engine::default(); + let ids = import_midi_bytes(&mut e, &soundcraft_midi::write_smf(&smf).unwrap(), 0, false).unwrap(); + let track = e.session().track(ids[0]).unwrap(); + assert_eq!((track.name.len(), track.clips()[0].name.len()), (soundcraft_model::MAX_NAME_CHARS, soundcraft_model::MAX_NAME_CHARS)); + } + #[test] fn new_sources_take_the_first_free_file_name() { let mut s = Session::default(); diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 55d605d0..ec8aaa2c 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -22,7 +22,10 @@ pub use markers::{Group, MarkerKind, MemoryLocation}; pub use mixer::{ Bus, ChannelFormat, INSERT_SLOTS, Insert, Mixer, OutputPath, Route, SEND_SLOTS, SendSlot, Speaker, SurroundPan, fader_db_to_pos, fader_pos_to_db, }; -pub use session::{BitDepthSetting, EditMode, EditState, SESSION_EXTENSION, Session, SessionError, Tool, TrackView, ZoomState}; +pub use session::{ + BitDepthSetting, EditMode, EditState, MAX_COMMENT_CHARS, MAX_NAME_CHARS, SESSION_EXTENSION, Session, SessionError, Tool, TrackView, ZoomState, + truncate_chars, +}; pub use source::{Source, SourceAudio, SourcePool, VideoSource}; pub use track::{Playlist, TRACK_COLORS, Track, TrackHeight, TrackKind}; diff --git a/crates/model/src/session.rs b/crates/model/src/session.rs index 9d384f5b..a3f906c3 100644 --- a/crates/model/src/session.rs +++ b/crates/model/src/session.rs @@ -575,6 +575,39 @@ impl Session { self.sample_rate = SampleRate::default(); } self.tempo.sanitize(); + truncate_chars(&mut self.name, MAX_NAME_CHARS); + truncate_chars(&mut self.comments, MAX_COMMENT_CHARS); + for t in &mut self.tracks { + truncate_chars(&mut t.name, MAX_NAME_CHARS); + truncate_chars(&mut t.comments, MAX_COMMENT_CHARS); + for p in &mut t.playlists { + truncate_chars(&mut p.name, MAX_NAME_CHARS); + p.clips.iter_mut().for_each(|c| truncate_chars(&mut c.name, MAX_NAME_CHARS)); + } + } + for m in &mut self.markers { + truncate_chars(&mut m.name, MAX_NAME_CHARS); + truncate_chars(&mut m.comments, MAX_COMMENT_CHARS); + } + self.key_signatures.iter_mut().for_each(|(_, k)| truncate_chars(k, MAX_NAME_CHARS)); + self.busses.iter_mut().for_each(|b| truncate_chars(&mut b.name, MAX_NAME_CHARS)); + self.outputs.iter_mut().for_each(|o| truncate_chars(&mut o.name, MAX_NAME_CHARS)); + self.groups.iter_mut().for_each(|g| truncate_chars(&mut g.name, MAX_NAME_CHARS)); + self.sources.iter_mut().for_each(|s| truncate_chars(&mut s.name, MAX_NAME_CHARS)); + self.videos.iter_mut().for_each(|v| truncate_chars(&mut v.name, MAX_NAME_CHARS)); + } +} + +/// The most characters kept of a name read from a file: longer ones can't be shown, and drawing +/// a name of a million characters overflows the renderer's buffers. +pub const MAX_NAME_CHARS: usize = 256; +/// The most characters kept of a comment read from a file. +pub const MAX_COMMENT_CHARS: usize = 4096; + +/// Cut `s` to at most `max` characters. +pub fn truncate_chars(s: &mut String, max: usize) { + if let Some((end, _)) = s.char_indices().nth(max) { + s.truncate(end); } } @@ -657,6 +690,19 @@ mod tests { assert_eq!(s.unique_track_name("Audio"), "Audio 1"); } + #[test] + fn names_from_a_file_are_cut_to_a_length_that_can_be_drawn() { + let mut s = Session::default(); + let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None); + if let Some(tr) = s.track_mut(t) { + tr.name = "é".repeat(1_000); + } + s.comments = "c".repeat(10_000); + let back = Session::from_json(&s.to_json().unwrap()).unwrap(); + assert_eq!(back.track(t).map(|tr| tr.name.chars().count()), Some(MAX_NAME_CHARS)); + assert_eq!(back.comments.len(), MAX_COMMENT_CHARS); + } + #[test] fn a_bad_sample_rate_or_tempo_is_repaired_on_load() { let mut v = serde_json::to_value(Session::default()).unwrap(); diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs index f13bc78a..dc2453b5 100644 --- a/crates/ui-egui/src/edit_window.rs +++ b/crates/ui-egui/src/edit_window.rs @@ -157,7 +157,7 @@ fn status_bar(app: &mut SoundApp, ui: &mut Ui) { let t = Tokens::current(); let r = ui.max_rect(); let s = app.engine.session(); - let left = format!( + let mut left = format!( "{} · {} Hz · {} · {} tracks · {}", s.name, s.sample_rate.hz(), @@ -169,6 +169,7 @@ fn status_bar(app: &mut SoundApp, ui: &mut Ui) { s.tracks.len(), app.ui.status ); + soundcraft_model::truncate_chars(&mut left, 4 * soundcraft_model::MAX_NAME_CHARS); ui.painter().text(pos2(r.min.x + 10.0, r.center().y), Align2::LEFT_CENTER, left, regular(11.0), t.text_dim); let undo = app.engine.undo_label().map_or(String::new(), |l| format!("Undo {l}")); ui.painter().text(