Skip to content

Mixer: gain stages, telemetry arena, port changes between ticks - #950

Open
jcelerier wants to merge 19 commits into
masterfrom
feature/mixer
Open

jcelerier wants to merge 19 commits into
masterfrom
feature/mixer

Conversation

@jcelerier

Copy link
Copy Markdown
Member

The libossia side of the ossia score mixer (ossia/score feature/mixer). 18 commits, each with its test where one applies.

Gain chain

  • Interval gain once. A bus applied its interval's gain and pan twice (0.25 instead of 0.5, proven by IntervalGainTest). forward_node now forwards, and the outlet applies gain and pan once. has_gain is gone.
  • Device gain stages. Each audio_parameter applies its gain at exactly one point: when the graph writes to it (outputs, virtual ports), when the graph reads it (inputs), or after the whole mix (/out/main). The gain is atomic.
  • /out/main is the master. audio_protocol::apply_main_gain trims every hardware output once the tick is summed, including what was sent straight to /out/N. reset_main_gain_ramp makes a new run start at the master gain.
  • /in/main scales every input read.
  • Ramps. Gain and pan changes ramp over one buffer in audio_outlet::post_process. pan is never grown on the audio thread.
  • Upmix per outlet: off, repeat the channels up to N, or add silent channels up to N.

Telemetry: execution → interface

ossia/dataflow/telemetry.{hpp,cpp} adds an arena of meter, bench and playhead taps.

  • The tick publishes one frame through a triple buffer at the interface's rate.
  • Levels the interface has not read are folded into the next frame. triple_buffer::publish_if_unread (a CAS) makes sure a frame read meanwhile is not delivered twice.
  • Frames are written in place and only copy the channels in use.
  • RMS is computed per channel, in double precision, over the frames that channel was there for.

Taps sit on audio outlets (after gain and pan), audio inlets, the hardware inputs and outputs, virtual audio parameters, and interval playheads. Node timing goes to bench taps instead of the bench_map that was copied on the audio thread (perf_map is now bench).

triple_buffer gains write_buffer/publish and consume()/read_buffer for in-place use, in both the general and the trivially copyable case.

Real-time safety

  • audio_engine::run_between_ticks(f) runs a short, allocation-free change at the start of the next callback and waits for it. No callback is skipped. When no callback comes, it runs f on the caller.
  • Deferred port lists. With defer_port_changes(true), the callback walks its own copy of the mapped and virtual port lists, which swap_ports hands over between two ticks. virtual_audio_parameter::make_channels/swap_channels resize a virtual port the same way.
  • Stale entries. The local-state policy no longer pushes to a parameter nothing wrote during the tick: its entries outlive their parameters.
  • Faust nodes publish their control and display values (faust_ui_values) instead of the interface reading the DSP's zones. A mono generator makes one channel with nothing plugged in, and follows the channels of a cable plugged into it.
  • The dummy engine can have channels.

Tests

New: TelemetryTest, TripleBufferTest, MainGainTest, IntervalGainTest, AudioEngineBetweenTicksTest. Also passing: OutletUnitTest, DataflowTest, SoundTest.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX

@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 03:12 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
@jcelerier
jcelerier deployed to Apple Certificate September 24, 2026 18:55 — with GitHub Actions Active
jcelerier and others added 6 commits October 8, 2026 12:33
A bus's forward_node scaled its output by pan and gain, then
audio_outlet::post_process scaled the same outlet again: a bus at 0.5
played at 0.25, and a stereo pan of {0.5, 1} came out as {0.25, 1}. The
node now only copies, and post_process is the one place gain and pan are
applied, for every audio outlet. has_gain no longer means anything and is
gone, along with the two helpers that scaled while copying.

Buses set below unity in existing sessions will sound louder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
The gain of /out/main scaled only what was written to /out/main, one writer
at a time. It is now a post-sum trim: audio_protocol::apply_main_gain scales
every hardware output once the tick has been summed into them, so signals
sent straight to /out/N follow the master too. A change ramps over the
buffer.

Each audio_parameter now says where its gain applies: on what the graph
writes (outputs, virtual ports), on what it reads (inputs, whose gain was
ignored until now), or outside, after the sum (/out/main). The gain itself
is an atomic float, since the UI writes it while the audio thread reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
A gain or pan change now ramps linearly over one buffer instead of
stepping, which clicked. The first buffer an outlet carries is scaled
directly: there is nothing to ramp from. Pan ramps on the first 16
channels; past those, a change steps.

Channels past the end of the pan weights have a weight of 1. They used to
get one pushed onto the vector on the audio thread, which allocated as soon
as a signal had more than two channels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
ossia::telemetry::arena gathers informative data from the execution into
one frame at fixed slots, handed to the interface through a triple buffer.
Every metering point accumulates peak, sum of squares and clips on its own
writer thread; the tick publishes once per period. A frame the interface
has not read yet is folded into the next one, so no peak is lost whatever
the reading rate, and nothing is allocated or locked on the audio thread.

