Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ GitHub are its section here (`tools/release.sh` copies them), so add to

## Unreleased

- **New Project no longer crashes while a reverb rings.** Starting a new
project, or opening one, while the old project's master effects were
still sounding could crash Slab.

- **Juniper-8, a Roland Jupiter-8.** Eight voices of two VCOs with
exponential cross mod, VCO-2 synced to VCO-1, and PWM on both pulses. A
6 dB HPF feeds the IR3109 filter, with 24 dB and its thinner 12 dB tap.
Expand Down
61 changes: 56 additions & 5 deletions src/document.zig
Original file line number Diff line number Diff line change
Expand Up @@ -995,11 +995,10 @@ pub fn apply(
if (objGet(mo, "volume")) |x| m.setVolume(@floatCast(asF64(x)));
if (objGet(mo, "pan")) |x| m.setPan(@floatCast(asF64(x)));
m.subsonic.store(if (objGet(mo, "subsonic")) |x| asBool(x) else false, .monotonic);
for (m.effects.items) |*fx| {
if (fx.mach.deinit) |d| d(fx.mach.state, alloc);
if (fx.shared_io) |io| alloc.destroy(io);
}
m.effects.clearRetainingCapacity();
// The master outlives the load and the audio thread may still be
// rendering its old chain (a fading switch plays one more chunk):
// retire the effects, freed once audio adopts the new chain.
while (m.effects.items.len > 0) try m.removeEffect(alloc, m.effects.items.len - 1);
if (objGet(mo, "effects")) |ev| try applyEffects(alloc, reg, m, ev);
m.publishMachines();
};
Expand Down Expand Up @@ -2213,3 +2212,55 @@ test "grooves round-trip: the project's own, the song's, a section's and a track
try std.testing.expectEqual(@as(f32, 0.5), loaded_buf[0].groove.amount);
try std.testing.expectEqual(@as(f32, -8), loaded_buf[0].groove.shift_ms);
}

test "realtime: a project load retires the master's effects until audio adopts the new chain" {
// New Project while the old master chain rang crashed the audio
// thread: the load freed the master's effects in place, and a fading
// adoption keeps rendering the old chain for one more chunk.
const M = struct {
freed: usize = 0,
fn destroy(st: *anyopaque, _: std.mem.Allocator) void {
const x: *@This() = @ptrCast(@alignCast(st));
x.freed += 1;
}
};
const alloc = std.testing.allocator;
var reg = registry_mod.Registry.init(alloc);
defer reg.deinit();
var master = try track_mod.Track.init(alloc, "Master", .{ .r = 0, .g = 0, .b = 0, .a = 255 }, test_machine);
master.kind = .master;
defer master.deinit(alloc);
setMaster(&master);
defer active_master = null;
var transport: transport_mod.Transport = .{};
transport.sample_rate = 48_000;
const blank = try serialize(alloc, &.{}, &transport);
defer alloc.free(blank);

var fx = M{};
var fx_mach = test_machine;
fx_mach.state = &fx;
fx_mach.deinit = M.destroy;
try master.addEffect(alloc, fx_mach, 0);
master.adoptMachines(false);
try std.testing.expectEqual(@as(usize, 1), master.audioEffects().len);

var loaded_buf: [1]track_mod.Track = undefined;
var loaded_count: usize = 0;
try apply(alloc, blank, &reg, loaded_buf[0..], &loaded_count, &transport, test_machine);
try std.testing.expectEqual(@as(usize, 0), master.effects.items.len);
// The audio side still holds the old effect: it must be alive.
master.collectMachines(alloc);
try std.testing.expectEqual(@as(usize, 0), fx.freed);
// Still ringing: the old chain renders its last chunk, then switches.
master.adoptMachines(true);
try std.testing.expectEqual(@as(usize, 1), master.audioEffects().len);
try std.testing.expect(@intFromPtr(master.audioEffects()[0].mach.state) == @intFromPtr(&fx));
master.collectMachines(alloc);
try std.testing.expectEqual(@as(usize, 0), fx.freed);
master.swap = .out_done;
master.adoptMachines(true);
try std.testing.expectEqual(@as(usize, 0), master.audioEffects().len);
master.collectMachines(alloc);
try std.testing.expectEqual(@as(usize, 1), fx.freed);
}
3 changes: 3 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2092,6 +2092,9 @@ pub fn main(init: std.process.Init) !void {
}

for (tracks) |*t| t.publishSnapshot(&audio_pool);
// The master's retired effects (a load or an edit) free once audio
// has moved past them.
master.collectMachines(alloc);
// Bounces go stale as their sources change (docs/27 §Provenance).
if (c.rl.GetTime() >= next_recipe_check and !bounce_job.active) {
next_recipe_check = c.rl.GetTime() + 0.5;
Expand Down
Loading