Compare commits

..
6 changed files with 41 additions and 257 deletions
-6
View File
@@ -734,12 +734,6 @@ fn read_bundle(bytes: &[u8], limits: BundleLimits) -> Result<(String, std::colle
if z.len() > limits.entries {
return Err(EngineError::Io("bundle: too many files".into()));
}
// Entries are stored, so the files we keep can only outgrow the bundle when several entries
// point at the same bytes: a small bundle naming one payload thousands of times would be
// copied out once per name.
if z.has_overlapping_files().map_err(zerr)? {
return Err(EngineError::Io("bundle: files share data".into()));
}
let (mut text, mut files, mut total) = (None, std::collections::HashMap::new(), 0u64);
for i in 0..z.len() {
let mut f = z.by_index(i).map_err(zerr)?;
-17
View File
@@ -194,23 +194,6 @@ mod bundle_tests {
}
}
#[test]
fn entries_sharing_one_payload_are_refused() {
use std::io::Write as _;
let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let opts = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
w.start_file("Song.scraft", opts).unwrap();
w.write_all(Session::default().to_json().unwrap().as_bytes()).unwrap();
w.start_file("Audio Files/a.wav", opts).unwrap();
w.write_all(&[7u8; 4096]).unwrap();
for i in 0..100 {
w.shallow_copy_file("Audio Files/a.wav", &format!("Audio Files/copy{i}.wav")).unwrap();
}
let bundle = w.finish().unwrap().into_inner();
let err = read_bundle(&bundle, BUNDLE_LIMITS).unwrap_err();
assert!(err.to_string().contains("share data"), "{err}");
}
#[test]
fn a_bundle_is_saved_only_if_it_opens_again() {
// #433: the writer keeps to the reader's limits.
+24 -108
View File
@@ -46,8 +46,7 @@ pub(crate) fn processing_order(s: &Session) -> Vec<usize> {
} else {
// A cycle keeps the existing bounded ordering for the entire graph, including any
// independent acyclic branches. Feedback does not have a finite longest-path depth.
let depths = bounded_aux_depths(s);
auxes.sort_by_key(|&i| depths.get(i).copied().unwrap_or(0));
auxes.sort_by_key(|&i| bounded_aux_depth(s, i));
}
order.extend(auxes);
order
@@ -116,38 +115,32 @@ fn aux_output_busses(t: &Track) -> impl Iterator<Item = BusId> + '_ {
.filter_map(|route| if let Route::Bus(bus) = route { Some(*bus) } else { None })
}
/// Hop cap for the bounded depth used when the aux graph has a cycle.
const MAX_BOUNDED_DEPTH: usize = 8;
/// For each track, the bounded depth used for a cyclic graph: the length of the longest chain of
/// Aux/Folder feeders into it (through a bus its input reads and the feeder's output or a send
/// writes), capped at [`MAX_BOUNDED_DEPTH`] hops. One pass per hop over the tracks and their bus
/// writes, so the cost is linear in the routing, not a fresh feeder search per track per
/// sort comparison.
fn bounded_aux_depths(s: &Session) -> Vec<usize> {
let is_aux = |t: &Track| matches!(t.kind, TrackKind::Aux | TrackKind::Folder);
// Tracks with a feeder chain of the current length ending at them (length 0: every aux).
let mut reached: Vec<bool> = s.tracks.iter().map(is_aux).collect();
let mut depths = vec![0usize; s.tracks.len()];
let mut fed = std::collections::HashSet::<BusId>::new();
for hop in 1..=MAX_BOUNDED_DEPTH {
fed.clear();
for (t, _) in s.tracks.iter().zip(&reached).filter(|(t, r)| **r && is_aux(t)) {
fed.extend(aux_output_busses(t));
}
let mut any = false;
for ((t, r), d) in s.tracks.iter().zip(reached.iter_mut()).zip(depths.iter_mut()) {
*r = matches!(&t.mixer.input, Route::Bus(b) if fed.contains(b));
if *r {
*d = hop;
any = true;
}
}
if !any {
/// The previous bounded depth calculation, retained unchanged for any cyclic graph.
fn bounded_aux_depth(s: &Session, i: usize) -> usize {
let mut d = 0;
let mut cur = vec![i];
for _ in 0..8 {
let inputs: Vec<&Route> = cur.iter().filter_map(|&k| s.tracks.get(k)).map(|t| &t.mixer.input).collect();
let feeders: Vec<usize> = auxes_feeding(s, &inputs);
if feeders.is_empty() {
break;
}
d += 1;
cur = feeders;
}
depths
d
}
fn auxes_feeding(s: &Session, inputs: &[&Route]) -> Vec<usize> {
s.tracks
.iter()
.enumerate()
.filter(|(_, t)| matches!(t.kind, TrackKind::Aux | TrackKind::Folder))
.filter(|(_, t)| {
inputs.iter().any(|r| matches!(r, Route::Bus(_)) && (&&t.mixer.output == r || t.mixer.sends.iter().flatten().any(|sn| &&sn.target == r)))
})
.map(|(i, _)| i)
.collect()
}
/// Implicit solo: an aux is not muted when a soloed track feeds its input bus.
@@ -211,81 +204,4 @@ mod processing_order_tests {
// Both deepest independent returns tie with the cycle at the legacy eight-hop cap.
assert_eq!(processing_order(&s), vec![9, 8, 7, 6, 5, 4, 3, 2, 0, 1, 10]);
}
/// The per-track feeder search `bounded_aux_depths` replaced, kept as its reference.
fn reference_depth(s: &Session, i: usize) -> usize {
let mut d = 0;
let mut cur = vec![i];
for _ in 0..MAX_BOUNDED_DEPTH {
let inputs: Vec<&Route> = cur.iter().filter_map(|&k| s.tracks.get(k)).map(|t| &t.mixer.input).collect();
let feeders: Vec<usize> = s
.tracks
.iter()
.enumerate()
.filter(|(_, t)| matches!(t.kind, TrackKind::Aux | TrackKind::Folder))
.filter(|(_, t)| {
inputs
.iter()
.any(|r| matches!(r, Route::Bus(_)) && (&&t.mixer.output == r || t.mixer.sends.iter().flatten().any(|sn| &&sn.target == r)))
})
.map(|(i, _)| i)
.collect();
if feeders.is_empty() {
break;
}
d += 1;
cur = feeders;
}
d
}
#[test]
fn bounded_depths_match_the_per_track_feeder_search() {
// A small pseudo-random graph of auxes, folders and audio tracks with cycles, joins,
// sends and unrouted inputs.
let mut s = Session::default();
let busses: Vec<_> = (0..6).map(|i| s.add_bus(&format!("B{i}"), ChannelFormat::Stereo)).collect();
let mut x = 12345u32;
let mut next = |m: usize| {
x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
(x >> 16) as usize % m
};
for i in 0..30 {
let kind = [TrackKind::Aux, TrackKind::Folder, TrackKind::Audio][next(3)];
let id = s.add_track(kind, ChannelFormat::Stereo, None);
let pick = |n: usize| if n < busses.len() { Route::Bus(busses[n]) } else { Route::Main };
let (input, output, send) = (pick(next(8)), pick(next(8)), pick(next(8)));
let t = s.track_mut(id).unwrap();
t.mixer.input = input;
t.mixer.output = output;
if i % 2 == 0 {
t.mixer.sends[0] = Some(SendSlot::new(send));
}
}
let depths = bounded_aux_depths(&s);
for (i, (t, depth)) in s.tracks.iter().zip(&depths).enumerate() {
if matches!(t.kind, TrackKind::Aux | TrackKind::Folder) {
assert_eq!(*depth, reference_depth(&s, i), "track {i}");
}
}
assert!(depths.contains(&MAX_BOUNDED_DEPTH));
}
#[test]
fn a_large_cyclic_graph_orders_quickly() {
// Thousands of auxes all reading and writing one bus: every aux feeds every other.
let mut s = Session::default();
let bus = s.add_bus("Loop", ChannelFormat::Stereo);
for _ in 0..3000 {
let id = s.add_track(TrackKind::Aux, ChannelFormat::Stereo, None);
let t = s.track_mut(id).unwrap();
t.mixer.input = Route::Bus(bus);
t.mixer.output = Route::Bus(bus);
t.mixer.sends[0] = Some(SendSlot::new(Route::Bus(bus)));
}
let start = std::time::Instant::now();
let order = processing_order(&s);
assert!(start.elapsed() < std::time::Duration::from_secs(5), "{:?}", start.elapsed());
assert_eq!(order, (0..3000).collect::<Vec<_>>());
}
}
+6 -124
View File
@@ -24,9 +24,6 @@ pub(crate) struct PluginSlot {
pub(crate) plugin: Box<dyn Plugin>,
/// Last values pushed to the plugin, in the insert's parameter-map order.
applied: Vec<f32>,
/// Scratch for `apply_params`: indexes of the track's automation lanes for this slot, kept
/// so the audio thread reuses its allocation.
lanes: Vec<usize>,
pub(crate) failed: bool,
}
@@ -194,7 +191,7 @@ impl SlotCtx<'_> {
let state = if third { ins.state_bytes() } else { None };
create_instance(&ins.plugin, state.as_deref(), self.sr, self.mb, ch).map(|n| n.plugin)
}?;
Some(PluginSlot { id: ins.plugin.clone(), ch, plugin, applied: Vec::new(), lanes: Vec::new(), failed: false })
Some(PluginSlot { id: ins.plugin.clone(), ch, plugin, applied: Vec::new(), failed: false })
}
}
@@ -494,40 +491,21 @@ pub(crate) fn apply_params(slot: &mut PluginSlot, ins: &soundcraft_model::Insert
let Ok(slot_u8) = u8::try_from(i) else {
return apply_values(slot, ins, |_, v| v);
};
let lanes = &t.automation;
let name = |j: usize| match lanes.get(j).map(|l| &l.param) {
Some(AutoParam::Plugin { param, .. }) => param.as_str(),
_ => "",
};
// This slot's lanes, ordered by parameter name (the parameter map's order) and keeping only
// the first lane per name (the one a lookup by name finds). Matching them to the parameters
// in one merge costs O(lanes log lanes + params) per block, not O(lanes x params).
let mut order = std::mem::take(&mut slot.lanes);
order.clear();
if t.mixer.automation_mode.reads() {
order
.extend(lanes.iter().enumerate().filter(|(_, l)| matches!(&l.param, AutoParam::Plugin { slot, .. } if *slot == slot_u8)).map(|(j, _)| j));
if !order.iter().any(|&j| lanes.get(j).is_some_and(|l| !l.points.is_empty())) {
order.clear();
}
order.sort_unstable_by(|&a, &b| name(a).cmp(name(b)).then(a.cmp(&b)));
order.dedup_by(|later, kept| name(*later) == name(*kept));
}
let mut next = order.iter().copied().peekable();
let automated = t.mixer.automation_mode.reads()
&& t.automation.iter().any(|l| matches!(&l.param, AutoParam::Plugin { slot, .. } if *slot == slot_u8) && !l.points.is_empty());
apply_values(slot, ins, |k, val| {
while next.next_if(|&j| name(j) < k).is_some() {}
if let Some(l) = next.peek().filter(|&&j| name(j) == k).and_then(|&j| lanes.get(j))
if automated
&& let Some(l) = t.automation.iter().find(|l| matches!(&l.param, AutoParam::Plugin { slot, param } if *slot == slot_u8 && param == k))
&& !l.points.is_empty()
{
return l.value_at(pos, val);
}
val
});
slot.lanes = order;
}
/// Sends each stored param (as mapped by `value`) to the plugin when it changed since last time.
fn apply_values(slot: &mut PluginSlot, ins: &soundcraft_model::Insert, mut value: impl FnMut(&str, f32) -> f32) {
fn apply_values(slot: &mut PluginSlot, ins: &soundcraft_model::Insert, value: impl Fn(&str, f32) -> f32) {
if slot.applied.len() != ins.params.len() {
slot.applied = vec![f32::NAN; ins.params.len()];
}
@@ -539,99 +517,3 @@ fn apply_values(slot: &mut PluginSlot, ins: &soundcraft_model::Insert, mut value
}
}
}
#[cfg(test)]
mod apply_params_tests {
use super::*;
use soundcraft_model::{AutomationLane, Insert};
use std::sync::Mutex;
/// A built-in gain that logs every parameter set.
struct Recorder {
inner: Box<dyn Plugin>,
sets: Arc<Mutex<Vec<(String, f32)>>>,
}
impl Plugin for Recorder {
fn info(&self) -> &'static soundcraft_dsp::PluginInfo {
self.inner.info()
}
fn prepare(&mut self, sr: f32, mb: usize, ch: usize) {
self.inner.prepare(sr, mb, ch);
}
fn reset(&mut self) {
self.inner.reset();
}
fn set_param(&mut self, id: &str, v: f32) -> bool {
self.sets.lock().unwrap().push((id.to_string(), v));
true
}
fn param(&self, id: &str) -> Option<f32> {
self.inner.param(id)
}
fn process(&mut self, io: &mut [Vec<f32>], frames: usize) {
self.inner.process(io, frames);
}
}
fn recorder() -> (PluginSlot, Arc<Mutex<Vec<(String, f32)>>>) {
let sets = Arc::new(Mutex::new(Vec::new()));
let plugin = Box::new(Recorder { inner: soundcraft_dsp::create("gain").unwrap(), sets: Arc::clone(&sets) });
(PluginSlot { id: "gain".into(), ch: 2, plugin, applied: Vec::new(), lanes: Vec::new(), failed: false }, sets)
}
fn lane(slot: u8, param: &str, point: Option<f32>) -> AutomationLane {
let mut l = AutomationLane::new(AutoParam::Plugin { slot, param: param.into() });
if let Some(v) = point {
l.set_point(0, v);
}
l
}
#[test]
fn first_lane_per_param_of_this_slot_drives_it() {
let mut s = Session::default();
let t = s.add_track(TrackKind::Audio, ChannelFormat::Stereo, None);
let tr = s.track_mut(t).unwrap();
tr.automation = vec![
lane(0, "c", None),
lane(1, "a", Some(5.0)),
lane(0, "b", Some(9.0)),
lane(0, "c", Some(7.0)),
lane(0, "zz", Some(1.0)),
lane(0, "b", Some(8.0)),
];
let mut ins = Insert::new("gain");
for (k, v) in [("a", 1.0), ("b", 2.0), ("c", 3.0), ("d", 4.0)] {
ins.params.insert(k.into(), v);
}
let (mut slot, sets) = recorder();
apply_params(&mut slot, &ins, tr, 0, 0);
let got = sets.lock().unwrap().clone();
let want: Vec<(String, f32)> = [("a", 1.0), ("b", 9.0), ("c", 3.0), ("d", 4.0)].iter().map(|(k, v)| (k.to_string(), *v)).collect();
assert_eq!(got, want);
// Unchanged values are not sent again.
apply_params(&mut slot, &ins, tr, 0, 0);
assert_eq!(sets.lock().unwrap().len(), 4);
}
#[test]
fn many_params_and_lanes_apply_without_a_quadratic_search() {
let n = 40_000;
let mut s = Session::default();
let t = s.add_track(TrackKind::Audio, ChannelFormat::Stereo, None);
let tr = s.track_mut(t).unwrap();
tr.automation = (0..n).rev().map(|i| lane(0, &format!("p{i:06}"), Some(i as f32))).collect();
let mut ins = Insert::new("gain");
for i in 0..n {
ins.params.insert(format!("p{i:06}"), -1.0);
}
let (mut slot, sets) = recorder();
let start = std::time::Instant::now();
apply_params(&mut slot, &ins, tr, 0, 0);
assert!(start.elapsed() < std::time::Duration::from_secs(5), "{:?}", start.elapsed());
let got = sets.lock().unwrap();
assert_eq!(got.len(), n);
assert!(got.iter().all(|(k, v)| k == &format!("p{:06}", *v as usize)));
}
}
+6
View File
@@ -47,6 +47,12 @@ done
echo "==> SoundCraft $VERSION Flatpak bundle for $ARCH from $(basename "$TARBALL")"
# The packaged app unpacks to tens of MB; an archive that lists far more would fill the build
# host's disk before anything here could check it.
MAX_UNPACKED_BYTES=$((2 * 1024 * 1024 * 1024))
UNPACKED="$(tar -tzvf "$TARBALL" | awk '{ s += $3 } END { print s + 0 }')"
[ "$UNPACKED" -le "$MAX_UNPACKED_BYTES" ] || { echo "error: $TARBALL unpacks to $UNPACKED bytes, more than $MAX_UNPACKED_BYTES" >&2; exit 1; }
WORK="$CARGO_TARGET_DIR/flatpak-bundle"
rm -rf "$WORK"
mkdir -p "$WORK/stage"
+5 -2
View File
@@ -19,10 +19,13 @@ fi
TMP="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
KEYCHAIN="$TMP/soundcraft-signing.keychain-db"
CERT="$TMP/soundcraft-signing.p12"
# The decoded certificate goes in a folder of this run's own (mktemp creates it, owner-only):
# a name another account could plant in the shared folder must not receive or redirect it.
CERT_DIR="$(mktemp -d "$TMP/soundcraft-signing.XXXXXX")"
CERT="$CERT_DIR/soundcraft-signing.p12"
KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}"
trap 'rm -f "$CERT"' EXIT
trap 'rm -rf "$CERT_DIR"' EXIT
printf '%s' "$APPLE_CERTIFICATE" | base64 --decode >"$CERT"
security create-keychain -p "$KC_PASS" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"