mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-30 13:36:44 -06:00
Compare commits
28
Commits
m/land-993
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7902d59b47 | ||
|
|
92e74f6e00 | ||
|
|
072e548ec8 | ||
|
|
98fae00688 | ||
|
|
d2079bf09d | ||
|
|
5447c94cb6 | ||
|
|
3276358bef | ||
|
|
081f43b90a | ||
|
|
b4412929a7 | ||
|
|
ca27f0f8b4 | ||
|
|
5b69a11d89 | ||
|
|
81ab012724 | ||
|
|
a62d955a39 | ||
|
|
af4970e388 | ||
|
|
9e768d659b | ||
|
|
4e9d778734 | ||
|
|
c2f14dff49 | ||
|
|
f39413033e | ||
|
|
19f4b695e2 | ||
|
|
17d553a7a7 | ||
|
|
57ae0a8b5d | ||
|
|
d9c293a92b | ||
|
|
7c029c40ce | ||
|
|
75e5b90a86 | ||
|
|
dab64379cb | ||
|
|
4f771268b0 | ||
|
|
9d5f5784d5 | ||
|
|
2e057309b9 |
@@ -26,8 +26,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
windows-arm64:
|
||||
# Immutable supporting workflow revision until the new install contract lands upstream.
|
||||
uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@8bb4e5ab95443171104ad88113fe1ef9b0e5ce3f
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@main
|
||||
with:
|
||||
app-name: soundcraft
|
||||
app-dir: SoundCraft
|
||||
|
||||
@@ -89,3 +89,8 @@ Push to the `release` branch to build signed installers for macOS (universal DMG
|
||||
GitHub profiles). Never hand-edit `contributors.json`.
|
||||
- Maintainers refresh the stats with `python3 ../../craftrules/scripts/contributors.py .` (it also
|
||||
re-verifies who wrote each `people.toml` entry; `--check` only verifies).
|
||||
|
||||
## CI workflows
|
||||
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
||||
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
|
||||
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
|
||||
|
||||
@@ -3,3 +3,8 @@ Read `AGENTS.md`: it is the entry point for agents in this repo.
|
||||
New workspace crates must include `[lints] workspace = true` in `Cargo.toml` to inherit
|
||||
`[workspace.lints]`. Production crate roots must declare the required crate-level lint attributes
|
||||
before modules or other crate items; crate-level `//!` documentation may precede them.
|
||||
|
||||
## CI workflows
|
||||
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
|
||||
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
|
||||
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
|
||||
|
||||
Generated
+12
-11
@@ -272,7 +272,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
@@ -1120,13 +1120,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=04aa1703b32d299e54f78fcde4182f2b393a9116#04aa1703b32d299e54f78fcde4182f2b393a9116"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
"egui",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"image",
|
||||
"log",
|
||||
"plist",
|
||||
"semver",
|
||||
@@ -1533,7 +1534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3244,7 +3245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3880,7 +3881,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3937,7 +3938,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4839,10 +4840,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.5",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5735,7 +5736,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6359,9 +6360,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.1.1"
|
||||
version = "9.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "518b847fd864440c379748ad684c449f293af68978a485a4ff16f7c901c1f7c4"
|
||||
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
|
||||
+2
-1
@@ -54,7 +54,8 @@ craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-l
|
||||
|
||||
# Video containers and bitstreams, shared with the other craft apps.
|
||||
craft-media = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
craft-update = { version = "=0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["egui"] }
|
||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||
craft-update = { version = "=0.1.0", git = "https://github.com/storytold/craft-libs", rev = "04aa1703b32d299e54f78fcde4182f2b393a9116", features = ["egui"] }
|
||||
|
||||
# ui
|
||||
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["serde"] }
|
||||
|
||||
@@ -185,7 +185,7 @@ fn app_icon() -> Option<egui::IconData> {
|
||||
fn main() -> eframe::Result {
|
||||
// Windows: the update helper is a copy of this program started with `--finish-update`.
|
||||
#[cfg(windows)]
|
||||
if let Some(result) = craft_update::self_update::finish(&craft_update::suite::SOUNDCRAFT, &std::env::args_os().skip(1).collect::<Vec<_>>()) {
|
||||
if let Some(result) = craft_update::self_update::finish(&soundcraft_ui_egui::UPDATE_APP, &std::env::args_os().skip(1).collect::<Vec<_>>()) {
|
||||
std::process::exit(i32::from(result.is_err()));
|
||||
}
|
||||
// First, so every start-up record (and the engine's panic hook, installed with the first
|
||||
|
||||
@@ -65,3 +65,6 @@ sync_github_name = true
|
||||
[people.luigiPinna]
|
||||
real_name = "Luigi Pinna"
|
||||
sync_github_name = true
|
||||
|
||||
[people.dylanmtaylor]
|
||||
sync_github_name = true
|
||||
|
||||
@@ -89,7 +89,11 @@ fn decode_group_audio(
|
||||
let (mut decoded, mut index, mut file_of) = (Vec::new(), BTreeMap::new(), BTreeMap::new());
|
||||
for (old, rel) in audio_entries {
|
||||
let full = dir.join(rel);
|
||||
if !full.starts_with(dir) || rel.contains("..") {
|
||||
if !full.starts_with(dir)
|
||||
|| Path::new(rel)
|
||||
.components()
|
||||
.any(|c| matches!(c, std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_)))
|
||||
{
|
||||
return Err(EngineError::BadParams(cmd_id.into(), format!("audio path escapes the group folder: {rel}")));
|
||||
}
|
||||
let key = crate::io::file_key(&full);
|
||||
@@ -233,6 +237,40 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_accepts_audio_names_with_consecutive_dots_but_not_parent_dirs() {
|
||||
let dir = std::env::temp_dir().join(format!("sc-grp-dots-{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let path = dir.join("drums.scgrp").to_string_lossy().to_string();
|
||||
let mut e = crate::demo::demo_engine();
|
||||
let kick = e.session().tracks.iter().find(|t| t.name == "Kick").map(|t| t.id).unwrap();
|
||||
let ids: Vec<_> = e.session().track(kick).unwrap().clips().iter().take(1).map(|c| c.id.0).collect();
|
||||
e.execute("file.export_clip_groups", &json!({"path": path, "clips": ids})).unwrap();
|
||||
let mut file: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
for (i, rel) in file["audio"].as_object_mut().unwrap().values_mut().enumerate() {
|
||||
let old = dir.join(rel.as_str().unwrap());
|
||||
let new = format!("Wait... what_{i}.wav");
|
||||
std::fs::rename(&old, dir.join(&new)).unwrap();
|
||||
*rel = json!(new);
|
||||
}
|
||||
std::fs::write(&path, file.to_string()).unwrap();
|
||||
let r = e.execute("file.import_clip_groups", &json!({"path": path, "at": 1000, "track": "Bass"}));
|
||||
assert!(r.is_ok(), "{r:?}");
|
||||
for rel in file["audio"].as_object_mut().unwrap().values_mut() {
|
||||
*rel = json!("../escape.wav");
|
||||
}
|
||||
std::fs::write(&path, file.to_string()).unwrap();
|
||||
let r = e.execute("file.import_clip_groups", &json!({"path": path, "at": 1000, "track": "Bass"}));
|
||||
assert!(matches!(r, Err(crate::EngineError::BadParams(..))), "{r:?}");
|
||||
for rel in file["audio"].as_object_mut().unwrap().values_mut() {
|
||||
*rel = json!(dir.join("Wait... what_0.wav").to_string_lossy());
|
||||
}
|
||||
std::fs::write(&path, file.to_string()).unwrap();
|
||||
let r = e.execute("file.import_clip_groups", &json!({"path": path, "at": 1000, "track": "Bass"}));
|
||||
assert!(matches!(r, Err(crate::EngineError::BadParams(..))), "absolute paths are refused: {r:?}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_clips_carry_no_auto_rename_scheme() {
|
||||
let dir = std::env::temp_dir().join(format!("sc-grp-names-{}", std::process::id()));
|
||||
|
||||
@@ -137,6 +137,7 @@ fn process(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
nc.name = new_name;
|
||||
nc.content = ClipContent::Audio { source: src, offset: 0 };
|
||||
nc.length = i64::try_from(frames).unwrap_or(nc.length);
|
||||
nc.clamp_fades();
|
||||
// The render already has the clip's stretch applied; keeping it would stretch the new file again.
|
||||
nc.stretch = 1.0;
|
||||
// Invert and Duplicate are phase/copy operations. The clip's gain stays a clip gain,
|
||||
@@ -246,6 +247,21 @@ mod tests {
|
||||
assert!((sample - 0.5).abs() < 1e-3, "duplicate keeps the file level and the clip gain, got {sample}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortening_process_clamps_fades_to_the_new_length() {
|
||||
let (mut e, id) = gained_clip();
|
||||
{
|
||||
let c = e.session_mut().find_clip_mut(id).unwrap();
|
||||
c.fade_in.len = 60;
|
||||
c.fade_out.len = 40;
|
||||
}
|
||||
e.execute("audiosuite.process", &json!({"process": "time_stretch", "params": {"ratio": 0.25}, "clips": [id.0]})).unwrap();
|
||||
let c = e.session().tracks[0].clips()[0].clone();
|
||||
assert!(c.length < 64, "{}", c.length);
|
||||
assert!(c.fade_in.len <= c.length, "fade in {} > length {}", c.fade_in.len, c.length);
|
||||
assert_eq!(c.fade_out.len, 40.min(c.length));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn processed_clip_does_not_keep_the_stretch_it_baked() {
|
||||
for stretch in [0.5, 2.0] {
|
||||
|
||||
@@ -799,8 +799,11 @@ pub fn trim_clip(s: &mut Session, id: ClipId, start: Option<Samples>, end: Optio
|
||||
lo = lo.max(0);
|
||||
}
|
||||
let file = |c: &Clip| {
|
||||
let start = c.start.saturating_sub(c.source_offset());
|
||||
material_len(s, c).map(|n| (start.min(c.start), start.saturating_add(n).max(c.end())))
|
||||
// The mixer plays from `offset + rel / stretch`, so file samples span `stretch` times as many timeline samples.
|
||||
let stretch = if c.stretch.is_finite() && c.stretch > 0.0 { c.stretch } else { 1.0 };
|
||||
let scale = |n: Samples| if (stretch - 1.0).abs() >= 1e-9 { (n as f64 * stretch).round() as Samples } else { n };
|
||||
let start = c.start.saturating_sub(scale(c.source_offset()));
|
||||
material_len(s, c).map(|n| (start.min(c.start), start.saturating_add(scale(n)).max(c.end())))
|
||||
};
|
||||
let t = TimeCtx::of(s);
|
||||
let mut c = old.clone();
|
||||
|
||||
@@ -364,3 +364,39 @@ fn paste_keeps_crossfade_overlap() {
|
||||
s.track(t).unwrap().clips().iter().filter(|c| c.start >= 20_000).map(|c| (c.start, c.length, c.fade_out.len, c.fade_in.len)).collect();
|
||||
assert_eq!(pasted, vec![(21_000, 2000, 500, 0), (22_500, 2500, 0, 500)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_clip_bounds_stretched_clip_to_its_file() {
|
||||
for (stretch, length, want) in [(0.5, 5000, 5000), (2.0, 1000, 20_000)] {
|
||||
let (mut s, t) = session_with_clip();
|
||||
let id = {
|
||||
let c = s.track_mut(t).and_then(|t| t.playlist_mut()).and_then(|p| p.clips.first_mut()).unwrap();
|
||||
c.stretch = stretch;
|
||||
c.start = 0;
|
||||
c.length = length;
|
||||
if let ClipContent::Audio { offset, .. } = &mut c.content {
|
||||
*offset = 0;
|
||||
}
|
||||
c.id
|
||||
};
|
||||
let c = trim_clip(&mut s, id, None, Some(99_999), false).unwrap();
|
||||
assert_eq!(c.end(), want, "stretch {stretch}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_clip_start_bound_scales_the_offset_by_stretch() {
|
||||
let (mut s, t) = session_with_clip();
|
||||
let id = {
|
||||
let c = s.track_mut(t).and_then(|t| t.playlist_mut()).and_then(|p| p.clips.first_mut()).unwrap();
|
||||
c.stretch = 2.0;
|
||||
c.start = 10_000;
|
||||
c.length = 1000;
|
||||
if let ClipContent::Audio { offset, .. } = &mut c.content {
|
||||
*offset = 1000;
|
||||
}
|
||||
c.id
|
||||
};
|
||||
let c = trim_clip(&mut s, id, Some(0), None, false).unwrap();
|
||||
assert_eq!(c.start, 8000);
|
||||
}
|
||||
|
||||
+107
-4
@@ -29,6 +29,12 @@ use dirty::same_document;
|
||||
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
|
||||
pub use soundcraft_model as model;
|
||||
|
||||
/// Upper bound on journaled commands, so a long session cannot grow memory without limit.
|
||||
const MAX_JOURNAL_ENTRIES: usize = 10_000;
|
||||
/// Upper bound on a single journaled entry's params (serialized), so junk fields from MCP or the
|
||||
/// control channel cannot keep a huge blob alive for the life of the process.
|
||||
const MAX_JOURNAL_ENTRY_BYTES: usize = 64 * 1024; // 64 KiB
|
||||
|
||||
/// Shuts the third-party plugin hosts down, running the module exit of every plugin binary they
|
||||
/// loaded. Call it at the end of `main`, once every plugin instance (mixer, player) is gone: a
|
||||
/// plugin that has been used can crash when its module exit only runs from an exit handler.
|
||||
@@ -176,8 +182,9 @@ pub struct Engine {
|
||||
pub clipboard: Clipboard,
|
||||
pub transport_requests: Vec<TransportRequest>,
|
||||
pub transport: TransportStatus,
|
||||
/// Command journal (id, params) for macro recording / agents.
|
||||
pub journal: Vec<(String, Value)>,
|
||||
/// Command journal (id, params) for macro recording / agents. Ring buffer: capped at
|
||||
/// [`MAX_JOURNAL_ENTRIES`] entries of up to [`MAX_JOURNAL_ENTRY_BYTES`] params each.
|
||||
pub journal: std::collections::VecDeque<(String, Value)>,
|
||||
/// Path of the session file, if saved.
|
||||
pub path: Option<String>,
|
||||
/// Revision counter; bumps on every document change but zoom/scroll (UI caches key on it).
|
||||
@@ -219,7 +226,7 @@ impl Engine {
|
||||
clipboard: Clipboard::default(),
|
||||
transport_requests: Vec::new(),
|
||||
transport: TransportStatus::default(),
|
||||
journal: Vec::new(),
|
||||
journal: std::collections::VecDeque::new(),
|
||||
path: None,
|
||||
revision: 1,
|
||||
dirty: false,
|
||||
@@ -422,7 +429,7 @@ impl Engine {
|
||||
self.push_undo(spec.label, before);
|
||||
}
|
||||
if spec.journal {
|
||||
self.journal.push((id.to_string(), params.clone()));
|
||||
self.journal_append(id, params);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
@@ -489,6 +496,16 @@ impl Engine {
|
||||
self.merge_key = None;
|
||||
}
|
||||
|
||||
/// Append a journaled command, clamping its params to [`MAX_JOURNAL_ENTRY_BYTES`] and the
|
||||
/// buffer to [`MAX_JOURNAL_ENTRIES`] entries (oldest are dropped).
|
||||
fn journal_append(&mut self, id: &str, params: &Value) {
|
||||
let clamped = (id.to_string(), journal_params(params));
|
||||
self.journal.push_back(clamped);
|
||||
if self.journal.len() > MAX_JOURNAL_ENTRIES {
|
||||
self.journal.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: execute with `{}` params.
|
||||
pub fn run(&mut self, id: &str) -> Result<Value> {
|
||||
self.execute(id, &json!({}))
|
||||
@@ -529,6 +546,92 @@ fn panic_message(p: &(dyn std::any::Any + Send)) -> String {
|
||||
p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
|
||||
}
|
||||
|
||||
/// The params to keep in the journal for a command: the value itself when it serializes within
|
||||
/// [`MAX_JOURNAL_ENTRY_BYTES`], or a truncation marker when it is too large or unparseable.
|
||||
///
|
||||
/// Serializes into a [`std::io::Write`] sink that fails as soon as the cap is exceeded, so an
|
||||
/// oversized `params` (a multi-MiB line from MCP or the control channel) is never fully
|
||||
/// serialized, cloned, or allocated just to be discarded.
|
||||
fn journal_params(params: &Value) -> Value {
|
||||
// Write through a bounded sink: serialization stops (WriteZero) the moment the cap is passed,
|
||||
// so an oversized value costs at most ~1× the cap, not its full serialized size.
|
||||
struct Bounded(Vec<u8>);
|
||||
impl std::io::Write for Bounded {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let room = MAX_JOURNAL_ENTRY_BYTES.saturating_sub(self.0.len());
|
||||
if room == 0 {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::WriteZero, "journal entry cap"));
|
||||
}
|
||||
let take = buf.len().min(room);
|
||||
self.0.extend_from_slice(&buf[..take]);
|
||||
if take < buf.len() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::WriteZero, "journal entry cap"));
|
||||
}
|
||||
Ok(take)
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let mut sink = Bounded(Vec::new());
|
||||
// Fails with WriteZero as soon as the cap is exceeded, so the allocation stays bounded even
|
||||
// for a multi-MiB params.
|
||||
let ok = serde_json::to_writer(&mut sink, params).is_ok();
|
||||
if !ok {
|
||||
return json!({"__truncated__": true});
|
||||
}
|
||||
params.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod journal_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn journal_is_bounded_to_max_entries() {
|
||||
let mut e = Engine::default();
|
||||
// Calling the private append helper directly exercises the ring buffer without the cost
|
||||
// of cloning the whole document on 10,000+ `track.new` calls.
|
||||
for i in 0..MAX_JOURNAL_ENTRIES + 500 {
|
||||
let n = format!("t{i}");
|
||||
e.journal_append("track.new", &json!({"name": n}));
|
||||
}
|
||||
assert_eq!(e.journal.len(), MAX_JOURNAL_ENTRIES);
|
||||
// The oldest entries were dropped, the newest kept: the first remaining is the 501st pushed,
|
||||
// and the newest still there.
|
||||
let first = e.journal.front().map(|(_, p)| p["name"].as_str().unwrap_or_default());
|
||||
assert_eq!(first, Some("t500"));
|
||||
let last = e.journal.back().map(|(_, p)| p["name"].as_str().unwrap_or_default());
|
||||
let last_name = format!("t{}", MAX_JOURNAL_ENTRIES + 499);
|
||||
assert_eq!(last, Some(last_name.as_str()));
|
||||
assert_eq!(e.journal.back().map(|(id, _)| id.as_str()), Some("track.new"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_params_are_truncated_in_the_journal() {
|
||||
let big = "x".repeat(MAX_JOURNAL_ENTRY_BYTES * 4);
|
||||
assert_eq!(journal_params(&json!({"name": big})), json!({"__truncated__": true}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_params_are_kept_whole() {
|
||||
let p = json!({"track": "Kick", "db": -6.0});
|
||||
assert_eq!(journal_params(&p), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cap_boundary_is_exact() {
|
||||
// A value just under the cap is kept whole; one just over is truncated. "Just" means the
|
||||
// bounded writer made at most ~two allocations of cap size, not a full multi-MiB buffer.
|
||||
// `{"n":"…"}` adds 9 bytes around the string.
|
||||
let small_str = MAX_JOURNAL_ENTRY_BYTES - 64;
|
||||
let small = json!({"n": "x".repeat(small_str)});
|
||||
assert_eq!(journal_params(&small), small);
|
||||
let big = json!({"n": "x".repeat(MAX_JOURNAL_ENTRY_BYTES)});
|
||||
assert_eq!(journal_params(&big), json!({"__truncated__": true}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod merge_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -105,6 +105,7 @@ impl SoundApp {
|
||||
quit_requested: false,
|
||||
arrange_request: false,
|
||||
touched: std::collections::HashSet::new(),
|
||||
touched_sends: std::collections::HashSet::new(),
|
||||
last_write_at: i64::MIN,
|
||||
editor_states: Vec::new(),
|
||||
editor_poll_due: true,
|
||||
@@ -159,7 +160,7 @@ impl SoundApp {
|
||||
if hidden {
|
||||
self.pointer_down = false;
|
||||
self.wants_keyboard = false;
|
||||
if matches!(self.gesture, Some(Gesture::Fader { .. })) {
|
||||
if matches!(self.gesture, Some(Gesture::Fader { .. } | Gesture::SendFader { .. })) {
|
||||
self.gesture = None;
|
||||
}
|
||||
}
|
||||
@@ -179,11 +180,11 @@ impl SoundApp {
|
||||
updates_ui::tick(self, ctx);
|
||||
}
|
||||
if !self.pointer_down {
|
||||
if matches!(self.gesture, Some(Gesture::Fader { .. })) {
|
||||
if matches!(self.gesture, Some(Gesture::Fader { .. } | Gesture::SendFader { .. })) {
|
||||
self.gesture = None;
|
||||
}
|
||||
// Keep a running automation pass merged until playback stops.
|
||||
if !self.wants_keyboard && self.touched.is_empty() {
|
||||
if !self.wants_keyboard && self.touched.is_empty() && self.touched_sends.is_empty() {
|
||||
self.engine.end_merge();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,8 @@ impl SoundApp {
|
||||
}
|
||||
|
||||
/// While playing, record volume automation from fader positions for tracks in Write mode, and
|
||||
/// for Touch/Latch tracks whose fader is (or, for Latch, was) being moved. Each point replaces the
|
||||
/// for Touch/Latch tracks whose fader is (or, for Latch, was) being moved. Send levels follow the
|
||||
/// same rules, per send, from its slot or its floating send window. Each point replaces the
|
||||
/// automation since the previous one, and Stop writes a last point. A pass and whatever is
|
||||
/// dragged during it, its fader included, are one undo step.
|
||||
pub(crate) fn write_automation(&mut self) {
|
||||
@@ -214,7 +215,7 @@ impl SoundApp {
|
||||
let may_write =
|
||||
playing && self.engine.session().tracks.iter().any(|t| matches!(t.mixer.automation_mode, M::Write | M::Touch | M::Latch | M::TouchLatch));
|
||||
self.engine.hold_merge(may_write.then_some(PASS));
|
||||
if !playing && self.touched.is_empty() {
|
||||
if !playing && self.touched.is_empty() && self.touched_sends.is_empty() {
|
||||
self.last_write_at = i64::MIN;
|
||||
return;
|
||||
}
|
||||
@@ -230,6 +231,10 @@ impl SoundApp {
|
||||
Some(Gesture::Fader { track, .. }) => Some(*track),
|
||||
_ => None,
|
||||
};
|
||||
let send_fader = match &self.gesture {
|
||||
Some(Gesture::SendFader { track, slot }) => Some((*track, *slot)),
|
||||
_ => None,
|
||||
};
|
||||
let writes: Vec<(TrackId, f32)> = self
|
||||
.engine
|
||||
.session()
|
||||
@@ -247,9 +252,39 @@ impl SoundApp {
|
||||
write.then_some((t.id, t.mixer.volume_db))
|
||||
})
|
||||
.collect();
|
||||
for (t, v) in writes {
|
||||
let send_writes: Vec<(TrackId, usize, f32)> = self
|
||||
.engine
|
||||
.session()
|
||||
.tracks
|
||||
.iter()
|
||||
.flat_map(|t| {
|
||||
let m = t.mixer.automation_mode;
|
||||
t.mixer.sends.iter().enumerate().filter_map(move |(slot, s)| s.as_ref().map(|s| (t.id, m, slot, s.level_db)))
|
||||
})
|
||||
.filter(|&(t, m, slot, _)| {
|
||||
let touched_now = dragging && send_fader == Some((t, slot));
|
||||
match m {
|
||||
M::Write => true,
|
||||
M::Touch => touched_now,
|
||||
M::Latch | M::TouchLatch => touched_now || self.touched_sends.contains(&(t, slot)),
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
.map(|(t, _, slot, db)| (t, slot, db))
|
||||
.collect();
|
||||
let points: Vec<(TrackId, String, f32)> = writes
|
||||
.iter()
|
||||
.map(|&(t, v)| (t, "volume".to_string(), v))
|
||||
.chain(send_writes.iter().map(|&(t, slot, v)| (t, format!("send_{}_level", (b'a' + slot as u8) as char), v)))
|
||||
.collect();
|
||||
for &(t, _) in &writes {
|
||||
self.touched.insert(t);
|
||||
let mut point = serde_json::json!({"track": t.0, "param": "volume", "at": pos, "value": v});
|
||||
}
|
||||
for &(t, slot, _) in &send_writes {
|
||||
self.touched_sends.insert((t, slot));
|
||||
}
|
||||
for (t, param, v) in points {
|
||||
let mut point = serde_json::json!({"track": t.0, "param": param, "at": pos, "value": v});
|
||||
if from != i64::MIN {
|
||||
point["from"] = from.into();
|
||||
}
|
||||
@@ -257,6 +292,7 @@ impl SoundApp {
|
||||
}
|
||||
if !playing {
|
||||
self.touched.clear();
|
||||
self.touched_sends.clear();
|
||||
self.engine.end_merge();
|
||||
}
|
||||
}
|
||||
@@ -296,6 +332,7 @@ impl SoundApp {
|
||||
self.punch = None;
|
||||
self.gesture = None;
|
||||
self.touched.clear();
|
||||
self.touched_sends.clear();
|
||||
self.last_write_at = i64::MIN;
|
||||
self.edit_layout.follow_hold = false;
|
||||
self.meters.clear();
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(super) fn fade_curve_drag_value(fade: soundcraft_model::Fade, anchor: (f32,
|
||||
|
||||
/// Runs before anything is painted, against the last frame's layout.
|
||||
pub(super) fn drive_drag(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
if app.gesture.is_none() || matches!(app.gesture, Some(Gesture::Fader { .. })) {
|
||||
if app.gesture.is_none() || matches!(app.gesture, Some(Gesture::Fader { .. } | Gesture::SendFader { .. })) {
|
||||
return;
|
||||
}
|
||||
let (s, tl) = (app.engine.session_arc(), rect(app.edit_layout.timeline));
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
//! executing commands. It can be replaced without touching anything below it.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
/// SoundCraft as craft-update knows it, with its 128 px icon and accent colour: the update
|
||||
/// prompt shows both, and an AppImage's menu entry gets the icon.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub const UPDATE_APP: craft_update::app::App = craft_update::app::App {
|
||||
icon_png: Some(include_bytes!("../../../assets/app-icon/hicolor/128x128/apps/ai.storyteller.soundcraft.png")),
|
||||
accent: Some([0x1f, 0xa5, 0xc9]),
|
||||
..craft_update::suite::SOUNDCRAFT
|
||||
};
|
||||
|
||||
pub mod appearance;
|
||||
pub mod bounce;
|
||||
pub mod control;
|
||||
@@ -235,6 +244,7 @@ pub enum Gesture {
|
||||
FadeCurve { clip: soundcraft_model::ClipId, fade_in: bool, curvature: f32, anchor: (f32, f32), height: f32 },
|
||||
Scrub { last: Samples },
|
||||
Fader { track: TrackId, start_db: f32 },
|
||||
SendFader { track: TrackId, slot: usize },
|
||||
ClipGain { clip: soundcraft_model::ClipId, track: TrackId, pos: f32, anchor: (f32, f32), fine: bool, fader_bottom: egui::Pos2 },
|
||||
Pencil { track: TrackId, points: Vec<(Samples, f32)> },
|
||||
Breakpoint { track: TrackId, param: soundcraft_model::AutoParam, at: Samples },
|
||||
@@ -384,6 +394,8 @@ pub struct SoundApp {
|
||||
pub arrange_request: bool,
|
||||
/// Tracks whose automation was touched during this pass (Touch/Latch writing).
|
||||
touched: std::collections::HashSet<TrackId>,
|
||||
/// Sends whose level was touched during this pass (Touch/Latch writing), as (track, slot).
|
||||
touched_sends: std::collections::HashSet<(TrackId, usize)>,
|
||||
/// The pass's last point; `i64::MIN` after a stop or a jump, so the next point erases nothing it skipped.
|
||||
last_write_at: Samples,
|
||||
/// Plugins changed in their editors whose state the session hasn't taken yet (see `editor_states_due`).
|
||||
|
||||
@@ -46,6 +46,7 @@ pub(crate) fn send_slot(app: &mut SoundApp, ui: &mut Ui, track: &Track, slot: us
|
||||
// applying it to the already-updated value made trackpad adjustment accelerate.
|
||||
let (dy, fine) = ui.input(|i| (i.pointer.delta().y, i.modifiers.shift));
|
||||
let db = dragged_send_db(s.level_db, dy, fine);
|
||||
app.gesture = Some(crate::Gesture::SendFader { track: id, slot });
|
||||
let _ =
|
||||
app.engine.execute_merged("mix.send_level", &json!({"track": id.0, "slot": slot, "db": db}), &format!("send:{}:{slot}", id.0));
|
||||
}
|
||||
@@ -120,6 +121,7 @@ fn send_window_contents(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
// Distinct from the strip's "Send A level for <track>" icon and from the slot's "Send level", so a
|
||||
// label query while the window is open still picks one node.
|
||||
if let Some(db) = fader(ui, fr, send.level_db, ui.scope_id().with("send_fader"), "Send level fader") {
|
||||
app.gesture = Some(crate::Gesture::SendFader { track: id, slot });
|
||||
let _ = app.engine.execute_merged(
|
||||
"mix.send_level",
|
||||
&json!({"track": id.0, "slot": slot, "db": db}),
|
||||
|
||||
@@ -244,6 +244,67 @@ fn a_pass_and_the_fader_ride_driving_it_are_one_undo_step() {
|
||||
assert_eq!(app.engine.undo_history().len(), steps + 1);
|
||||
}
|
||||
|
||||
/// #1146: a send's level writes its own automation lane, by the track's mode like its fader: Touch
|
||||
/// while the send is dragged, Latch from the first drag until stop, Write throughout. The track's
|
||||
/// volume lane is left alone.
|
||||
#[test]
|
||||
fn a_dragged_send_level_writes_send_automation() {
|
||||
use soundcraft_model::AutoParam;
|
||||
let points = |app: &SoundApp, param: &AutoParam| app.engine.session().track_by_name("Lead").unwrap().lane(param).map_or(0, |l| l.points.len());
|
||||
let level = |app: &SoundApp| app.engine.session().track_by_name("Lead").unwrap().lane(&AutoParam::SendLevel(0)).map(|l| l.points.clone());
|
||||
let pass = |app: &mut SoundApp| {
|
||||
app.handle_transport(0.1);
|
||||
app.write_automation();
|
||||
};
|
||||
for mode in ["touch", "latch", "write"] {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let lead = app.engine.session().track_by_name("Lead").unwrap().id;
|
||||
assert!(app.engine.session().track(lead).unwrap().mixer.sends[0].is_some(), "the demo's Lead sends to the reverb");
|
||||
let volume = points(&app, &AutoParam::Volume);
|
||||
app.run("mix.automation_mode", json!({"tracks": ["Lead"], "mode": mode})).unwrap();
|
||||
app.run("transport.play", json!({})).unwrap();
|
||||
for _ in 0..3 {
|
||||
pass(&mut app);
|
||||
}
|
||||
let before_drag = points(&app, &AutoParam::SendLevel(0));
|
||||
if mode == "write" {
|
||||
assert!(before_drag > 0, "Write writes the send without a drag");
|
||||
} else {
|
||||
assert_eq!(before_drag, 0, "{mode}: nothing written before the send is touched");
|
||||
}
|
||||
// What the send's slot and its floating window do while the level is dragged.
|
||||
app.pointer_down = true;
|
||||
app.gesture = Some(super::Gesture::SendFader { track: lead, slot: 0 });
|
||||
for frame in 0..4 {
|
||||
pass(&mut app);
|
||||
let db = -12.0 + frame as f32;
|
||||
app.engine.execute_merged("mix.send_level", &json!({"track": lead.0, "slot": 0, "db": db}), &format!("send:{}:0", lead.0)).unwrap();
|
||||
}
|
||||
let dragged = points(&app, &AutoParam::SendLevel(0));
|
||||
assert!(dragged >= before_drag + 3, "{mode}: the drag is written ({before_drag} -> {dragged})");
|
||||
assert!(level(&app).unwrap().iter().any(|p| (p.value - (-10.0)).abs() < 1e-6), "{mode}: the points follow the send level");
|
||||
// Released.
|
||||
app.pointer_down = false;
|
||||
app.gesture = None;
|
||||
for _ in 0..3 {
|
||||
pass(&mut app);
|
||||
}
|
||||
let released = points(&app, &AutoParam::SendLevel(0));
|
||||
if mode == "touch" {
|
||||
assert_eq!(released, dragged, "Touch stops writing when the send is let go");
|
||||
} else {
|
||||
assert!(released > dragged, "{mode}: keeps writing after the release");
|
||||
}
|
||||
app.run("transport.stop", json!({})).unwrap();
|
||||
app.handle_transport(0.0);
|
||||
app.write_automation();
|
||||
if mode != "write" {
|
||||
assert_eq!(points(&app, &AutoParam::Volume), volume, "{mode}: the track's volume lane is left alone");
|
||||
}
|
||||
assert!(app.touched_sends.is_empty(), "{mode}: stop ends the pass");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_states_read_during_a_drag_keep_it_one_undo_step() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
@@ -1515,9 +1576,15 @@ fn editor_gesture_polling_survives_discarded_and_hidden_host_iterations() {
|
||||
}
|
||||
|
||||
/// The engine's revision, undo history and journal are as they were.
|
||||
fn assert_history_unchanged(app: &SoundApp, revision: u64, undo: usize, history: &[String], journal: &[(String, serde_json::Value)]) {
|
||||
fn assert_history_unchanged(
|
||||
app: &SoundApp,
|
||||
revision: u64,
|
||||
undo: usize,
|
||||
history: &[String],
|
||||
journal: &std::collections::VecDeque<(String, serde_json::Value)>,
|
||||
) {
|
||||
assert_eq!(app.engine.revision, revision);
|
||||
assert_eq!(app.engine.undo_history().len(), undo);
|
||||
assert_eq!(app.engine.undo_history(), history);
|
||||
assert_eq!(app.engine.journal, journal);
|
||||
assert_eq!(&app.engine.journal, journal);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Default for Runtime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
updater: Updater::new(&craft_update::suite::SOUNDCRAFT, env!("CARGO_PKG_VERSION")),
|
||||
updater: Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
startup_check: false,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -909,4 +909,63 @@ mod tests {
|
||||
fn huge_pic_init_qp_is_an_error() {
|
||||
assert_eq!(invalid_pps(pps(None, 0, i32::MAX)), "pic_init_qp_minus26 out of range");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poc_extreme_offsets_do_not_overflow() {
|
||||
use crate::h264::slice::{PocState, SliceHeader, SliceType};
|
||||
let mut s = sps(SpsFields::default()).unwrap();
|
||||
s.log2_max_frame_num = 16;
|
||||
s.offset_for_ref_frame = vec![i32::MAX, i32::MAX];
|
||||
s.offset_for_non_ref_pic = i32::MAX;
|
||||
s.offset_for_top_to_bottom_field = i32::MAX;
|
||||
// Drive FrameNumOffset up to the i32 limit by wrapping frame_num repeatedly.
|
||||
let mut state = PocState::default();
|
||||
for _ in 0..32_767 {
|
||||
state.update_gap_frame(5, &s);
|
||||
state.update_gap_frame(1, &s);
|
||||
}
|
||||
// prev_frame_num > frame_num below, so the next picture adds max_frame_num once more.
|
||||
state.update_gap_frame(5, &s);
|
||||
for (poc_type, nal_ref_idc, st) in [1u32, 2].into_iter().flat_map(|t| [1u8, 0].into_iter().flat_map(move |r| [(t, r, false), (t, r, true)])) {
|
||||
s.pic_order_cnt_type = poc_type;
|
||||
let state = if st { state.clone() } else { PocState::default() };
|
||||
let sh = SliceHeader {
|
||||
nal_ref_idc,
|
||||
nal_unit_type: 1,
|
||||
idr: false,
|
||||
first_mb_in_slice: 0,
|
||||
slice_type_raw: 0,
|
||||
slice_type: SliceType::P,
|
||||
pps_id: 0,
|
||||
colour_plane_id: 0,
|
||||
frame_num: 2,
|
||||
field_pic: false,
|
||||
bottom_field: false,
|
||||
idr_pic_id: 0,
|
||||
pic_order_cnt_lsb: 0,
|
||||
delta_pic_order_cnt_bottom: 0,
|
||||
delta_pic_order_cnt: [i32::MAX, i32::MAX],
|
||||
redundant_pic_cnt: 0,
|
||||
direct_spatial_mv_pred: false,
|
||||
num_ref_idx_active: [1, 0],
|
||||
ref_pic_list_mod: [Vec::new(), Vec::new()],
|
||||
pred_weight_table: None,
|
||||
no_output_of_prior_pics: false,
|
||||
long_term_reference: false,
|
||||
adaptive_ref_pic_marking: false,
|
||||
mmcos: Vec::new(),
|
||||
cabac_init_idc: 0,
|
||||
slice_qp_delta: 0,
|
||||
sp_for_switch: false,
|
||||
slice_qs_delta: 0,
|
||||
disable_deblocking_filter_idc: 0,
|
||||
slice_alpha_c0_offset_div2: 0,
|
||||
slice_beta_offset_div2: 0,
|
||||
slice_group_change_cycle: 0,
|
||||
header_bits: 0,
|
||||
};
|
||||
// Must not panic; the value itself is meaningless for such a stream.
|
||||
let _ = state.compute(&sh, &s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,7 +480,7 @@ impl PocState {
|
||||
1 => {
|
||||
let frame_num_offset = self.frame_num_offset(sh.idr, sh.frame_num, sps);
|
||||
let n = sps.offset_for_ref_frame.len() as i32;
|
||||
let mut abs_frame_num = if n != 0 { frame_num_offset + sh.frame_num as i32 } else { 0 };
|
||||
let mut abs_frame_num = if n != 0 { frame_num_offset.saturating_add(sh.frame_num as i32) } else { 0 };
|
||||
if sh.nal_ref_idc == 0 && abs_frame_num > 0 {
|
||||
abs_frame_num -= 1;
|
||||
}
|
||||
@@ -488,17 +488,17 @@ impl PocState {
|
||||
if abs_frame_num > 0 {
|
||||
let cycle_cnt = (abs_frame_num - 1) / n;
|
||||
let in_cycle = (abs_frame_num - 1) % n;
|
||||
let delta_cycle: i32 = sps.offset_for_ref_frame.iter().sum();
|
||||
expected = cycle_cnt.wrapping_mul(delta_cycle);
|
||||
let delta_cycle: i32 = sps.offset_for_ref_frame.iter().fold(0i32, |a, &o| a.saturating_add(o));
|
||||
expected = cycle_cnt.saturating_mul(delta_cycle);
|
||||
for &o in &sps.offset_for_ref_frame[..=in_cycle as usize] {
|
||||
expected += o;
|
||||
expected = expected.saturating_add(o);
|
||||
}
|
||||
}
|
||||
if sh.nal_ref_idc == 0 {
|
||||
expected += sps.offset_for_non_ref_pic;
|
||||
expected = expected.saturating_add(sps.offset_for_non_ref_pic);
|
||||
}
|
||||
let top = expected + sh.delta_pic_order_cnt[0];
|
||||
let bottom = top + sps.offset_for_top_to_bottom_field + sh.delta_pic_order_cnt[1];
|
||||
let top = expected.saturating_add(sh.delta_pic_order_cnt[0]);
|
||||
let bottom = top.saturating_add(sps.offset_for_top_to_bottom_field).saturating_add(sh.delta_pic_order_cnt[1]);
|
||||
Poc { top, bottom, msb: 0, frame_num_offset }
|
||||
}
|
||||
_ => {
|
||||
@@ -506,9 +506,9 @@ impl PocState {
|
||||
let temp = if sh.idr {
|
||||
0
|
||||
} else if sh.nal_ref_idc == 0 {
|
||||
2 * (frame_num_offset + sh.frame_num as i32) - 1
|
||||
2i32.saturating_mul(frame_num_offset.saturating_add(sh.frame_num as i32)).saturating_sub(1)
|
||||
} else {
|
||||
2 * (frame_num_offset + sh.frame_num as i32)
|
||||
2i32.saturating_mul(frame_num_offset.saturating_add(sh.frame_num as i32))
|
||||
};
|
||||
Poc { top: temp, bottom: temp, msb: 0, frame_num_offset }
|
||||
}
|
||||
@@ -519,7 +519,7 @@ impl PocState {
|
||||
if idr {
|
||||
0
|
||||
} else if self.prev_frame_num > frame_num {
|
||||
self.prev_frame_num_offset + sps.max_frame_num() as i32
|
||||
self.prev_frame_num_offset.saturating_add(sps.max_frame_num() as i32)
|
||||
} else {
|
||||
self.prev_frame_num_offset
|
||||
}
|
||||
@@ -532,7 +532,7 @@ impl PocState {
|
||||
if sh.nal_ref_idc != 0 {
|
||||
if mmco5 {
|
||||
self.prev_ref_msb = 0;
|
||||
self.prev_ref_lsb = poc.top - poc.frame();
|
||||
self.prev_ref_lsb = poc.top.saturating_sub(poc.frame());
|
||||
} else {
|
||||
self.prev_ref_msb = poc.msb;
|
||||
self.prev_ref_lsb = sh.pic_order_cnt_lsb as i32;
|
||||
|
||||
Reference in New Issue
Block a user