An audio outlet can carry a tap, fed after its gain and pan. The hardware
inputs and outputs have taps of their own, fed from the driver's buffers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
Every program with at most one input and one output runs as a mono effect,
one clone per incoming channel. A generator has no input, so with nothing
cabled into it it made no channel at all: `process = os.osc(440);` in a box
was silent. It now makes one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
It hands the graph silent inputs and discards its outputs, so that a
session without a sound card can still be routed and metered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
jcelerier and others added 4 commits October 8, 2026 12:33
The interface sets the address of a tap that the telemetry arena owns;
what the graph writes to the port is accumulated into it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
The local state keeps an entry for every parameter it ever wrote to and
writes to all of them at each commit, so removing a port that a process
had written to crashed the next tick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
audio_engine::run_between_ticks runs a short change on the audio thread at
the start of the next callback and waits for it; run_parked, which skipped
callbacks, is gone. The audio protocol can defer port changes: its callback
walks its own lists, swapped in between two ticks, and a virtual port's
channels are made off the audio thread and swapped the same way.

The execution state no longer writes to a parameter nothing wrote to during
the tick: its entries outlive their parameters. execution_state::forget is
removed, as it is not needed any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
- triple_buffer: the trivially copyable case has the in-place interface
  too, so that tuples of floats (tuplet) keep working; publish_if_unread
  replaces a value only if nobody read it.
- telemetry: a frame the interface reads while the next one is being made
  is no longer delivered twice: the new frame carries the unread levels
  only if it replaces an unread frame. Frames are written in place, and
  only the channels in use are copied. RMS is computed per channel, over
  the frames that channel was there for, in double precision.
- audio_protocol: reset_main_gain_ramp, so that a run starts at the master
  gain instead of ramping from the one of the previous run.
- faust: a mono generator with a signal plugged into its inlet makes one
  channel per incoming channel again; with nothing plugged in, one.
- graph: perf_map is called bench, which it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKbSZft9tvQ2sm9KeSchMX
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 10, 2026 02:18 — with GitHub Actions Active
…hes it

/out/N applied its gain to what was written to it, before the sum. Sound sent
to /out/main or to a mapped port reached the channel without it, so a mixer's
output fader did nothing to most of what its channel played. It is now applied
with /out/main's, once the tick is summed (gain_stage::external), ramped over
the buffer like the master.

/in/N likewise only scaled what read /in/N. It now also trims its channel for
/in/main and for the mapped input ports: audio_parameter::read_gain gives the
gain of each channel a parameter reads, and the mixer meters with it.

A mapped port's own gain still applies to what goes through that port only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lc7tYGHRn6TmaxMEfBTswW
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses
edumeneses deployed to Apple Certificate October 12, 2026 15:13 — with GitHub Actions Active
@edumeneses

Copy link
Copy Markdown
Contributor

Hi JM, I pushed 51236ae to this branch, after Edu tested the mixer and decided how the hardware faders should behave.

What Edu saw: with sound sent to /out/main, the /out/1 fader in the mixer did nothing. With the gain stages here, /out/N only scales what is written straight to /out/N, before the sum. Anything reaching the channel through /out/main or through a mapped port skips that gain. The same holds for inputs: /in/3 didn't touch /in/main or a mapped port reading input 3.

What changed: /out/N and /in/N now act like the channel strips of a console.

  • Outputs: /out/N is now gain_stage::external. apply_main_gain applies /out/main × /out/N to each channel once the tick is summed, with a ramp over the buffer per channel. reset_main_gain_ramp also resets the channel ramps.
  • Inputs: audio_parameter::read_gain(chan) gives the gain each channel is read with: the parameter's own gain (pull stage) × /in/main × the channel's /in/N. /in/N itself isn't counted twice. clone_value uses it, and so do the score mixer's input meters.
    • hardware_inputs points to audio_ins. It is set on /in/main and on the mapped ports.
    • hardware_channel is identity for /in/main and follows mapping for an input mapped port.
  • A mapped port's own gain still applies only to what goes through that port, like a send.

Tests: MainGainTest has 7 cases and 96 assertions. I changed the old /out/N expectation and added a channel-gain ramp case and an /in/N trim case. TelemetryTest passes, and so do score's audio, port edit, telemetry, routing and outlet tests.

The score side is ossia/score#2302 08e951e4e1: the per-channel input meter, the help text of the Inputs/Outputs tabs, and the submodule bump. If you'd rather have this as a separate PR on top of #950, tell us and I'll split it out.

— Edu's Claude assistant

This branch was successfully deployed

1 active deployment
Apple Certificate — 51236ae4 Deployed Oct 12, 2026 by edumeneses via build-macos #1270
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants