diff --git a/apps/soundcraft-cli/src/main.rs b/apps/soundcraft-cli/src/main.rs index 9a51cc54..58cc0098 100644 --- a/apps/soundcraft-cli/src/main.rs +++ b/apps/soundcraft-cli/src/main.rs @@ -110,7 +110,7 @@ fn info(args: &[String]) -> ExitCode { Ok(b) => b, Err(e) => return fail(format!("{path}: {e}")), }; - let ext = std::path::Path::new(path).extension().and_then(|x| x.to_str()); + let ext = std::path::Path::new(path).extension().and_then(std::ffi::OsStr::to_str); match soundcraft_audio_io::probe(&bytes, ext) { Ok(i) => { outln!("{}", serde_json::to_string_pretty(&json!({"format": i.format, "sample_format": i.sample_format, "sample_rate": i.sample_rate, "channels": i.channels, "frames": i.frames, "seconds": i.duration_secs(), "bwf": i.bwf})).unwrap_or_default()); @@ -134,7 +134,7 @@ fn convert(args: &[String]) -> ExitCode { Ok(b) => b, Err(e) => return fail(format!("{input}: {e}")), }; - let ext = std::path::Path::new(input).extension().and_then(|x| x.to_str()); + let ext = std::path::Path::new(input).extension().and_then(std::ffi::OsStr::to_str); let (_, mut buf) = match soundcraft_audio_io::decode(&bytes, ext) { Ok(x) => x, Err(e) => return fail(format!("{input}: {e}")), @@ -146,7 +146,7 @@ fn convert(args: &[String]) -> ExitCode { if args.iter().any(|a| a == "--normalize") { soundcraft_dsp::offline::normalize(&mut buf.channels, -0.1, false); } - let out_ext = std::path::Path::new(output).extension().and_then(|x| x.to_str()).unwrap_or("wav").to_ascii_lowercase(); + let out_ext = std::path::Path::new(output).extension().and_then(std::ffi::OsStr::to_str).unwrap_or("wav").to_ascii_lowercase(); let Some(format) = soundcraft_audio_io::encode_format_for(&out_ext) else { return fail(format!("{output}: cannot write `.{out_ext}` files (supported: {})", soundcraft_audio_io::ENCODE_EXTENSIONS)); }; diff --git a/apps/soundcraft-web/index.html b/apps/soundcraft-web/index.html index af72b21b..124e0b71 100644 --- a/apps/soundcraft-web/index.html +++ b/apps/soundcraft-web/index.html @@ -2,7 +2,7 @@ - + SoundCraft diff --git a/apps/soundcraft/src/main.rs b/apps/soundcraft/src/main.rs index 808cdef8..cd27ec44 100644 --- a/apps/soundcraft/src/main.rs +++ b/apps/soundcraft/src/main.rs @@ -182,7 +182,7 @@ fn services() -> Services { if let Some(dir) = path.parent().filter(|dir| dir.is_dir()) { d = d.set_directory(dir); } - if let Some(n) = path.file_name().and_then(|n| n.to_str()) { + if let Some(n) = path.file_name().and_then(std::ffi::OsStr::to_str) { d = d.set_file_name(n); } d.save_file().map(|p| p.to_string_lossy().into_owned()) diff --git a/crates/clap-host/src/scan.rs b/crates/clap-host/src/scan.rs index 92a6f6d3..6bfbadd6 100644 --- a/crates/clap-host/src/scan.rs +++ b/crates/clap-host/src/scan.rs @@ -51,7 +51,7 @@ pub fn os_search_paths(home: Option, common_program_files: Option bool { - p.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("clap")) + p.extension().and_then(std::ffi::OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("clap")) } /// Recursively collects `.clap` files and bundles under `dirs` (missing directories are skipped). diff --git a/crates/engine/src/clip_group_file.rs b/crates/engine/src/clip_group_file.rs index 35bb2df3..9a44cca5 100644 --- a/crates/engine/src/clip_group_file.rs +++ b/crates/engine/src/clip_group_file.rs @@ -50,7 +50,7 @@ pub fn export(e: &Engine, ids: &[ClipId], path: &str) -> Result { }; let n = clips.len(); lanes.sort_by_key(|(i, _)| *i); - let stem = p.file_stem().and_then(|x| x.to_str()).unwrap_or("Clip Group").to_string(); + let stem = p.file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Clip Group").to_string(); let audio_dir_name = format!("{} Audio", crate::io::sanitize_name(&stem)); let dir = p.parent().map(Path::to_path_buf).unwrap_or_default(); let mut audio = BTreeMap::new(); @@ -80,26 +80,17 @@ pub fn export(e: &Engine, ids: &[ClipId], path: &str) -> Result { Ok(n) } -/// Import a clip group file at `at`, one lane per track starting at `first` (new audio tracks -/// are made when the session runs out). Returns the ids of the placed clips. -pub fn import(e: &mut Engine, path: &str, first: Option, at: Samples) -> Result> { - let id = "file.import_clip_groups"; - let text = crate::io::read_text(path)?; - let file: GroupFile = serde_json::from_str(&text).map_err(|err| EngineError::BadParams(id.into(), format!("not a clip group file: {err}")))?; - if file.format != FORMAT { - return Err(EngineError::BadParams(id.into(), "not a SoundCraft clip group file".into())); - } - if file.lanes.iter().map(|l| l.len().max(1)).sum::() > MAX_CLIPS || file.audio.len() > MAX_CLIPS { - return Err(EngineError::BadParams(id.into(), "clip group file has too many clips".into())); - } - let dir = Path::new(path).parent().map(Path::to_path_buf).unwrap_or_default(); - // Decode audio first so a bad file changes nothing, and each file once however many ids name it. +fn decode_group_audio( + audio_entries: &BTreeMap, + dir: &Path, + rate: u32, + cmd_id: &str, +) -> Result<(Vec<(String, soundcraft_audio_io::AudioBuffer)>, BTreeMap)> { let (mut decoded, mut index, mut file_of) = (Vec::new(), BTreeMap::new(), BTreeMap::new()); - let rate = e.session().sample_rate.hz(); - for (old, rel) in &file.audio { + for (old, rel) in audio_entries { let full = dir.join(rel); - if !full.starts_with(&dir) || rel.contains("..") { - return Err(EngineError::BadParams(id.into(), format!("audio path escapes the group folder: {rel}"))); + if !full.starts_with(dir) || rel.contains("..") { + return Err(EngineError::BadParams(cmd_id.into(), format!("audio path escapes the group folder: {rel}"))); } let key = crate::io::file_key(&full); let n = match index.get(&key) { @@ -116,6 +107,25 @@ pub fn import(e: &mut Engine, path: &str, first: Option, at: Samples) - }; file_of.insert(*old, n); } + Ok((decoded, file_of)) +} + +/// Import a clip group file at `at`, one lane per track starting at `first` (new audio tracks +/// are made when the session runs out). Returns the ids of the placed clips. +pub fn import(e: &mut Engine, path: &str, first: Option, at: Samples) -> Result> { + let id = "file.import_clip_groups"; + let text = crate::io::read_text(path)?; + let file: GroupFile = serde_json::from_str(&text).map_err(|err| EngineError::BadParams(id.into(), format!("not a clip group file: {err}")))?; + if file.format != FORMAT { + return Err(EngineError::BadParams(id.into(), "not a SoundCraft clip group file".into())); + } + if file.lanes.iter().map(|l| l.len().max(1)).sum::() > MAX_CLIPS || file.audio.len() > MAX_CLIPS { + return Err(EngineError::BadParams(id.into(), "clip group file has too many clips".into())); + } + let dir = Path::new(path).parent().map(Path::to_path_buf).unwrap_or_default(); + // Decode audio first so a bad file changes nothing, and each file once however many ids name it. + let rate = e.session().sample_rate.hz(); + let (decoded, file_of) = decode_group_audio(&file.audio, &dir, rate, id)?; let src_rate = f64::from(file.sample_rate.max(1)); let s = e.session_mut(); let ratio = f64::from(s.sample_rate.hz()) / src_rate; @@ -123,7 +133,7 @@ pub fn import(e: &mut Engine, path: &str, first: Option, at: Samples) - let sources: Vec = decoded .into_iter() .map(|(rel, buf)| { - let name = Path::new(&rel).file_name().and_then(|x| x.to_str()).unwrap_or("clip group").to_string(); + let name = Path::new(&rel).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or("clip group").to_string(); crate::io::add_source(s, &name, buf, None, FileFormat::Wav) }) .collect(); diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 40e3d3db..d5b18db1 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -241,7 +241,7 @@ fn import_audio(e: &mut Engine, p: &Value) -> Result { let mut out = Vec::new(); for path in &paths { let bytes = crate::io::read_file(path)?; - let name = std::path::Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or("Audio").to_string(); + let name = std::path::Path::new(path).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or("Audio").to_string(); let r = crate::io::import_audio_bytes(e, &name, &bytes, Some(path.as_str()), target, at)?; out.push(r); } @@ -302,7 +302,7 @@ pub fn bounce_job(e: &Engine, p: &Value) -> Result { let path = str_param(p, "path").ok_or_else(|| bad("file.bounce_mix", "`path` required"))?.to_string(); let r = range_param(e, "file.bounce_mix", p)?; let r = if r.is_empty() { soundcraft_time::Range::new(0, e.session().content_end().max(1)) } else { r }; - let ext = std::path::Path::new(&path).extension().and_then(|x| x.to_str()).unwrap_or("wav").to_ascii_lowercase(); + let ext = std::path::Path::new(&path).extension().and_then(std::ffi::OsStr::to_str).unwrap_or("wav").to_ascii_lowercase(); let format_name = str_param(p, "format").unwrap_or(ext.as_str()); let format = soundcraft_audio_io::encode_format_for(format_name).ok_or_else(|| { bad("file.bounce_mix", format!("cannot write `{format_name}` files (supported: {})", soundcraft_audio_io::ENCODE_EXTENSIONS)) diff --git a/crates/engine/src/cmd/health.rs b/crates/engine/src/cmd/health.rs index 260503ec..36819c99 100644 --- a/crates/engine/src/cmd/health.rs +++ b/crates/engine/src/cmd/health.rs @@ -12,6 +12,55 @@ pub fn specs() -> Vec { ] } +struct ClipContext<'a> { + track: &'a soundcraft_model::Track, + playlist: &'a soundcraft_model::Playlist, + active: bool, + session_sample_rate: u32, +} + +fn check_audio_clip( + ctx: &ClipContext<'_>, + clip: &soundcraft_model::Clip, + source: soundcraft_model::SourceId, + offset: i64, + metadata: Option<&soundcraft_model::Source>, + audio: Option<&soundcraft_model::SourceAudio>, + issues: &mut Vec, +) { + let mut add = |code: &str, message: &str| { + issues.push(json!({"code": code, "message": message, "track": ctx.track.name, "track_id": ctx.track.id.0, + "playlist": ctx.playlist.name, "active": ctx.active, "clip": clip.name, "clip_id": clip.id.0, + "source_id": source.0, "path": metadata.map(|m| m.path.as_str())})); + }; + if metadata.is_none() { + add("missing_source", "Source metadata is missing. Reimport the audio and replace this clip."); + } + let Some(audio) = audio else { + add("unavailable_audio", "Audio is not loaded; this clip will be silent. Restore the media and reopen the session, or reimport it."); + return; + }; + if audio.buffer.sample_rate != ctx.session_sample_rate { + add("sample_rate_mismatch", "Loaded audio differs from the session sample rate. Reimport it to convert to the session rate."); + } + if audio.buffer.channels.is_empty() || audio.frames() == 0 { + add("empty_audio", "Loaded audio has no playable frames. Reimport a valid audio file."); + return; + } + if audio.buffer.channels.iter().any(|channel| channel.len() != audio.frames()) { + add("uneven_channels", "Loaded audio channels have different lengths. Reimport a valid audio file; bounds below use the shortest channel."); + } + if !clip.stretch.is_finite() || clip.stretch <= 0.0 || clip.length <= 0 { + add("invalid_clip", "Clip length or stretch is invalid. Replace this clip."); + } + let stretch = if clip.stretch.is_finite() && clip.stretch > 0.0 { clip.stretch } else { 1.0 }; + // Match the mixer's last requested source sample, including elastic playback. + let last = offset as f64 + clip.length.saturating_sub(1).max(0) as f64 / stretch; + if offset < 0 || last >= audio.frames() as f64 { + add("source_bounds", "Clip extends outside the loaded audio; part will be silent. Trim the clip or replace its source."); + } +} + fn report(e: &mut Engine, _: &Value) -> Result { let s = e.session(); let sources: BTreeMap<_, _> = s.sources.iter().map(|source| (source.id, source)).collect(); @@ -24,51 +73,16 @@ fn report(e: &mut Engine, _: &Value) -> Result { for track in &s.tracks { for (index, playlist) in track.playlists.iter().enumerate() { let active = index == track.active_playlist; + let ctx = ClipContext { track, playlist, active, session_sample_rate: s.sample_rate.hz() }; for clip in &playlist.clips { let ClipContent::Audio { source, offset } = clip.content else { continue }; clips += 1; if active { active_end = active_end.max(clip.end()); } - let metadata = sources.get(&source); - let audio = s.pool.get(source); - let mut add = |code: &str, message: &str| { - issues.push(json!({"code": code, "message": message, "track": track.name, "track_id": track.id.0, - "playlist": playlist.name, "active": active, "clip": clip.name, "clip_id": clip.id.0, - "source_id": source.0, "path": metadata.map(|m| m.path.as_str())})); - }; - if metadata.is_none() { - add("missing_source", "Source metadata is missing. Reimport the audio and replace this clip."); - } - let Some(audio) = audio else { - add( - "unavailable_audio", - "Audio is not loaded; this clip will be silent. Restore the media and reopen the session, or reimport it.", - ); - continue; - }; - if audio.buffer.sample_rate != s.sample_rate.hz() { - add("sample_rate_mismatch", "Loaded audio differs from the session sample rate. Reimport it to convert to the session rate."); - } - if audio.buffer.channels.is_empty() || audio.frames() == 0 { - add("empty_audio", "Loaded audio has no playable frames. Reimport a valid audio file."); - continue; - } - if audio.buffer.channels.iter().any(|channel| channel.len() != audio.frames()) { - add( - "uneven_channels", - "Loaded audio channels have different lengths. Reimport a valid audio file; bounds below use the shortest channel.", - ); - } - if !clip.stretch.is_finite() || clip.stretch <= 0.0 || clip.length <= 0 { - add("invalid_clip", "Clip length or stretch is invalid. Replace this clip."); - } - let stretch = if clip.stretch.is_finite() && clip.stretch > 0.0 { clip.stretch } else { 1.0 }; - // Match the mixer's last requested source sample, including elastic playback. - let last = offset as f64 + clip.length.saturating_sub(1).max(0) as f64 / stretch; - if offset < 0 || last >= audio.frames() as f64 { - add("source_bounds", "Clip extends outside the loaded audio; part will be silent. Trim the clip or replace its source."); - } + let metadata = sources.get(&source).copied(); + let audio = s.pool.get(source).map(|a| a.as_ref()); + check_audio_clip(&ctx, clip, source, offset, metadata, audio, &mut issues); } } } diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index b6f379bd..9a984d62 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -81,7 +81,7 @@ impl CommandSpec { CommandInfo { id: self.id.into(), label: self.label.into(), - menu: self.menu.iter().map(|s| s.to_string()).collect(), + menu: self.menu.iter().map(ToString::to_string).collect(), shortcut: self.shortcut.map(str::to_string), params: self.params.into(), enabled: en.is_ok(), diff --git a/crates/engine/src/cmd/sampler.rs b/crates/engine/src/cmd/sampler.rs index eb401bbd..f4634e3d 100644 --- a/crates/engine/src/cmd/sampler.rs +++ b/crates/engine/src/cmd/sampler.rs @@ -56,7 +56,7 @@ fn load(e: &mut Engine, p: &Value) -> Result { } } let bytes = crate::io::read_file(&path)?; - let name = Path::new(&path).file_name().and_then(|n| n.to_str()).unwrap_or("Sample").to_string(); + let name = Path::new(&path).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or("Sample").to_string(); attach(e, t, &name, &bytes, Some(&path), values) } @@ -69,7 +69,7 @@ pub(crate) fn attach(e: &mut Engine, t: TrackId, name: &str, bytes: &[u8], path: return Err(bad(id, "only instrument tracks have a sampler")); } let info = soundcraft_dsp::plugin_info(SAMPLER).ok_or_else(|| bad(id, "the sampler is not available"))?; - let ext = Path::new(name).extension().and_then(|x| x.to_str()).map(str::to_ascii_lowercase); + let ext = Path::new(name).extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase); let (format, buf) = soundcraft_audio_io::decode(bytes, ext.as_deref()).map_err(|err| EngineError::Io(format!("{name}: {err}")))?; if buf.frames() == 0 || buf.num_channels() == 0 { return Err(EngineError::Io(format!("{name}: no audio"))); diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index 4b6601b6..5d85a162 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -5,6 +5,7 @@ use serde_json::{Value, json}; use soundcraft_audio_io::{AudioBuffer, EncodeOptions, FileFormat}; use soundcraft_model::{BusId, ChannelFormat, Clip, ClipContent, Session, Source, SourceAudio, SourceId, TrackId, TrackKind}; use soundcraft_time::{Range, Samples}; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -114,7 +115,7 @@ pub fn add_source(s: &mut Session, name: &str, mut buf: AudioBuffer, path: Optio buf.sample_rate = sr; } let id = SourceId(s.alloc()); - let stem = Path::new(name).file_stem().and_then(|x| x.to_str()).unwrap_or(name).to_string(); + let stem = Path::new(name).file_stem().and_then(OsStr::to_str).unwrap_or(name).to_string(); let stored = match path { Some(p) => p.to_string(), None => unique_audio_rel(s, &stem), @@ -145,7 +146,7 @@ fn stamp_at_rate(stamp: u64, file_rate: u32, session_rate: u32) -> u64 { /// Decode and place an audio file. With no target track, a new track of matching width is made. pub fn import_audio_bytes(e: &mut Engine, name: &str, bytes: &[u8], path: Option<&str>, target: Option, at: Samples) -> Result { - let ext = Path::new(name).extension().and_then(|x| x.to_str()).map(str::to_ascii_lowercase); + let ext = Path::new(name).extension().and_then(OsStr::to_str).map(str::to_ascii_lowercase); let (info, buf) = soundcraft_audio_io::decode(bytes, ext.as_deref()).map_err(|err| EngineError::Io(format!("{name}: {err}")))?; let buf = at_rate(buf, e.session().sample_rate.hz(), MAX_IMPORT_SAMPLES) .ok_or_else(|| EngineError::Io(format!("{name}: file is too large to import")))?; @@ -164,7 +165,7 @@ pub fn import_audio_bytes(e: &mut Engine, name: &str, bytes: &[u8], path: Option let track = match target { Some(t) => t, None => { - let stem = Path::new(name).file_stem().and_then(|x| x.to_str()).unwrap_or("Audio").to_string(); + let stem = Path::new(name).file_stem().and_then(OsStr::to_str).unwrap_or("Audio").to_string(); s.add_track(TrackKind::Audio, ChannelFormat::for_channels(channels), Some(&stem)) } }; @@ -188,8 +189,8 @@ pub fn load_sampler_bytes(e: &mut Engine, track: TrackId, name: &str, bytes: &[u /// never copied) plus its audio on a new audio track when it has a usable audio stream. Fails /// only when the file has neither a readable picture nor audio. pub fn import_video_bytes(e: &mut Engine, path: &str, bytes: Vec, at: Samples) -> Result { - let stem = Path::new(path).file_stem().and_then(|n| n.to_str()).unwrap_or("Video").to_string(); - let ext = Path::new(path).extension().and_then(|x| x.to_str()).unwrap_or("mp4").to_ascii_lowercase(); + let stem = Path::new(path).file_stem().and_then(OsStr::to_str).unwrap_or("Video").to_string(); + let ext = Path::new(path).extension().and_then(OsStr::to_str).unwrap_or("mp4").to_ascii_lowercase(); let bytes: Arc<[u8]> = bytes.into(); let movie = soundcraft_video::Movie::open(Arc::clone(&bytes)).map(|m| m.info().clone()); let audio_name = format!("{stem} audio.{}", if ext == "mov" { "mp4" } else { ext.as_str() }); @@ -210,7 +211,7 @@ pub fn import_video_bytes(e: &mut Engine, path: &str, bytes: Vec, at: Sample let vid = SourceId(s.alloc()); s.videos.push(soundcraft_model::VideoSource { id: vid, - name: Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or("movie").to_string(), + name: Path::new(path).file_name().and_then(OsStr::to_str).unwrap_or("movie").to_string(), path: abs, width: info.width, height: info.height, @@ -992,7 +993,7 @@ fn write_session(session: &Session, path: &str, overwrite: bool) -> Result<(Sess sync_dir(&audio_dir); } s.folder = Some(dir.clone()); - if !overwrite && let Some(stem) = p.file_stem().and_then(|x| x.to_str()) { + if !overwrite && let Some(stem) = p.file_stem().and_then(OsStr::to_str) { s.name = stem.to_string(); } let text = s.to_json().map_err(|err| EngineError::Io(err.to_string()))?; @@ -1052,7 +1053,7 @@ fn copy_new(from: &Path, to: &Path) -> std::io::Result<()> { /// Copy the session file about to be replaced to `Backups/ backup .`, keeping the /// newest ten. fn back_up(file: &Path) { - let (Some(dir), Some(stem), Some(ext)) = (file.parent(), file.file_stem().and_then(|x| x.to_str()), file.extension().and_then(|x| x.to_str())) + let (Some(dir), Some(stem), Some(ext)) = (file.parent(), file.file_stem().and_then(OsStr::to_str), file.extension().and_then(OsStr::to_str)) else { return; }; @@ -1195,7 +1196,7 @@ pub(crate) fn decode_file(files: &mut DecodedFiles, p: &Path, rate: u32) -> std: .entry(file_key(p)) .or_insert_with(|| { let bytes = read_file(p).map_err(|_| p.display().to_string())?; - decode_source(&bytes, p.extension().and_then(|x| x.to_str()), rate).map_err(|err| format!("{}: {err}", p.display())) + decode_source(&bytes, p.extension().and_then(OsStr::to_str), rate).map_err(|err| format!("{}: {err}", p.display())) }) .clone() } @@ -1227,7 +1228,7 @@ pub fn set_deliver(h: DeliverHook) { pub fn write_file(path: &str, bytes: &[u8]) -> Result<()> { if cfg!(target_arch = "wasm32") { let deliver = DELIVER.get().ok_or_else(|| EngineError::Io("saving files is not available in this build".into()))?; - let name = Path::new(path).file_name().and_then(|n| n.to_str()).filter(|n| !n.is_empty()).unwrap_or("Untitled"); + let name = Path::new(path).file_name().and_then(OsStr::to_str).filter(|n| !n.is_empty()).unwrap_or("Untitled"); return deliver(name, bytes).map_err(EngineError::Io); } crate::file_access::check_write(path).map_err(EngineError::Io)?; @@ -1272,7 +1273,7 @@ pub fn session_bundle(session: &Session) -> Result<(Vec, Session)> { /// Save as a bundle named after `path`'s file name, delivered through the host ([`write_file`]). fn save_session_bundle(e: &mut Engine, path: &str) -> Result { - let stem = Path::new(path).file_stem().and_then(|x| x.to_str()).filter(|x| !x.is_empty()).map(str::to_string); + let stem = Path::new(path).file_stem().and_then(OsStr::to_str).filter(|x| !x.is_empty()).map(str::to_string); let mut s = e.session().clone(); if let Some(stem) = stem { s.name = stem; @@ -1311,7 +1312,7 @@ pub fn open_session_bytes(e: &mut Engine, name: &str, bytes: &[u8]) -> Result match decode_source(&bytes, Path::new(&src.path).extension().and_then(|x| x.to_str()), s.sample_rate.hz()) { + Some(bytes) => match decode_source(&bytes, Path::new(&src.path).extension().and_then(OsStr::to_str), s.sample_rate.hz()) { Ok(audio) => Some(audio), Err(err) => { missing.push(format!("{}: {err}", src.path)); @@ -1330,7 +1331,7 @@ pub fn open_session_bytes(e: &mut Engine, name: &str, bytes: &[u8]) -> Result Result<(String, std::collections::HashMap String { - p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::().cloned()).unwrap_or_else(|| "panic".into()) + p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::().cloned()).unwrap_or_else(|| "panic".into()) } #[cfg(test)] diff --git a/crates/model/src/mixer.rs b/crates/model/src/mixer.rs index bef493e8..7f0a24e9 100644 --- a/crates/model/src/mixer.rs +++ b/crates/model/src/mixer.rs @@ -196,7 +196,7 @@ impl ChannelFormat { pub fn channel_label(self, ch: usize) -> String { match self.speakers().get(ch) { Some(s) => s.label().to_string(), - None if self.is_ambisonic() => ["W", "Y", "Z", "X"].get(ch).map_or_else(|| format!("{}", ch + 1), |s| s.to_string()), + None if self.is_ambisonic() => ["W", "Y", "Z", "X"].get(ch).map_or_else(|| format!("{}", ch + 1), ToString::to_string), None => format!("{}", ch + 1), } } diff --git a/crates/model/src/session.rs b/crates/model/src/session.rs index b72818dc..cac69d99 100644 --- a/crates/model/src/session.rs +++ b/crates/model/src/session.rs @@ -288,7 +288,7 @@ impl Default for EditState { timeline_selection: Range::default(), flags: ["view.clip.name", "view.clip.gain_line", "waveform.peak", "view.clip.overlap_shadows", "view.marker.ruler_lines"] .iter() - .map(|s| s.to_string()) + .map(ToString::to_string) .collect(), values: std::collections::BTreeMap::new(), } diff --git a/crates/time/src/format.rs b/crates/time/src/format.rs index 3c11c844..1244db86 100644 --- a/crates/time/src/format.rs +++ b/crates/time/src/format.rs @@ -124,7 +124,7 @@ pub fn parse_position( } TimeFormat::BarsBeats => { let parts: Vec = - t.split(['|', '.', ' ']).filter(|p| !p.is_empty()).map(|p| p.parse::()).collect::>().map_err(|_| err())?; + t.split(['|', '.', ' ']).filter(|p| !p.is_empty()).map(str::parse::).collect::>().map_err(|_| err())?; let bb = BarBeat { bar: *parts.first().ok_or_else(err)?, beat: parts.get(1).copied().unwrap_or(1).max(1), diff --git a/crates/ui-egui/src/credits.rs b/crates/ui-egui/src/credits.rs index bd58c366..a53128cb 100644 --- a/crates/ui-egui/src/credits.rs +++ b/crates/ui-egui/src/credits.rs @@ -210,10 +210,7 @@ impl Default for View { } } -/// About ▸ Contributors: a name toggle, a sort, and the list as a grab bag or a table. -pub fn contributors_ui(ui: &mut egui::Ui) { - let id = egui::Id::new("credits_view"); - let mut v = ui.data_mut(|d| d.get_temp::(id)).unwrap_or_default(); +fn contributors_toolbar(ui: &mut egui::Ui, v: &mut View) { ui.horizontal_wrapped(|ui| { ui.label("Show"); for m in NameMode::ALL { @@ -242,6 +239,24 @@ pub fn contributors_ui(ui: &mut egui::Ui) { v.table = true; } }); +} + +fn contributors_grab_bag(ui: &mut egui::Ui, list: &[&Contributor], names: NameMode) { + ui.horizontal_wrapped(|ui| { + for (i, c) in list.iter().enumerate() { + if i > 0 { + ui.label(RichText::new("·").weak()); + } + ui.hyperlink_to(c.name(names), format!("https://github.com/{}", c.login)).on_hover_text(c.summary()); + } + }); +} + +/// About ▸ Contributors: a name toggle, a sort, and the list as a grab bag or a table. +pub fn contributors_ui(ui: &mut egui::Ui) { + let id = egui::Id::new("credits_view"); + let mut v = ui.data_mut(|d| d.get_temp::(id)).unwrap_or_default(); + contributors_toolbar(ui, &mut v); let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending); ui.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().weak()); ui.separator(); @@ -251,14 +266,7 @@ pub fn contributors_ui(ui: &mut egui::Ui) { } else if v.table { table(ui, &list, &mut v); } else { - ui.horizontal_wrapped(|ui| { - for (i, c) in list.iter().enumerate() { - if i > 0 { - ui.label(RichText::new("·").weak()); - } - ui.hyperlink_to(c.name(v.names), format!("https://github.com/{}", c.login)).on_hover_text(c.summary()); - } - }); + contributors_grab_bag(ui, &list, v.names); } }); ui.data_mut(|d| d.insert_temp(id, v)); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3fdd9892..8b532400 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -776,7 +776,7 @@ impl SoundApp { for d in std::fs::read_dir(dir).ok()?.flatten() { for f in std::fs::read_dir(d.path()).into_iter().flatten().flatten() { let p = f.path(); - if p.extension().and_then(|x| x.to_str()) == Some("scraft") + if p.extension().and_then(std::ffi::OsStr::to_str) == Some("scraft") && let Ok(m) = f.metadata().and_then(|m| m.modified()) && best.as_ref().is_none_or(|(t, _)| m > *t) { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 99e4b503..9ba795ca 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -86,7 +86,7 @@ pub fn ui_aliases() -> Vec<(&'static str, &'static str)> { /// Mix-view menu paths, allocated once. Rebuilding them on every menu draw leaked a copy per frame. fn mix_view_aliases() -> &'static [(&'static str, &'static str)] { - static ALIASES: std::sync::OnceLock> = std::sync::OnceLock::new(); + static ALIASES: std::sync::OnceLock> = std::sync::OnceLock::new(); ALIASES.get_or_init(|| { [ "Inserts A-E", @@ -633,7 +633,7 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option { app.ui.mix_views = - ["inserts_ae", "inserts_fj", "sends_ae", "sends_fj", "io", "comments", "color"].iter().map(|s| s.to_string()).collect() + ["inserts_ae", "inserts_fj", "sends_ae", "sends_fj", "io", "comments", "color"].iter().map(ToString::to_string).collect() } "minimal" => app.ui.mix_views.clear(), "" => return Some(Err("`section` required".into())), diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index a208511f..07664994 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -371,6 +371,29 @@ fn big_counter(app: &mut SoundApp, ctx: &egui::Context) { app.ui.show_big_counter = open; } +fn memory_location_row(app: &mut SoundApp, ui: &mut Ui, n: u32, name: &str, loc: &str, len: &str, kind: &str) { + if ui.button(n.to_string()).on_hover_text("Recall").clicked() { + let _ = app.run("markers.recall", json!({"number": n})); + } + let key = egui::Id::new(("mem_name", n)); + let mut edit: String = ui.ctx().memory(|m| m.data.get_temp(key)).unwrap_or_else(|| name.to_string()); + let r = ui.add(egui::TextEdit::singleline(&mut edit).desired_width(140.0)); + if r.changed() { + ui.ctx().memory_mut(|m| m.data.insert_temp(key, edit.clone())); + } + if r.lost_focus() && edit != name { + let _ = app.run("markers.edit", json!({"number": n, "rename": edit})); + ui.ctx().memory_mut(|m| m.data.remove::(key)); + } + ui.label(egui::RichText::new(loc).font(mono(11.0))); + ui.label(egui::RichText::new(len).font(mono(11.0))); + ui.label(kind); + if ui.small_button("✕").on_hover_text("Delete").clicked() { + let _ = app.run("markers.delete", json!({"number": n})); + } + ui.end_row(); +} + pub(crate) fn memory_locations(app: &mut SoundApp, ui: &mut Ui) { ui.horizontal(|ui| { if ui.button("+ Marker").clicked() { @@ -402,27 +425,8 @@ pub(crate) fn memory_locations(app: &mut SoundApp, ui: &mut Ui) { ui.label(egui::RichText::new(h).strong()); } ui.end_row(); - for (n, name, loc, len, kind) in rows { - if ui.button(n.to_string()).on_hover_text("Recall").clicked() { - let _ = app.run("markers.recall", json!({"number": n})); - } - let key = egui::Id::new(("mem_name", n)); - let mut edit: String = ui.ctx().memory(|m| m.data.get_temp(key)).unwrap_or_else(|| name.clone()); - let r = ui.add(egui::TextEdit::singleline(&mut edit).desired_width(140.0)); - if r.changed() { - ui.ctx().memory_mut(|m| m.data.insert_temp(key, edit.clone())); - } - if r.lost_focus() && edit != name { - let _ = app.run("markers.edit", json!({"number": n, "rename": edit})); - ui.ctx().memory_mut(|m| m.data.remove::(key)); - } - ui.label(egui::RichText::new(loc).font(mono(11.0))); - ui.label(egui::RichText::new(len).font(mono(11.0))); - ui.label(kind); - if ui.small_button("✕").on_hover_text("Delete").clicked() { - let _ = app.run("markers.delete", json!({"number": n})); - } - ui.end_row(); + for (n, name, loc, len, kind) in &rows { + memory_location_row(app, ui, *n, name, loc, len, kind); } }); }); @@ -669,7 +673,7 @@ fn preset_menu( return; }; let mut names: Vec = std::fs::read_dir(&dir) - .map(|rd| rd.flatten().filter_map(|e| e.path().file_stem().and_then(|x| x.to_str()).map(str::to_string)).collect()) + .map(|rd| rd.flatten().filter_map(|e| e.path().file_stem().and_then(std::ffi::OsStr::to_str).map(str::to_string)).collect()) .unwrap_or_default(); names.sort(); for n in &names { diff --git a/crates/ui-egui/src/recent.rs b/crates/ui-egui/src/recent.rs index 12be3980..597cc99c 100644 --- a/crates/ui-egui/src/recent.rs +++ b/crates/ui-egui/src/recent.rs @@ -13,6 +13,7 @@ use crate::SoundApp; use serde_json::json; +use std::ffi::OsStr; use std::path::Path; /// How many sessions the menu keeps. @@ -45,8 +46,8 @@ pub fn note(list: &mut Vec, path: &str) -> bool { /// "name — folder" for a session path. fn label(path: &str) -> String { let p = Path::new(path); - let name = p.file_stem().and_then(|n| n.to_str()).unwrap_or(path); - match p.parent().and_then(|d| d.file_name()).and_then(|n| n.to_str()) { + let name = p.file_stem().and_then(OsStr::to_str).unwrap_or(path); + match p.parent().and_then(Path::file_name).and_then(OsStr::to_str) { Some(folder) => format!("{name} — {folder}"), None => name.to_string(), } diff --git a/crates/ui-egui/src/sampler_window.rs b/crates/ui-egui/src/sampler_window.rs index b259343d..45f9697b 100644 --- a/crates/ui-egui/src/sampler_window.rs +++ b/crates/ui-egui/src/sampler_window.rs @@ -8,6 +8,7 @@ use crate::theme::{Tokens, mono, regular}; use egui::{Align2, Color32, CursorIcon, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::json; use soundcraft_model::{Insert, TrackId}; +use std::ffi::OsStr; use std::path::Path; /// The built-in Sampler's plugin id. @@ -21,7 +22,7 @@ const MARKERS: [(&str, bool); 3] = [("start", false), ("loop_start", true), ("lo /// Whether a file name looks like audio the sampler can load. pub fn is_audio_name(name: &str) -> bool { - Path::new(name).extension().and_then(|x| x.to_str()).is_some_and(|x| AUDIO_EXTS.contains(&x.to_ascii_lowercase().as_str())) + Path::new(name).extension().and_then(OsStr::to_str).is_some_and(|x| AUDIO_EXTS.contains(&x.to_ascii_lowercase().as_str())) } /// Sampler parameters that are MIDI notes. diff --git a/crates/ui-egui/src/spot.rs b/crates/ui-egui/src/spot.rs index c9b4102b..e92798fe 100644 --- a/crates/ui-egui/src/spot.rs +++ b/crates/ui-egui/src/spot.rs @@ -113,9 +113,44 @@ fn recorded_at(s: &Session, time_reference: u64) -> Samples { i64::try_from(time_reference).unwrap_or(0).saturating_sub(s.timecode_start) } +fn spot_field_row(ui: &mut Ui, s: &Session, d: &mut Spot, i: usize, label: &str) { + let show = |at: Samples, f: TimeFormat| s.format_position(at, f); + ui.label(label); + if d.editable(i) { + let id = Id::new(("spot_field", i)); + if !ui.memory(|m| m.has_focus(id)) { + d.text[i] = show(d.value(i), d.format); + } + let r = ui.add(TextEdit::singleline(&mut d.text[i]).id(id).font(mono(13.0)).desired_width(150.0)); + if r.changed() + && let Ok(v) = s.parse_position(&d.text[i], d.format) + && (-MAX_POSITION..=MAX_POSITION).contains(&v) + { + d.set(i, v); + } + if std::mem::take(&mut d.fresh) { + r.request_focus(); + select_all(ui, id, &d.text[i]); + } + } else { + ui.label(RichText::new(show(d.value(i), d.format)).font(mono(13.0))); + } + ui.end_row(); +} + +fn original_time_stamp_row(ui: &mut Ui, s: &Session, d: &mut Spot, at: Samples) { + ui.label("Original Time Stamp"); + ui.horizontal(|ui| { + ui.label(RichText::new(s.format_position(at, TimeFormat::Timecode)).font(mono(13.0))); + if ui.button("Use").on_hover_text("Spot to where it was recorded").clicked() { + d.start = at; + } + }); + ui.end_row(); +} + /// The dialog's fields; the caller adds OK and Cancel. pub fn fields(ui: &mut Ui, s: &Session, d: &mut Spot) { - let show = |at: Samples, f: TimeFormat| s.format_position(at, f); egui::Grid::new("spot").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| { ui.label("Time Scale"); egui::ComboBox::from_id_salt("spot_scale").selected_text(d.format.label()).show_ui(ui, |ui| { @@ -128,40 +163,13 @@ pub fn fields(ui: &mut Ui, s: &Session, d: &mut Spot) { if i == 1 && matches!(d.target, Target::Trim { .. }) { continue; } - ui.label(label); - if d.editable(i) { - let id = Id::new(("spot_field", i)); - if !ui.memory(|m| m.has_focus(id)) { - d.text[i] = show(d.value(i), d.format); - } - let r = ui.add(TextEdit::singleline(&mut d.text[i]).id(id).font(mono(13.0)).desired_width(150.0)); - if r.changed() - && let Ok(v) = s.parse_position(&d.text[i], d.format) - && (-MAX_POSITION..=MAX_POSITION).contains(&v) - { - d.set(i, v); - } - if std::mem::take(&mut d.fresh) { - r.request_focus(); - select_all(ui, id, &d.text[i]); - } - } else { - ui.label(RichText::new(show(d.value(i), d.format)).font(mono(13.0))); - } - ui.end_row(); + spot_field_row(ui, s, d, i, label); } ui.label("Duration"); ui.label(RichText::new(format_length(d.start, d.len, d.format, s.sample_rate, &s.tempo, s.frame_rate)).font(mono(13.0))); ui.end_row(); if let Some(at) = d.stamp { - ui.label("Original Time Stamp"); - ui.horizontal(|ui| { - ui.label(RichText::new(show(at, TimeFormat::Timecode)).font(mono(13.0))); - if ui.button("Use").on_hover_text("Spot to where it was recorded").clicked() { - d.start = at; - } - }); - ui.end_row(); + original_time_stamp_row(ui, s, d, at); } }); } diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 41d83ac4..c3169790 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -43,10 +43,40 @@ pub fn show(app: &mut SoundApp, ui: &mut Ui) { }); } +struct ModeCellState { + cur: EditMode, + snap: bool, + locked: bool, +} + +fn edit_mode_tooltip(m: EditMode, locked: bool, cmd: &str, fkey: &str) -> String { + match m { + EditMode::Shuffle if locked => format!("Shuffle is locked ({cmd}-click to unlock)"), + EditMode::Shuffle => format!("Shuffle mode ({fkey}); {cmd}-click locks it out"), + EditMode::Grid => format!("Grid mode ({fkey}; again for Relative); Shift-click: Snap to Grid in the other modes"), + _ => format!("{} mode ({fkey}); Shift-click adds Snap to Grid", if m == EditMode::Slip { "Slip" } else { "Spot" }), + } +} + +fn draw_mode_cell(p: &egui::Painter, cell: Rect, resp: &egui::Response, t: &Tokens, m: EditMode, state: &ModeCellState, label: &str) { + let on = state.cur == m || (m == EditMode::Grid && state.cur == EditMode::GridRelative); + key(p, cell, resp, if on { t.mode_on } else { t.mode_off }); + let fg = if on { t.mode_on_text } else { t.mode_off_text }; + if m == EditMode::Grid && state.snap { + p.rect_stroke(cell, 1.0, Stroke::new(1.5, t.mode_on), StrokeKind::Inside); + } + if m == EditMode::Shuffle && state.locked { + icons::draw(p, Rect::from_center_size(cell.center(), vec2(15.0, 15.0)), "lock", fg); + } else { + let txt = if m == EditMode::Grid && state.cur == EditMode::GridRelative { "REL GRID" } else { label }; + p.text(cell.center(), Align2::CENTER_CENTER, txt, bold(10.5), fg); + } +} + fn edit_modes(app: &mut SoundApp, ui: &mut Ui) { let t = Tokens::current(); let ed = &app.engine.session().edit; - let (cur, snap, locked) = (ed.edit_mode, ed.flag(SNAP_TO_GRID), ed.flag(SHUFFLE_LOCK)); + let state = ModeCellState { cur: ed.edit_mode, snap: ed.flag(SNAP_TO_GRID), locked: ed.flag(SHUFFLE_LOCK) }; let cmd = crate::shortcuts::shortcut_label("Cmd", ui.ctx().os() == egui::os::OperatingSystem::Mac); let modes = [ (EditMode::Shuffle, 0, 0, "SHUFFLE", "F1"), @@ -63,28 +93,13 @@ fn edit_modes(app: &mut SoundApp, ui: &mut Ui) { for (m, cx, cy, label, fkey) in modes { let cell = Rect::from_min_size(pos2(inner.min.x + cw * cx as f32, inner.min.y + ch * cy as f32), vec2(cw, ch)).shrink(1.0); let resp = ui.interact(cell, ui.id().with(("mode", label)), Sense::click()); - let on = cur == m || (m == EditMode::Grid && cur == EditMode::GridRelative); - key(ui.painter(), cell, &resp, if on { t.mode_on } else { t.mode_off }); - let fg = if on { t.mode_on_text } else { t.mode_off_text }; - if m == EditMode::Grid && snap { - ui.painter().rect_stroke(cell, 1.0, Stroke::new(1.5, t.mode_on), StrokeKind::Inside); - } - if m == EditMode::Shuffle && locked { - icons::draw(ui.painter(), Rect::from_center_size(cell.center(), vec2(15.0, 15.0)), "lock", fg); - } else { - let txt = if m == EditMode::Grid && cur == EditMode::GridRelative { "REL GRID" } else { label }; - ui.painter().text(cell.center(), Align2::CENTER_CENTER, txt, bold(10.5), fg); - } + draw_mode_cell(ui.painter(), cell, &resp, &t, m, &state, label); + let on = state.cur == m || (m == EditMode::Grid && state.cur == EditMode::GridRelative); resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, true, on, label)); if resp.clicked() { mode_click(app, m, ui.input(|i| i.modifiers)); } - resp.on_hover_text(match m { - EditMode::Shuffle if locked => format!("Shuffle is locked ({cmd}-click to unlock)"), - EditMode::Shuffle => format!("Shuffle mode ({fkey}); {cmd}-click locks it out"), - EditMode::Grid => format!("Grid mode ({fkey}; again for Relative); Shift-click: Snap to Grid in the other modes"), - _ => format!("{} mode ({fkey}); Shift-click adds Snap to Grid", if m == EditMode::Slip { "Slip" } else { "Spot" }), - }); + resp.on_hover_text(edit_mode_tooltip(m, state.locked, &cmd, fkey)); } }); } diff --git a/crates/ui-egui/src/video_window.rs b/crates/ui-egui/src/video_window.rs index 61809e78..3e66e165 100644 --- a/crates/ui-egui/src/video_window.rs +++ b/crates/ui-egui/src/video_window.rs @@ -18,6 +18,43 @@ pub fn show(app: &mut SoundApp, ctx: &egui::Context) { } } +fn video_status_text(app: &SoundApp, pic: Option<&(MovieKey, Result, f64, f64)>) -> String { + match pic { + Some((_, Err(why), ..)) => why.clone(), + Some((k, ..)) => match app.video.info(*k) { + Some(Ok(i)) => format!("{}x{} {:.3} fps {}", i.width, i.height, i.frame_rate, i.codec_detail), + Some(Err(e)) => e.clone(), + None => "opening…".into(), + }, + None => String::new(), + } +} + +fn draw_video_surface(app: &SoundApp, painter: &egui::Painter, img: Rect, pic: Option<&(MovieKey, Result, f64, f64)>, on: bool) { + let msg = if !on { + Some("Video Track Offline".to_string()) + } else { + match pic { + None => None, + Some((_, Err(why), ..)) => Some(why.clone()), + Some((k, ..)) => match (app.video.current(*k), app.video.current_error(*k)) { + (Some(tex), _) => { + painter.image(tex.id(), img, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); + None + } + (None, Some(e)) => Some(e.to_string()), + (None, None) => match app.video.info(*k) { + Some(Err(e)) => Some(e.clone()), + _ => None, + }, + }, + } + }; + if let Some(m) = msg { + painter.text(img.center(), Align2::CENTER_CENTER, m, regular(12.0), Color32::from_gray(170)); + } +} + fn video(app: &mut SoundApp, ctx: &egui::Context) { app.video.poll(ctx); let pos = app.position(); @@ -38,15 +75,7 @@ fn video(app: &mut SoundApp, ctx: &egui::Context) { egui::Window::new("Video").open(&mut open).default_size(vec2(480.0, 480.0 / aspect + 28.0)).resizable(true).show(ctx, |ui| { ui.horizontal(|ui| { crate::widgets::checkbox(ui, &mut burn, "Timecode burn-in"); - let text = match &pic { - Some((_, Err(why), ..)) => why.clone(), - Some((k, ..)) => match app.video.info(*k) { - Some(Ok(i)) => format!("{}x{} {:.3} fps {}", i.width, i.height, i.frame_rate, i.codec_detail), - Some(Err(e)) => e.clone(), - None => "opening…".into(), - }, - None => String::new(), - }; + let text = video_status_text(app, pic.as_ref()); ui.label(egui::RichText::new(text).font(regular(10.5)).color(Color32::from_gray(150))); }); let avail = ui.available_rect_before_wrap(); @@ -60,28 +89,7 @@ fn video(app: &mut SoundApp, ctx: &egui::Context) { w = h * aspect; } let img = Rect::from_center_size(r.center(), vec2(w, h)); - let msg = if !on { - Some("Video Track Offline".to_string()) - } else { - match &pic { - None => None, - Some((_, Err(why), ..)) => Some(why.clone()), - Some((k, ..)) => match (app.video.current(*k), app.video.current_error(*k)) { - (Some(tex), _) => { - painter.image(tex.id(), img, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); - None - } - (None, Some(e)) => Some(e.to_string()), - (None, None) => match app.video.info(*k) { - Some(Err(e)) => Some(e.clone()), - _ => None, - }, - }, - } - }; - if let Some(m) = msg { - painter.text(img.center(), Align2::CENTER_CENTER, m, regular(12.0), Color32::from_gray(170)); - } + draw_video_surface(app, &painter, img, pic.as_ref(), on); if burn { let tc = s.format_position(pos, TimeFormat::Timecode); let size = (h / 12.0).clamp(10.0, 36.0); diff --git a/crates/ui-egui/src/windows.rs b/crates/ui-egui/src/windows.rs index 78a91081..62bb3ae2 100644 --- a/crates/ui-egui/src/windows.rs +++ b/crates/ui-egui/src/windows.rs @@ -341,7 +341,7 @@ fn workspace(app: &mut SoundApp, ctx: &egui::Context) { return None; } let is_dir = e.file_type().is_ok_and(|t| t.is_dir()); - let ext = std::path::Path::new(&name).extension().and_then(|x| x.to_str()).unwrap_or("").to_ascii_lowercase(); + let ext = std::path::Path::new(&name).extension().and_then(std::ffi::OsStr::to_str).unwrap_or("").to_ascii_lowercase(); (is_dir || ["wav", "aif", "aiff", "flac", "mp3", "ogg", "m4a", "m4b", "caf", "mid", "midi", "scraft"].contains(&ext.as_str())) .then_some((name, is_dir)) }) diff --git a/crates/video/src/h264/cabac_mb.rs b/crates/video/src/h264/cabac_mb.rs index 76c9c9a5..3379e193 100644 --- a/crates/video/src/h264/cabac_mb.rs +++ b/crates/video/src/h264/cabac_mb.rs @@ -240,11 +240,11 @@ impl SliceDecoder<'_> { if c.decode_decision(base + inc) == 0 { return 0; } - let mut prefix = 1; - while prefix < 9 && c.decode_decision(base + (prefix + 2).min(6)) == 1 { + let mut prefix: i32 = 1; + while prefix < 9 && c.decode_decision(base + (prefix + 2).min(6) as usize) == 1 { prefix += 1; } - let mut val = prefix as i32; + let mut val = prefix; if prefix >= 9 { // EG3 suffix let mut k = 3; diff --git a/crates/video/src/h264/params.rs b/crates/video/src/h264/params.rs index 99232ac7..0970a106 100644 --- a/crates/video/src/h264/params.rs +++ b/crates/video/src/h264/params.rs @@ -575,7 +575,7 @@ impl Pps { ensure!(sps_id < 32, "seq_parameter_set_id out of range"); let sps = spss .get(sps_id as usize) - .and_then(|s| s.as_ref()) + .and_then(Option::as_ref) .ok_or_else(|| crate::h264::Error::MissingParameterSet(format!("SPS {sps_id} for PPS {id}")))?; let entropy_coding_mode = r.read_flag()?; let bottom_field_pic_order_in_frame_present = r.read_flag()?; diff --git a/crates/video/src/prores/decode.rs b/crates/video/src/prores/decode.rs index fcb8b07f..01b1900f 100644 --- a/crates/video/src/prores/decode.rs +++ b/crates/video/src/prores/decode.rs @@ -200,7 +200,7 @@ pub fn decode_frame_into(data: &[u8], opts: &DecodeOptions, out: &mut Frame) -> let mut ac = alpha.as_mut().map(|a| a.chunks_mut(band_h * width)); while let (Some(y), Some(cb), Some(cr)) = (yc.next(), cbc.next(), crc.next()) { let rows = y.len() / width; - let a = ac.as_mut().and_then(|it| it.next()); + let a = ac.as_mut().and_then(Iterator::next); bands.push(Band { y, cb, cr, a, rows }); } } diff --git a/crates/vst3-host/src/scan.rs b/crates/vst3-host/src/scan.rs index f6a2b211..e9ea2962 100644 --- a/crates/vst3-host/src/scan.rs +++ b/crates/vst3-host/src/scan.rs @@ -79,7 +79,7 @@ pub fn os_search_paths(os: Os, home: Option, common_program_files: Opti /// True when the path names a VST3 plugin (`*.vst3`, case-insensitive). pub fn is_vst3_path(p: &Path) -> bool { - p.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("vst3")) + p.extension().and_then(std::ffi::OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("vst3")) } /// Recursively collects `.vst3` bundles and files under `dirs` (missing directories are skipped). @@ -181,7 +181,7 @@ pub fn bundle_binary_for(os: Os, path: &Path) -> Option { .flatten() .map(|e| e.path()) .filter(|p| p.is_file()) - .filter(|p| ext.is_none_or(|e| p.extension().and_then(|x| x.to_str()).is_some_and(|x| x.eq_ignore_ascii_case(e)))) + .filter(|p| ext.is_none_or(|e| p.extension().and_then(std::ffi::OsStr::to_str).is_some_and(|x| x.eq_ignore_ascii_case(e)))) .take(64) .collect(); files.sort(); diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index 72d95f8b..e23fed92 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -131,6 +131,10 @@ if has appimage; then cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png" ln -s "$APP_ID.png" "$APPDIR/.DirIcon" + verify_sha256() { + local sha="$1" file="$2" + printf '%s %s\n' "$sha" "$file" | sha256sum -c - + } TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}" if [[ -z "$TOOL" ]]; then if [[ -z "$APPIMAGETOOL_SHA256" ]]; then @@ -142,21 +146,21 @@ if has appimage; then download="$TOOL.download" curl --proto '=https' -fsSL -o "$download" \ "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" - printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c - + verify_sha256 "$APPIMAGETOOL_SHA256" "$download" chmod 755 "$download" mv "$download" "$TOOL" fi - printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c - + verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL" fi RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH" if [[ ! -f "$RUNTIME" ]]; then download="$RUNTIME.download" curl --proto '=https' -fsSL -o "$download" \ "https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH" - printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c - + verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$download" mv "$download" "$RUNTIME" fi - printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c - + verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be # relative). OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage" diff --git a/xtask/src/main.rs b/xtask/src/main.rs index c2da6561..79b67325 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -97,7 +97,7 @@ fn assets() -> Result<(), String> { let attribution = std::fs::read_to_string(root().join("ATTRIBUTION.md")).map_err(|e| format!("ATTRIBUTION.md: {e}"))?; let mut missing = Vec::new(); for f in files.lines() { - let ext = Path::new(f).extension().and_then(|x| x.to_str()).unwrap_or("").to_ascii_lowercase(); + let ext = Path::new(f).extension().and_then(std::ffi::OsStr::to_str).unwrap_or("").to_ascii_lowercase(); if !ASSET_EXT.contains(&ext.as_str()) { continue; }