mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-11 16:35:42 -06:00
Cut names from files to a length that can be drawn
A MIDI file whose track name was a million characters long imported fine, and the next frame crashed the app: drawing that name needed a 320 MB vertex buffer, past wgpu's 256 MB limit, and the panic comes from painting, outside anything that catches it. Saved, the session crashed again on every open. A 4 MB status line in ui.json did the same at every launch. Names read from files are now cut to 256 characters and comments to 4096: everything Session::sanitize repairs on load (session, track, playlist, clip, marker, bus, output, group, source, video and key names), MIDI track names on import and clip names from clip group files. The status bar draws at most 1024 characters.
This commit is contained in:
@@ -144,6 +144,7 @@ pub fn import(e: &mut Engine, path: &str, first: Option<TrackId>, 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);
|
||||
|
||||
+13
-1
@@ -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();
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user