Skip to content
Open
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ GitHub are its section here (`tools/release.sh` copies them), so add to

## Unreleased

- **A loudness stage for the master.** The FX Rack has a new `master-loud`
preset: glue compression, a diode clip that rounds off the peaks, and a
limiter, behind four macros. LOUD drives the clip and the limiter
together, so the mix gets denser instead of just hotter: on neon_tide it
takes -13 LUFS to -11 at the default, -9 at 0.8 and -8 at full, with the
peak-to-average gap falling from 15 dB to 11. GLUE, CLIP and CEIL
trim it. Put it last on the master.
- **The machine menu shows every machine again.** The "+" and replace
menus stopped at 40 rows, so FX Rack, Rack and the newest effects (the
harsh pack) were cut off the end. The cap is 72, and "+" on the master
lists effects only. Dropping an effect or rack preset from the browser
onto the machine bay while the master is selected now lands on the
master chain, not the first track. In the mixer, dropping a preset on a
strip (a track, a bus or the master) loads it there.

## 0.0.15 — 2026-10-22

- **The SM-24 Mono has a patch panel.** Its new PATCH page holds four
Expand Down
7 changes: 7 additions & 0 deletions docs/21-production-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,13 @@ eq2 (hpf 25 Hz, gentle air shelf) → comp2 gentle-bus-glue → limiter2 (ceil
knee changed. Compare meters on the same WAV before encoding.
- **Limiter gain**: raise it until the integrated loudness reaches the
target, then stop.
- **Or use the FX Rack `master-loud`** as the last master effect: glue,
a diode clip and a limiter behind four macros (LOUD, GLUE, CLIP, CEIL).
LOUD drives clip and limiter together, so it gets denser, not only
hotter. A mix with a 15 dB peak-to-average gap, as an open mix with
only a gentle limiter leaves, reads about -11 LUFS at 0.5 and -9 at 0.8.
The float mix never clips on its own, so nothing makes a mix loud unless
something on the master does.

| Target | Integrated |
|---|---|
Expand Down
1 change: 1 addition & 0 deletions machines/fxrack/presets/master-loud.preset
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"slab": "preset", "schema": 1, "machine": "fxrack", "note": "A loudness stage for the master: GLUE compresses, CLIP rounds off the peaks, then a limiter at CEIL. LOUD drives the clip and the limiter together, so it gets denser, not just louder.", "rack": 2, "kind": "effect", "lanes": [{"name": "MASTER", "effects": [{"machine": "comp2", "params": {"comp-thresh": -12, "comp-ratio": 2, "comp-knee": 8, "comp-atk": 0.01, "comp-rel": 0.15, "comp-hpf": 80, "comp-det": 1, "comp-makeup": 0, "comp-mix": 1}}, {"machine": "sat2", "params": {"sat-drive": 4, "sat-mode": 3, "sat-sag": 0, "sat-color": 1, "sat-tone": 18000, "sat-mix": 0.5, "sat-out": -2}}, {"machine": "limiter2", "params": {"lim-gain": 8, "lim-ceil": -1.0, "lim-look": 2, "lim-rel": 0.12}}]}], "macros": [{"name": "LOUD", "value": 0.5}, {"name": "GLUE", "value": 0.4}, {"name": "CLIP", "value": 0.5}, {"name": "CEIL", "value": 0.6}, {"name": "", "value": 0}, {"name": "", "value": 0}, {"name": "", "value": 0}, {"name": "", "value": 0}], "chain": 0, "maps": [{"macro": 1, "lane": 1, "insert": 3, "control": "lim-gain", "min": 0, "max": 16}, {"macro": 1, "lane": 1, "insert": 2, "control": "sat-drive", "min": 0, "max": 9}, {"macro": 1, "lane": 1, "insert": 2, "control": "sat-out", "min": 0, "max": -5}, {"macro": 2, "lane": 1, "insert": 1, "control": "comp-thresh", "min": -3, "max": -22}, {"macro": 2, "lane": 1, "insert": 1, "control": "comp-makeup", "min": 0, "max": 3}, {"macro": 3, "lane": 1, "insert": 2, "control": "sat-mix", "min": 0, "max": 1}, {"macro": 4, "lane": 1, "insert": 3, "control": "lim-ceil", "min": -3, "max": -0.3}]}
22 changes: 18 additions & 4 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1471,7 +1471,7 @@ pub fn main(init: std.process.Init) !void {
.preset, .table => if (sel_t) |t| .{ .header = t } else .new_track,
else => if (sel_t) |t| .{ .lane = .{ .track = t, .beat = trackEnd(&tracks[t]) } } else .new_track,
};
const app = App{ .alloc = alloc, .history = &history, .tracks_buf = &tracks_buf, .track_count = &track_count, .tracks = &tracks, .transport = &transport, .engine = &engine, .audio = &audio, .reg = &reg, .pool = &audio_pool, .selected_track = &selected_track, .selected_clip = &selected_clip, .prev_selected_clip = &prev_selected_clip, .project_path = &project_path, .project_path_chosen = &project_path_chosen, .dirty = &dirty, .status = &status, .edit_snap = edit_snap };
const app = App{ .alloc = alloc, .history = &history, .tracks_buf = &tracks_buf, .track_count = &track_count, .tracks = &tracks, .transport = &transport, .engine = &engine, .audio = &audio, .reg = &reg, .pool = &audio_pool, .selected_track = &selected_track, .selected_clip = &selected_clip, .prev_selected_clip = &prev_selected_clip, .project_path = &project_path, .project_path_chosen = &project_path_chosen, .dirty = &dirty, .status = &status, .edit_snap = edit_snap, .master = &master };
if (browserDrop(app, &lib, br.selection(), cur, target)) lib_stale = true;
rects = layout.compute(sw, sh);
};
Expand Down Expand Up @@ -1995,6 +1995,11 @@ pub fn main(init: std.process.Init) !void {
if (kind == .song and pane.contains(rects.arrangement, mx, my)) {
target = .open_song;
browser.drawTarget(ui, uiRect(rects.arrangement), true, "OPEN THIS SONG");
} else if (layout.mixer_visible and mixer.dropHit(mx, my) != null) {
const hit = mixer.dropHit(mx, my).?;
target = if (hit.master) .master else .{ .header = hit.track };
drop_ok = dropAccepts(kind, target, tracks);
browser.drawTarget(ui, hit.rect, drop_ok, if (kind == .table) "LOAD TABLE" else if (kind == .preset) "LOAD PRESET" else "");
} else if (!layout.mixer_visible and arrangement.dropHit(tracks, mx, my) != null) {
const hit = arrangement.dropHit(tracks, mx, my).?;
if (hit.track) |ti| {
Expand All @@ -2013,8 +2018,8 @@ pub fn main(init: std.process.Init) !void {
.new_track => browser.drawNewTrack(ui, hit.lane, drop_ok),
else => {},
}
} else if (bay.contains(@intFromFloat(mx), @intFromFloat(my)) and selected_track != null) {
target = .{ .header = selected_track.? };
} else if (bay.contains(@intFromFloat(mx), @intFromFloat(my)) and (device_sel == .master or selected_track != null)) {
target = if (device_sel == .master) .master else .{ .header = selected_track.? };
drop_ok = dropAccepts(kind, target, tracks);
browser.drawTarget(ui, bay, drop_ok, if (kind == .table) "LOAD TABLE" else if (kind == .preset) "LOAD PRESET" else "");
}
Expand All @@ -2024,7 +2029,7 @@ pub fn main(init: std.process.Init) !void {
} else if (!ui.raw_in.down) {
browser.endDrag(ui, &br);
if (drop_ok) {
const app = App{ .alloc = alloc, .history = &history, .tracks_buf = &tracks_buf, .track_count = &track_count, .tracks = &tracks, .transport = &transport, .engine = &engine, .audio = &audio, .reg = &reg, .pool = &audio_pool, .selected_track = &selected_track, .selected_clip = &selected_clip, .prev_selected_clip = &prev_selected_clip, .project_path = &project_path, .project_path_chosen = &project_path_chosen, .dirty = &dirty, .status = &status, .edit_snap = edit_snap };
const app = App{ .alloc = alloc, .history = &history, .tracks_buf = &tracks_buf, .track_count = &track_count, .tracks = &tracks, .transport = &transport, .engine = &engine, .audio = &audio, .reg = &reg, .pool = &audio_pool, .selected_track = &selected_track, .selected_clip = &selected_clip, .prev_selected_clip = &prev_selected_clip, .project_path = &project_path, .project_path_chosen = &project_path_chosen, .dirty = &dirty, .status = &status, .edit_snap = edit_snap, .master = &master };
if (browserDrop(app, &lib, br.selection(), cur, target)) lib_stale = true;
rects = layout.compute(sw, sh);
}
Expand Down Expand Up @@ -5337,6 +5342,9 @@ const BrowserDrop = union(enum) {
header: usize,
/// Below the tracks: a new track with the item on it.
new_track,
/// The master chain (the machine bay while the master is selected):
/// an effect preset joins it.
master,
open_song,
};

Expand All @@ -5360,6 +5368,7 @@ const App = struct {
dirty: *bool,
status: *StatusMessage,
edit_snap: snap_mod.Setting,
master: *track_mod.Track,
};

/// The beat after a track's last clip.
Expand All @@ -5380,6 +5389,7 @@ fn dropAccepts(kind: library_mod.Kind, d: BrowserDrop, tracks: []const track_mod
else => false,
},
.header => kind == .preset or kind == .table,
.master => kind == .preset,
};
}

Expand Down Expand Up @@ -5408,6 +5418,10 @@ fn browserDrop(app: App, lib: *library_mod.Library, sel: []const u32, first: u32
queueOpen(app.alloc, it.path);
return true;
}
if (target == .master) {
if (it.kind == .preset and dropPreset(app, app.master, it, false)) app.dirty.* = true;
return false;
}
// One undo step for the whole drop, a new track included.
const before = document_mod.serialize(app.alloc, app.tracks.*, app.transport) catch return false;
var ti: usize = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/ui/machine_bay.zig
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ pub fn draw(ui: *Ui, r_legacy: c.rl.Rectangle, device: ?*Track, track_idx: ?usiz
_ = ui.plate(Rect.xywh(x + TITLE_H, area.y, area.right() - x - TITLE_H, TITLE_H), .{});
_ = ui.plate(Rect.xywh(x, area.y + TITLE_H, area.right() - x, area.h - TITLE_H), .{});
}
if (machinePickerMenu(ADD_MENU_KEY, reg)) |pick| {
if (machinePickerMenuFor(ADD_MENU_KEY, reg, if (is_bus) .effects else .all)) |pick| {
result.add_machine = pick.reg_idx;
result.add_preset = pick.preset;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/menu.zig
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ fn commandShortcut(cmd: EditCommand) ?[]const u8 {
// ── Metrics ──────────────────────────────────────────────────────────

pub const MAX_DEPTH = 4;
const MAX_ITEMS = 40;
const MAX_ITEMS = 72;
const LABEL_MAX = 64;
const HINT_MAX = 16;
const ROW_H: i32 = 18;
Expand Down
33 changes: 31 additions & 2 deletions src/ui/mixer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ pub const Result = struct {
color_pick: ?arrangement.ColorPick = null,
};

/// Where a browser drop lands in the last drawn mixer: a strip, or the
/// master (`master`). Rebuilt every frame by `draw`.
pub const StripHit = struct { track: usize = 0, master: bool = false, rect: Rect = .{} };
var hits: [routing.MAX_TRACKS + 1]StripHit = undefined;
var hit_n: usize = 0;

pub fn dropHit(x: f32, y: f32) ?StripHit {
const ix: i32 = @intFromFloat(x);
const iy: i32 = @intFromFloat(y);
for (hits[0..hit_n]) |h| if (h.rect.contains(ix, iy)) return h;
return null;
}

pub fn draw(
ui: *Ui,
r_legacy: c.rl.Rectangle,
Expand All @@ -74,6 +87,7 @@ pub fn draw(
beat: f64,
) Result {
var res: Result = .{};
hit_n = 0;
const r = bridge.fromRl(r_legacy);
ui.rect(r, ui_style.chassis);
var area = r;
Expand Down Expand Up @@ -136,26 +150,41 @@ pub fn draw(
ui.clip(strips_r);
var x = strips_r.x - scroll_x;
for (o.main()) |row| {
drawStrip(ui, Rect.xywh(x, strips_r.y, STRIP_W, strips_r.h), tracks, &o, row.ti, send_rows, device_sel, selected_track, beat, &res);
const sr = Rect.xywh(x, strips_r.y, STRIP_W, strips_r.h);
drawStrip(ui, sr, tracks, &o, row.ti, send_rows, device_sel, selected_track, beat, &res);
addHit(row.ti, false, sr, strips_r);
x += STRIP_W;
}
// Nothing shows bare chassis (docs/06 §Packing).
if (x < strips_r.right()) _ = ui.plate(Rect.xywh(x, strips_r.y, strips_r.right() - x, strips_r.h), .{ .fill = ui_style.face.shade(-4) });
ui.unclip();

const bus_area_full = bus_area;
ui.clip(bus_area);
for (o.returns()) |row| {
drawStrip(ui, bus_area.cutLeft(STRIP_W), tracks, &o, row.ti, send_rows, device_sel, selected_track, beat, &res);
const sr = bus_area.cutLeft(STRIP_W);
drawStrip(ui, sr, tracks, &o, row.ti, send_rows, device_sel, selected_track, beat, &res);
addHit(row.ti, false, sr, bus_area_full);
}
ui.unclip();

drawMasterStrip(ui, master_r, master, send_rows, device_sel);
addHit(0, true, master_r, master_r);
dragStrips(ui, tracks, &o, selected_track, band, strips_r, bus_x0, &res);
if (route_menu.tick(tracks, selected_track.*)) |e| res.route = e;
input_menu.tick(tracks);
return res;
}

fn addHit(ti: usize, is_master: bool, r: Rect, clip: Rect) void {
if (hit_n >= hits.len) return;
const x0 = @max(r.x, clip.x);
const x1 = @min(r.right(), clip.right());
if (x1 <= x0) return;
hits[hit_n] = .{ .track = ti, .master = is_master, .rect = Rect.xywh(x0, r.y, x1 - x0, r.h) };
hit_n += 1;
}

/// The rows every strip shares, so sections line up across the page.
const Rows = struct {
title: Rect,
Expand Down
Loading