chore: resolve SonarCloud maintainability issues (#1003)

* chore: resolve SonarCloud maintainability issues

* fix(packaging): extract verify_sha256 helper in package.sh

* style: apply rustfmt formatting

* fix(video): ensure prefix is i32 in cabac_mb
This commit is contained in:
Dylan M. Taylor
2026-10-23 22:33:59 -04:00
committed by GitHub
parent 6b84a4f046
commit 64669e9a7c
30 changed files with 296 additions and 222 deletions
+3 -3
View File
@@ -110,7 +110,7 @@ fn info(args: &[String]) -> ExitCode {
Ok(b) => b, Ok(b) => b,
Err(e) => return fail(format!("{path}: {e}")), 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) { match soundcraft_audio_io::probe(&bytes, ext) {
Ok(i) => { 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()); 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, Ok(b) => b,
Err(e) => return fail(format!("{input}: {e}")), 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) { let (_, mut buf) = match soundcraft_audio_io::decode(&bytes, ext) {
Ok(x) => x, Ok(x) => x,
Err(e) => return fail(format!("{input}: {e}")), Err(e) => return fail(format!("{input}: {e}")),
@@ -146,7 +146,7 @@ fn convert(args: &[String]) -> ExitCode {
if args.iter().any(|a| a == "--normalize") { if args.iter().any(|a| a == "--normalize") {
soundcraft_dsp::offline::normalize(&mut buf.channels, -0.1, false); 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 { 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)); return fail(format!("{output}: cannot write `.{out_ext}` files (supported: {})", soundcraft_audio_io::ENCODE_EXTENSIONS));
}; };
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>SoundCraft</title> <title>SoundCraft</title>
<link data-trunk rel="icon" type="image/png" href="../../assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png" /> <link data-trunk rel="icon" type="image/png" href="../../assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png" />
<link data-trunk rel="rust" data-bin="soundcraft-web" data-wasm-opt="z" data-weak-refs /> <link data-trunk rel="rust" data-bin="soundcraft-web" data-wasm-opt="z" data-weak-refs />
+1 -1
View File
@@ -182,7 +182,7 @@ fn services() -> Services {
if let Some(dir) = path.parent().filter(|dir| dir.is_dir()) { if let Some(dir) = path.parent().filter(|dir| dir.is_dir()) {
d = d.set_directory(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 = d.set_file_name(n);
} }
d.save_file().map(|p| p.to_string_lossy().into_owned()) d.save_file().map(|p| p.to_string_lossy().into_owned())
+1 -1
View File
@@ -51,7 +51,7 @@ pub fn os_search_paths(home: Option<PathBuf>, common_program_files: Option<PathB
/// True when the path names a CLAP plugin (`*.clap`, case-insensitive). /// True when the path names a CLAP plugin (`*.clap`, case-insensitive).
pub fn is_clap_path(p: &Path) -> bool { pub fn is_clap_path(p: &Path) -> 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). /// Recursively collects `.clap` files and bundles under `dirs` (missing directories are skipped).
+30 -20
View File
@@ -50,7 +50,7 @@ pub fn export(e: &Engine, ids: &[ClipId], path: &str) -> Result<usize> {
}; };
let n = clips.len(); let n = clips.len();
lanes.sort_by_key(|(i, _)| *i); 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 audio_dir_name = format!("{} Audio", crate::io::sanitize_name(&stem));
let dir = p.parent().map(Path::to_path_buf).unwrap_or_default(); let dir = p.parent().map(Path::to_path_buf).unwrap_or_default();
let mut audio = BTreeMap::new(); let mut audio = BTreeMap::new();
@@ -80,26 +80,17 @@ pub fn export(e: &Engine, ids: &[ClipId], path: &str) -> Result<usize> {
Ok(n) Ok(n)
} }
/// Import a clip group file at `at`, one lane per track starting at `first` (new audio tracks fn decode_group_audio(
/// are made when the session runs out). Returns the ids of the placed clips. audio_entries: &BTreeMap<u64, String>,
pub fn import(e: &mut Engine, path: &str, first: Option<TrackId>, at: Samples) -> Result<Vec<ClipId>> { dir: &Path,
let id = "file.import_clip_groups"; rate: u32,
let text = crate::io::read_text(path)?; cmd_id: &str,
let file: GroupFile = serde_json::from_str(&text).map_err(|err| EngineError::BadParams(id.into(), format!("not a clip group file: {err}")))?; ) -> Result<(Vec<(String, soundcraft_audio_io::AudioBuffer)>, BTreeMap<u64, usize>)> {
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::<usize>() > 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 (mut decoded, mut index, mut file_of) = (Vec::new(), BTreeMap::new(), BTreeMap::new()); 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 audio_entries {
for (old, rel) in &file.audio {
let full = dir.join(rel); let full = dir.join(rel);
if !full.starts_with(&dir) || rel.contains("..") { if !full.starts_with(dir) || rel.contains("..") {
return Err(EngineError::BadParams(id.into(), format!("audio path escapes the group folder: {rel}"))); return Err(EngineError::BadParams(cmd_id.into(), format!("audio path escapes the group folder: {rel}")));
} }
let key = crate::io::file_key(&full); let key = crate::io::file_key(&full);
let n = match index.get(&key) { let n = match index.get(&key) {
@@ -116,6 +107,25 @@ pub fn import(e: &mut Engine, path: &str, first: Option<TrackId>, at: Samples) -
}; };
file_of.insert(*old, n); 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<TrackId>, at: Samples) -> Result<Vec<ClipId>> {
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::<usize>() > 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 src_rate = f64::from(file.sample_rate.max(1));
let s = e.session_mut(); let s = e.session_mut();
let ratio = f64::from(s.sample_rate.hz()) / src_rate; let ratio = f64::from(s.sample_rate.hz()) / src_rate;
@@ -123,7 +133,7 @@ pub fn import(e: &mut Engine, path: &str, first: Option<TrackId>, at: Samples) -
let sources: Vec<SourceId> = decoded let sources: Vec<SourceId> = decoded
.into_iter() .into_iter()
.map(|(rel, buf)| { .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) crate::io::add_source(s, &name, buf, None, FileFormat::Wav)
}) })
.collect(); .collect();
+2 -2
View File
@@ -241,7 +241,7 @@ fn import_audio(e: &mut Engine, p: &Value) -> Result<Value> {
let mut out = Vec::new(); let mut out = Vec::new();
for path in &paths { for path in &paths {
let bytes = crate::io::read_file(path)?; 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)?; let r = crate::io::import_audio_bytes(e, &name, &bytes, Some(path.as_str()), target, at)?;
out.push(r); out.push(r);
} }
@@ -302,7 +302,7 @@ pub fn bounce_job(e: &Engine, p: &Value) -> Result<crate::io::Bounce> {
let path = str_param(p, "path").ok_or_else(|| bad("file.bounce_mix", "`path` required"))?.to_string(); 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 = 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 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_name = str_param(p, "format").unwrap_or(ext.as_str());
let format = soundcraft_audio_io::encode_format_for(format_name).ok_or_else(|| { 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)) bad("file.bounce_mix", format!("cannot write `{format_name}` files (supported: {})", soundcraft_audio_io::ENCODE_EXTENSIONS))
+53 -39
View File
@@ -12,6 +12,55 @@ pub fn specs() -> Vec<CommandSpec> {
] ]
} }
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<Value>,
) {
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<Value> { fn report(e: &mut Engine, _: &Value) -> Result<Value> {
let s = e.session(); let s = e.session();
let sources: BTreeMap<_, _> = s.sources.iter().map(|source| (source.id, source)).collect(); let sources: BTreeMap<_, _> = s.sources.iter().map(|source| (source.id, source)).collect();
@@ -24,51 +73,16 @@ fn report(e: &mut Engine, _: &Value) -> Result<Value> {
for track in &s.tracks { for track in &s.tracks {
for (index, playlist) in track.playlists.iter().enumerate() { for (index, playlist) in track.playlists.iter().enumerate() {
let active = index == track.active_playlist; let active = index == track.active_playlist;
let ctx = ClipContext { track, playlist, active, session_sample_rate: s.sample_rate.hz() };
for clip in &playlist.clips { for clip in &playlist.clips {
let ClipContent::Audio { source, offset } = clip.content else { continue }; let ClipContent::Audio { source, offset } = clip.content else { continue };
clips += 1; clips += 1;
if active { if active {
active_end = active_end.max(clip.end()); active_end = active_end.max(clip.end());
} }
let metadata = sources.get(&source); let metadata = sources.get(&source).copied();
let audio = s.pool.get(source); let audio = s.pool.get(source).map(|a| a.as_ref());
let mut add = |code: &str, message: &str| { check_audio_clip(&ctx, clip, source, offset, metadata, audio, &mut issues);
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.");
}
} }
} }
} }
+1 -1
View File
@@ -81,7 +81,7 @@ impl CommandSpec {
CommandInfo { CommandInfo {
id: self.id.into(), id: self.id.into(),
label: self.label.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), shortcut: self.shortcut.map(str::to_string),
params: self.params.into(), params: self.params.into(),
enabled: en.is_ok(), enabled: en.is_ok(),
+2 -2
View File
@@ -56,7 +56,7 @@ fn load(e: &mut Engine, p: &Value) -> Result<Value> {
} }
} }
let bytes = crate::io::read_file(&path)?; 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) 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")); 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 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}")))?; 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 { if buf.frames() == 0 || buf.num_channels() == 0 {
return Err(EngineError::Io(format!("{name}: no audio"))); return Err(EngineError::Io(format!("{name}: no audio")));
+16 -15
View File
@@ -5,6 +5,7 @@ use serde_json::{Value, json};
use soundcraft_audio_io::{AudioBuffer, EncodeOptions, FileFormat}; use soundcraft_audio_io::{AudioBuffer, EncodeOptions, FileFormat};
use soundcraft_model::{BusId, ChannelFormat, Clip, ClipContent, Session, Source, SourceAudio, SourceId, TrackId, TrackKind}; use soundcraft_model::{BusId, ChannelFormat, Clip, ClipContent, Session, Source, SourceAudio, SourceId, TrackId, TrackKind};
use soundcraft_time::{Range, Samples}; use soundcraft_time::{Range, Samples};
use std::ffi::OsStr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; 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; buf.sample_rate = sr;
} }
let id = SourceId(s.alloc()); 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 { let stored = match path {
Some(p) => p.to_string(), Some(p) => p.to_string(),
None => unique_audio_rel(s, &stem), 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. /// 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<TrackId>, at: Samples) -> Result<Value> { pub fn import_audio_bytes(e: &mut Engine, name: &str, bytes: &[u8], path: Option<&str>, target: Option<TrackId>, at: Samples) -> Result<Value> {
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 (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) 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")))?; .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 { let track = match target {
Some(t) => t, Some(t) => t,
None => { 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)) 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 /// 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. /// only when the file has neither a readable picture nor audio.
pub fn import_video_bytes(e: &mut Engine, path: &str, bytes: Vec<u8>, at: Samples) -> Result<Value> { pub fn import_video_bytes(e: &mut Engine, path: &str, bytes: Vec<u8>, at: Samples) -> Result<Value> {
let stem = Path::new(path).file_stem().and_then(|n| n.to_str()).unwrap_or("Video").to_string(); 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(|x| x.to_str()).unwrap_or("mp4").to_ascii_lowercase(); let ext = Path::new(path).extension().and_then(OsStr::to_str).unwrap_or("mp4").to_ascii_lowercase();
let bytes: Arc<[u8]> = bytes.into(); let bytes: Arc<[u8]> = bytes.into();
let movie = soundcraft_video::Movie::open(Arc::clone(&bytes)).map(|m| m.info().clone()); 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() }); 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<u8>, at: Sample
let vid = SourceId(s.alloc()); let vid = SourceId(s.alloc());
s.videos.push(soundcraft_model::VideoSource { s.videos.push(soundcraft_model::VideoSource {
id: vid, 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, path: abs,
width: info.width, width: info.width,
height: info.height, height: info.height,
@@ -992,7 +993,7 @@ fn write_session(session: &Session, path: &str, overwrite: bool) -> Result<(Sess
sync_dir(&audio_dir); sync_dir(&audio_dir);
} }
s.folder = Some(dir.clone()); 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(); s.name = stem.to_string();
} }
let text = s.to_json().map_err(|err| EngineError::Io(err.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/<name> backup <n>.<ext>`, keeping the /// Copy the session file about to be replaced to `Backups/<name> backup <n>.<ext>`, keeping the
/// newest ten. /// newest ten.
fn back_up(file: &Path) { 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 { else {
return; return;
}; };
@@ -1195,7 +1196,7 @@ pub(crate) fn decode_file(files: &mut DecodedFiles, p: &Path, rate: u32) -> std:
.entry(file_key(p)) .entry(file_key(p))
.or_insert_with(|| { .or_insert_with(|| {
let bytes = read_file(p).map_err(|_| p.display().to_string())?; 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() .clone()
} }
@@ -1227,7 +1228,7 @@ pub fn set_deliver(h: DeliverHook) {
pub fn write_file(path: &str, bytes: &[u8]) -> Result<()> { pub fn write_file(path: &str, bytes: &[u8]) -> Result<()> {
if cfg!(target_arch = "wasm32") { if cfg!(target_arch = "wasm32") {
let deliver = DELIVER.get().ok_or_else(|| EngineError::Io("saving files is not available in this build".into()))?; 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); return deliver(name, bytes).map_err(EngineError::Io);
} }
crate::file_access::check_write(path).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<u8>, Session)> {
/// Save as a bundle named after `path`'s file name, delivered through the host ([`write_file`]). /// 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<usize> { fn save_session_bundle(e: &mut Engine, path: &str) -> Result<usize> {
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(); let mut s = e.session().clone();
if let Some(stem) = stem { if let Some(stem) = stem {
s.name = stem; s.name = stem;
@@ -1311,7 +1312,7 @@ pub fn open_session_bytes(e: &mut Engine, name: &str, bytes: &[u8]) -> Result<Ve
continue; continue;
} }
let audio = match files.remove(&src.path) { let audio = match files.remove(&src.path) {
Some(bytes) => 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), Ok(audio) => Some(audio),
Err(err) => { Err(err) => {
missing.push(format!("{}: {err}", src.path)); missing.push(format!("{}: {err}", src.path));
@@ -1330,7 +1331,7 @@ pub fn open_session_bytes(e: &mut Engine, name: &str, bytes: &[u8]) -> Result<Ve
} }
// Movies are referenced by path, never copied into a session: none can be found here. // Movies are referenced by path, never copied into a session: none can be found here.
missing.extend(s.videos.iter().map(|v| v.path.clone())); missing.extend(s.videos.iter().map(|v| v.path.clone()));
let stem = Path::new(name).file_stem().and_then(|x| x.to_str()).filter(|x| !x.is_empty()).unwrap_or("Untitled").to_string(); let stem = Path::new(name).file_stem().and_then(OsStr::to_str).filter(|x| !x.is_empty()).unwrap_or("Untitled").to_string();
e.replace_session(s); e.replace_session(s);
e.path = Some(format!("{}.{BUNDLE_EXTENSION}", sanitize_name(&stem))); e.path = Some(format!("{}.{BUNDLE_EXTENSION}", sanitize_name(&stem)));
for m in &missing { for m in &missing {
@@ -1362,7 +1363,7 @@ fn read_bundle(bytes: &[u8]) -> Result<(String, std::collections::HashMap<String
let mut data = Vec::new(); let mut data = Vec::new();
// The declared size is hostile too: never read past it. // The declared size is hostile too: never read past it.
f.by_ref().take(size).read_to_end(&mut data).map_err(|err| EngineError::Io(format!("bundle: {rel}: {err}")))?; f.by_ref().take(size).read_to_end(&mut data).map_err(|err| EngineError::Io(format!("bundle: {rel}: {err}")))?;
let is_session = !rel.contains('/') && Path::new(&rel).extension().and_then(|x| x.to_str()) == Some(soundcraft_model::SESSION_EXTENSION); let is_session = !rel.contains('/') && Path::new(&rel).extension().and_then(OsStr::to_str) == Some(soundcraft_model::SESSION_EXTENSION);
if is_session && text.is_none() { if is_session && text.is_none() {
text = Some(String::from_utf8(data).map_err(|_| EngineError::Io(format!("bundle: {rel} is not text")))?); text = Some(String::from_utf8(data).map_err(|_| EngineError::Io(format!("bundle: {rel} is not text")))?);
} else { } else {
@@ -1451,7 +1452,7 @@ fn add_recording_in(
continue; continue;
} }
let resampled = buf.sample_rate != s.sample_rate.hz(); let resampled = buf.sample_rate != s.sample_rate.hz();
let name = path.file_stem().and_then(|x| x.to_str()).unwrap_or("Take"); let name = path.file_stem().and_then(OsStr::to_str).unwrap_or("Take");
let src = add_source(s, name, buf, Some(&path.to_string_lossy()), FileFormat::Wav); let src = add_source(s, name, buf, Some(&path.to_string_lossy()), FileFormat::Wav);
let Some(x) = s.sources.last_mut() else { continue }; let Some(x) = s.sources.last_mut() else { continue };
x.unsaved = resampled; x.unsaved = resampled;
+1 -1
View File
@@ -449,7 +449,7 @@ fn install_panic_hook() {
} }
fn panic_message(p: &(dyn std::any::Any + Send)) -> String { fn panic_message(p: &(dyn std::any::Any + Send)) -> String {
p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into()) p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
} }
#[cfg(test)] #[cfg(test)]
+1 -1
View File
@@ -196,7 +196,7 @@ impl ChannelFormat {
pub fn channel_label(self, ch: usize) -> String { pub fn channel_label(self, ch: usize) -> String {
match self.speakers().get(ch) { match self.speakers().get(ch) {
Some(s) => s.label().to_string(), 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), None => format!("{}", ch + 1),
} }
} }
+1 -1
View File
@@ -288,7 +288,7 @@ impl Default for EditState {
timeline_selection: Range::default(), timeline_selection: Range::default(),
flags: ["view.clip.name", "view.clip.gain_line", "waveform.peak", "view.clip.overlap_shadows", "view.marker.ruler_lines"] flags: ["view.clip.name", "view.clip.gain_line", "waveform.peak", "view.clip.overlap_shadows", "view.marker.ruler_lines"]
.iter() .iter()
.map(|s| s.to_string()) .map(ToString::to_string)
.collect(), .collect(),
values: std::collections::BTreeMap::new(), values: std::collections::BTreeMap::new(),
} }
+1 -1
View File
@@ -124,7 +124,7 @@ pub fn parse_position(
} }
TimeFormat::BarsBeats => { TimeFormat::BarsBeats => {
let parts: Vec<i64> = let parts: Vec<i64> =
t.split(['|', '.', ' ']).filter(|p| !p.is_empty()).map(|p| p.parse::<i64>()).collect::<Result<_, _>>().map_err(|_| err())?; t.split(['|', '.', ' ']).filter(|p| !p.is_empty()).map(str::parse::<i64>).collect::<Result<_, _>>().map_err(|_| err())?;
let bb = BarBeat { let bb = BarBeat {
bar: *parts.first().ok_or_else(err)?, bar: *parts.first().ok_or_else(err)?,
beat: parts.get(1).copied().unwrap_or(1).max(1), beat: parts.get(1).copied().unwrap_or(1).max(1),
+20 -12
View File
@@ -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. fn contributors_toolbar(ui: &mut egui::Ui, v: &mut View) {
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::<View>(id)).unwrap_or_default();
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
ui.label("Show"); ui.label("Show");
for m in NameMode::ALL { for m in NameMode::ALL {
@@ -242,6 +239,24 @@ pub fn contributors_ui(ui: &mut egui::Ui) {
v.table = true; 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::<View>(id)).unwrap_or_default();
contributors_toolbar(ui, &mut v);
let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending); 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.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().weak());
ui.separator(); ui.separator();
@@ -251,14 +266,7 @@ pub fn contributors_ui(ui: &mut egui::Ui) {
} else if v.table { } else if v.table {
table(ui, &list, &mut v); table(ui, &list, &mut v);
} else { } else {
ui.horizontal_wrapped(|ui| { contributors_grab_bag(ui, &list, v.names);
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());
}
});
} }
}); });
ui.data_mut(|d| d.insert_temp(id, v)); ui.data_mut(|d| d.insert_temp(id, v));
+1 -1
View File
@@ -776,7 +776,7 @@ impl SoundApp {
for d in std::fs::read_dir(dir).ok()?.flatten() { for d in std::fs::read_dir(dir).ok()?.flatten() {
for f in std::fs::read_dir(d.path()).into_iter().flatten().flatten() { for f in std::fs::read_dir(d.path()).into_iter().flatten().flatten() {
let p = f.path(); 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()) && let Ok(m) = f.metadata().and_then(|m| m.modified())
&& best.as_ref().is_none_or(|(t, _)| m > *t) && best.as_ref().is_none_or(|(t, _)| m > *t)
{ {
+2 -2
View File
@@ -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. /// 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)] { fn mix_view_aliases() -> &'static [(&'static str, &'static str)] {
static ALIASES: std::sync::OnceLock<Vec<(&'static str, &'static str)>> = std::sync::OnceLock::new(); static ALIASES: std::sync::OnceLock<Vec<(&str, &str)>> = std::sync::OnceLock::new();
ALIASES.get_or_init(|| { ALIASES.get_or_init(|| {
[ [
"Inserts A-E", "Inserts A-E",
@@ -633,7 +633,7 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
match sec { match sec {
"all" => { "all" => {
app.ui.mix_views = 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(), "minimal" => app.ui.mix_views.clear(),
"" => return Some(Err("`section` required".into())), "" => return Some(Err("`section` required".into())),
+26 -22
View File
@@ -371,6 +371,29 @@ fn big_counter(app: &mut SoundApp, ctx: &egui::Context) {
app.ui.show_big_counter = open; 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::<String>(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) { pub(crate) fn memory_locations(app: &mut SoundApp, ui: &mut Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("+ Marker").clicked() { 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.label(egui::RichText::new(h).strong());
} }
ui.end_row(); ui.end_row();
for (n, name, loc, len, kind) in rows { for (n, name, loc, len, kind) in &rows {
if ui.button(n.to_string()).on_hover_text("Recall").clicked() { memory_location_row(app, ui, *n, name, loc, len, kind);
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::<String>(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();
} }
}); });
}); });
@@ -669,7 +673,7 @@ fn preset_menu(
return; return;
}; };
let mut names: Vec<String> = std::fs::read_dir(&dir) let mut names: Vec<String> = 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(); .unwrap_or_default();
names.sort(); names.sort();
for n in &names { for n in &names {
+3 -2
View File
@@ -13,6 +13,7 @@
use crate::SoundApp; use crate::SoundApp;
use serde_json::json; use serde_json::json;
use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
/// How many sessions the menu keeps. /// How many sessions the menu keeps.
@@ -45,8 +46,8 @@ pub fn note(list: &mut Vec<String>, path: &str) -> bool {
/// "name — folder" for a session path. /// "name — folder" for a session path.
fn label(path: &str) -> String { fn label(path: &str) -> String {
let p = Path::new(path); let p = Path::new(path);
let name = p.file_stem().and_then(|n| n.to_str()).unwrap_or(path); let name = p.file_stem().and_then(OsStr::to_str).unwrap_or(path);
match p.parent().and_then(|d| d.file_name()).and_then(|n| n.to_str()) { match p.parent().and_then(Path::file_name).and_then(OsStr::to_str) {
Some(folder) => format!("{name} — {folder}"), Some(folder) => format!("{name} — {folder}"),
None => name.to_string(), None => name.to_string(),
} }
+2 -1
View File
@@ -8,6 +8,7 @@ use crate::theme::{Tokens, mono, regular};
use egui::{Align2, Color32, CursorIcon, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use egui::{Align2, Color32, CursorIcon, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use serde_json::json; use serde_json::json;
use soundcraft_model::{Insert, TrackId}; use soundcraft_model::{Insert, TrackId};
use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
/// The built-in Sampler's plugin id. /// 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. /// Whether a file name looks like audio the sampler can load.
pub fn is_audio_name(name: &str) -> bool { 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. /// Sampler parameters that are MIDI notes.
+38 -30
View File
@@ -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) 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. /// The dialog's fields; the caller adds OK and Cancel.
pub fn fields(ui: &mut Ui, s: &Session, d: &mut Spot) { 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| { egui::Grid::new("spot").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label("Time Scale"); ui.label("Time Scale");
egui::ComboBox::from_id_salt("spot_scale").selected_text(d.format.label()).show_ui(ui, |ui| { 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 { .. }) { if i == 1 && matches!(d.target, Target::Trim { .. }) {
continue; continue;
} }
ui.label(label); spot_field_row(ui, s, d, i, 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();
} }
ui.label("Duration"); 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.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(); ui.end_row();
if let Some(at) = d.stamp { if let Some(at) = d.stamp {
ui.label("Original Time Stamp"); original_time_stamp_row(ui, s, d, at);
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();
} }
}); });
} }
+34 -19
View File
@@ -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) { fn edit_modes(app: &mut SoundApp, ui: &mut Ui) {
let t = Tokens::current(); let t = Tokens::current();
let ed = &app.engine.session().edit; 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 cmd = crate::shortcuts::shortcut_label("Cmd", ui.ctx().os() == egui::os::OperatingSystem::Mac);
let modes = [ let modes = [
(EditMode::Shuffle, 0, 0, "SHUFFLE", "F1"), (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 { 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 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 resp = ui.interact(cell, ui.id().with(("mode", label)), Sense::click());
let on = cur == m || (m == EditMode::Grid && cur == EditMode::GridRelative); draw_mode_cell(ui.painter(), cell, &resp, &t, m, &state, label);
key(ui.painter(), cell, &resp, if on { t.mode_on } else { t.mode_off }); let on = state.cur == m || (m == EditMode::Grid && state.cur == EditMode::GridRelative);
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);
}
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, true, on, label)); resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, true, on, label));
if resp.clicked() { if resp.clicked() {
mode_click(app, m, ui.input(|i| i.modifiers)); mode_click(app, m, ui.input(|i| i.modifiers));
} }
resp.on_hover_text(match m { resp.on_hover_text(edit_mode_tooltip(m, state.locked, &cmd, fkey));
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" }),
});
} }
}); });
} }
+39 -31
View File
@@ -18,6 +18,43 @@ pub fn show(app: &mut SoundApp, ctx: &egui::Context) {
} }
} }
fn video_status_text(app: &SoundApp, pic: Option<&(MovieKey, Result<String, String>, 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<String, String>, 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) { fn video(app: &mut SoundApp, ctx: &egui::Context) {
app.video.poll(ctx); app.video.poll(ctx);
let pos = app.position(); 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| { 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| { ui.horizontal(|ui| {
crate::widgets::checkbox(ui, &mut burn, "Timecode burn-in"); crate::widgets::checkbox(ui, &mut burn, "Timecode burn-in");
let text = match &pic { let text = video_status_text(app, pic.as_ref());
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(),
};
ui.label(egui::RichText::new(text).font(regular(10.5)).color(Color32::from_gray(150))); ui.label(egui::RichText::new(text).font(regular(10.5)).color(Color32::from_gray(150)));
}); });
let avail = ui.available_rect_before_wrap(); let avail = ui.available_rect_before_wrap();
@@ -60,28 +89,7 @@ fn video(app: &mut SoundApp, ctx: &egui::Context) {
w = h * aspect; w = h * aspect;
} }
let img = Rect::from_center_size(r.center(), vec2(w, h)); let img = Rect::from_center_size(r.center(), vec2(w, h));
let msg = if !on { draw_video_surface(app, &painter, img, pic.as_ref(), 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));
}
if burn { if burn {
let tc = s.format_position(pos, TimeFormat::Timecode); let tc = s.format_position(pos, TimeFormat::Timecode);
let size = (h / 12.0).clamp(10.0, 36.0); let size = (h / 12.0).clamp(10.0, 36.0);
+1 -1
View File
@@ -341,7 +341,7 @@ fn workspace(app: &mut SoundApp, ctx: &egui::Context) {
return None; return None;
} }
let is_dir = e.file_type().is_ok_and(|t| t.is_dir()); 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())) (is_dir || ["wav", "aif", "aiff", "flac", "mp3", "ogg", "m4a", "m4b", "caf", "mid", "midi", "scraft"].contains(&ext.as_str()))
.then_some((name, is_dir)) .then_some((name, is_dir))
}) })
+3 -3
View File
@@ -240,11 +240,11 @@ impl SliceDecoder<'_> {
if c.decode_decision(base + inc) == 0 { if c.decode_decision(base + inc) == 0 {
return 0; return 0;
} }
let mut prefix = 1; let mut prefix: i32 = 1;
while prefix < 9 && c.decode_decision(base + (prefix + 2).min(6)) == 1 { while prefix < 9 && c.decode_decision(base + (prefix + 2).min(6) as usize) == 1 {
prefix += 1; prefix += 1;
} }
let mut val = prefix as i32; let mut val = prefix;
if prefix >= 9 { if prefix >= 9 {
// EG3 suffix // EG3 suffix
let mut k = 3; let mut k = 3;
+1 -1
View File
@@ -575,7 +575,7 @@ impl Pps {
ensure!(sps_id < 32, "seq_parameter_set_id out of range"); ensure!(sps_id < 32, "seq_parameter_set_id out of range");
let sps = spss let sps = spss
.get(sps_id as usize) .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}")))?; .ok_or_else(|| crate::h264::Error::MissingParameterSet(format!("SPS {sps_id} for PPS {id}")))?;
let entropy_coding_mode = r.read_flag()?; let entropy_coding_mode = r.read_flag()?;
let bottom_field_pic_order_in_frame_present = r.read_flag()?; let bottom_field_pic_order_in_frame_present = r.read_flag()?;
+1 -1
View File
@@ -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)); 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()) { while let (Some(y), Some(cb), Some(cr)) = (yc.next(), cbc.next(), crc.next()) {
let rows = y.len() / width; 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 }); bands.push(Band { y, cb, cr, a, rows });
} }
} }
+2 -2
View File
@@ -79,7 +79,7 @@ pub fn os_search_paths(os: Os, home: Option<PathBuf>, common_program_files: Opti
/// True when the path names a VST3 plugin (`*.vst3`, case-insensitive). /// True when the path names a VST3 plugin (`*.vst3`, case-insensitive).
pub fn is_vst3_path(p: &Path) -> bool { 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). /// 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<PathBuf> {
.flatten() .flatten()
.map(|e| e.path()) .map(|e| e.path())
.filter(|p| p.is_file()) .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) .take(64)
.collect(); .collect();
files.sort(); files.sort();
+8 -4
View File
@@ -131,6 +131,10 @@ if has appimage; then
cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png" cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png"
ln -s "$APP_ID.png" "$APPDIR/.DirIcon" 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)}" TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
if [[ -z "$TOOL" ]]; then if [[ -z "$TOOL" ]]; then
if [[ -z "$APPIMAGETOOL_SHA256" ]]; then if [[ -z "$APPIMAGETOOL_SHA256" ]]; then
@@ -142,21 +146,21 @@ if has appimage; then
download="$TOOL.download" download="$TOOL.download"
curl --proto '=https' -fsSL -o "$download" \ curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" "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" chmod 755 "$download"
mv "$download" "$TOOL" mv "$download" "$TOOL"
fi fi
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c - verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL"
fi fi
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH" RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
if [[ ! -f "$RUNTIME" ]]; then if [[ ! -f "$RUNTIME" ]]; then
download="$RUNTIME.download" download="$RUNTIME.download"
curl --proto '=https' -fsSL -o "$download" \ curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH" "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" mv "$download" "$RUNTIME"
fi 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 # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
# relative). # relative).
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage" OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
+1 -1
View File
@@ -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 attribution = std::fs::read_to_string(root().join("ATTRIBUTION.md")).map_err(|e| format!("ATTRIBUTION.md: {e}"))?;
let mut missing = Vec::new(); let mut missing = Vec::new();
for f in files.lines() { 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()) { if !ASSET_EXT.contains(&ext.as_str()) {
continue; continue;
} }