feat: N-channel audio support — per-channel EQ + routing (Linux)#1
Merged
Conversation
…g matrix Foundational N-channel support (backlog item 16), platform-independent layer. - dsp: ChannelMask (per-band channel targeting) + ChannelMatrix (out×in remap); mask-aware band loop; ProcessorChain.routing + route()/out_channels(); set_channels() resizes filter state and rebuilds effects; Surround gated to exactly stereo. Offline N-channel test harness (channel_tests.rs). - ipc: ChannelMask/RoutingMatrix wire types (i64 bit-cast serde so the all-ones default survives TOML's i64 range), BandState.channels, and the SetBandChannels/SetChannelRouting/SwapChannels/ClearRouting commands; DaemonState gains out_channels, channel_layout, routing. - daemon: AudioCommand SetBandChannels/SetRouting on both RT and shadow paths; build_band preserves a band's channel mask across param edits; snapshot reports channel state. - preset: EqBand.channels; APO Channel: directive parse + write; .toml profiles round-trip masks; presets predating per-channel EQ load as global bands. Backends still stereo-locked (Phase 3). make check is green.
- Single channel-count source of truth: audio::target_channels() (env RESONANCE_CHANNELS, default stereo), used by both the backend ports and the daemon's shadow chain so a ReplaceChain can never hand the RT thread a chain whose width disagrees with the live ports. - Virtual sink + pw_filter are built with N ports from the channel layout (FL,FR,FC,LFE,RL,RR,... for <=8, AUX0.. beyond); audio.position follows. - create_links pairs ports by audio.channel label with a positional fallback (was a hardcoded FL/FR pair). - RT callback interleaves/deinterleaves N channels, applies the square routing matrix (swap / per-channel gain), N-channel peak/RMS meters + mono spectrum. make check green. Live multichannel verification pending.
… status
resonance channel {info,swap,clear,band} drives the N-channel IPC commands;
status now shows a route line and per-band channel tags. Maps directly onto
the SwapChannels/ClearRouting/SetBandChannels commands (wire-tested in ipc).
…der, APO numerics Adversarial review of the diff surfaced these (all confirmed, several reproduced): - RT safety: SetChannelRouting never checked the matrix in_ch against the live channel count; a non-square matrix from IPC could panic or garble the RT audio thread. Now the daemon rejects a non-square/mismatched matrix, the PipeWire guard requires in_ch==out_ch==channels, and ChannelMatrix::apply clamps its frame loop to both buffers (panic-free for any matrix/buffer pair). - Wire compat: the four new Command variants were inserted mid-enum, shifting every later postcard ordinal, so a version-skewed client silently misdecodes. Moved them to the end of the enum. - APO: parse numeric Channel: positions (1-based, exact for any layout) and emit a numeric token for channels with no WAVE name, so per-channel bands round-trip losslessly instead of collapsing to ALL or dropping bits. - CLI: 'channel band' resolves names against the live layout (correct on 4/5/7ch) and rejects out-of-range indices; an out-of-range band index now errors; bare 'channel' runs info; '[none]' instead of '[]' for a degenerate mask. - Daemon: rebuild_chain / apply_profile build from the live RT rate, not the frozen shadow rate (a transient wrong-rate glitch on preset load on non-48k graphs, e.g. a 96 kHz DAC). - Doc: corrected the serde(default) comment (TOML/JSON disk only, not postcard). New tests: ChannelMatrix::apply clamp, APO numeric + high-channel round-trip.
This was referenced Jun 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
N-channel audio support — per-channel EQ + routing (backlog item 16)
Removes the hard stereo assumption end-to-end on Linux: Resonance can now process an arbitrary channel count, target EQ bands at individual channels, and remap channels.
What landed
DSP core (
resonance-dsp)ChannelMask— per-band channel targeting (bitset, defaultALL= global, fully back-compatible).ChannelMatrix—out×inrouting/remap (identity / swap / per-channel gain), allocation-freeapply, frame loop clamped to both buffers (panic-free for any matrix).ProcessorChain: mask-aware band cascade,routingstage +route()/out_channels(),set_channels(n)(resizes filter state, rebuilds effects). Surround is gated to exactly stereo.Data model (
resonance-ipc,resonance-preset, daemon)ChannelMask(i64 bit-cast serde so the all-ones default survives TOML's i64 range) +RoutingMatrix;BandState.channels;DaemonState.{out_channels, channel_layout, routing}.SetBandChannels/SetChannelRouting/SwapChannels/ClearRouting(appended at the end of the enum to keep postcard ordinals stable)..txtChannel:directive parse + write (named and 1-based numeric positions);.tomlprofiles round-trip masks; presets predating per-channel EQ load as global.PipeWire backend (
resonance-daemon)audio::target_channels()— single source of truth (envRESONANCE_CHANNELS, default stereo), used by both the backend ports and the daemon's shadow chain so aReplaceChaincan never hand the RT thread a width-mismatched chain.audio.channellabel with positional fallback; N-channel RT interleave/deinterleave + square routing.CLI
resonance channel info | swap | clear | band+ per-channel tags and a routing line instatus.bandresolves channel names against the live layout (correct on 4/5/7ch) and range-checks indices.Verification
make checkgreen (fmt + clippy-D warnings+test --all); offline N-channel harness inchannel_tests.rs.module-null-sink channels=6device — virtual sink + 6-in/6-out filter, all ports linked FL/FR/FC/LFE/RL/RR by label.channel info, per-band targeting ([FL]),swap/clearrouting display, out-of-range index/channel rejection, barechannel→ info.2740714(routing in_ch validation + RT guard + apply clamp; mid-enum variant insertion; APO numeric channels + lossless write; CLI layout-aware names + range checks; preset-load wrong-rate glitch on non-48k graphs).Not in this PR (follow-ups)