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
95 changes: 91 additions & 4 deletions backend/src/blocks/builtin/mixer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ impl BlockBuilder for MixerBuilder {
});
}

// Add per-channel direct outputs
if get_bool_prop(properties, "direct_outs", false) {
for ch in 0..num_channels {
outputs.push(ExternalPad {
name: format!("direct_out_{}", ch + 1),
label: Some(format!("D{}", ch + 1)),
media_type: MediaType::Audio,
internal_element_id: format!("direct_out_tee_{}", ch),
internal_pad_name: "src_%u".to_string(),
});
}
}

Some(ExternalPads { inputs, outputs })
}

Expand All @@ -112,6 +125,7 @@ impl BlockBuilder for MixerBuilder {
let num_aux_buses = parse_num_aux_buses(properties);
let num_groups = parse_num_groups(properties);
let sample_rate = parse_sample_rate(properties);
let direct_outs = get_bool_prop(properties, "direct_outs", false);
let dsp_backend = get_string_prop(properties, "dsp_backend", "rust");
if dsp_backend != "rust" && dsp_backend != "lv2" {
warn!(
Expand Down Expand Up @@ -1213,10 +1227,83 @@ impl BlockBuilder for MixerBuilder {
ElementPadRef::element(&routing_tee_id),
ElementPadRef::pad(&to_main_vol_id, "sink"),
));
internal_links.push((
ElementPadRef::pad(&to_main_vol_id, "src"),
ElementPadRef::pad(&to_main_queue_id, "sink"),
));
if direct_outs {
// Direct out: what this channel contributes to Main, with
// fader, mute and the to-main switch applied, before the sum.
// to_main_vol → direct_tee → to_main_queue
// direct_tee → direct_queue → direct_out_tee → direct_out_N
let direct_tee_id = format!("{}:direct_tee_{}", instance_id, ch);
let direct_tee = gst::ElementFactory::make("tee")
.name(&direct_tee_id)
.property("allow-not-linked", true)
.build()
.map_err(|e| {
BlockBuildError::ElementCreation(format!("direct_tee ch{}: {}", ch_num, e))
})?;
elements.push((direct_tee_id.clone(), direct_tee));

// The tee pushes to both branches on the channel's streaming
// thread, so a direct-out consumer that stops pulling would
// block it and silence the channel on Main. The leaky queue
// drops direct-out audio instead.
//
// A consumer that syncs to the clock holds each buffer for the
// flow's latency, so the queue must hold that much audio or it
// leaks nearly all of it. The default limits (1 s, 200
// buffers) are below a 1-2 s flow latency, or below 400 ms
// with 1 ms buffers. A consumer that stalls and resumes
// first gets up to this much old audio.
let direct_queue_id = format!("{}:direct_queue_{}", instance_id, ch);
let direct_queue = gst::ElementFactory::make("queue")
.name(&direct_queue_id)
.property_from_str("leaky", "downstream")
.property("max-size-time", 3 * gst::ClockTime::SECOND.nseconds())
.property("max-size-buffers", 0u32)
.property("max-size-bytes", 0u32)
.build()
.map_err(|e| {
BlockBuildError::ElementCreation(format!(
"direct_queue ch{}: {}",
ch_num, e
))
})?;
elements.push((direct_queue_id.clone(), direct_queue));

let direct_out_tee_id = format!("{}:direct_out_tee_{}", instance_id, ch);
let direct_out_tee = gst::ElementFactory::make("tee")
.name(&direct_out_tee_id)
.property("allow-not-linked", true)
.build()
.map_err(|e| {
BlockBuildError::ElementCreation(format!(
"direct_out_tee ch{}: {}",
ch_num, e
))
})?;
elements.push((direct_out_tee_id.clone(), direct_out_tee));

internal_links.push((
ElementPadRef::pad(&to_main_vol_id, "src"),
ElementPadRef::pad(&direct_tee_id, "sink"),
));
internal_links.push((
ElementPadRef::element(&direct_tee_id),
ElementPadRef::pad(&to_main_queue_id, "sink"),
));
internal_links.push((
ElementPadRef::element(&direct_tee_id),
ElementPadRef::pad(&direct_queue_id, "sink"),
));
internal_links.push((
ElementPadRef::pad(&direct_queue_id, "src"),
ElementPadRef::pad(&direct_out_tee_id, "sink"),
));
} else {
internal_links.push((
ElementPadRef::pad(&to_main_vol_id, "src"),
ElementPadRef::pad(&to_main_queue_id, "sink"),
));
}
internal_links.push((
ElementPadRef::pad(&to_main_queue_id, "src"),
ElementPadRef::element(&mixer_id),
Expand Down
15 changes: 15 additions & 0 deletions backend/src/blocks/builtin/mixer/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ pub(super) fn mixer_definition() -> BlockDefinition {
live: false,
persist: None,
},
// Per-channel direct outputs — construction-time only.
ExposedProperty {
name: "direct_outs".to_string(),
label: "Direct Outs".to_string(),
description: "Add a direct_out_N output per channel: the channel's contribution to Main (after fader, mute and the to-Main switch, before the sum). A channel routed only to a group is silent on it. A channel with no input sends nothing, so an output that waits for its first audio (NDI, DeckLink audio, TAMS) gets nothing and keeps the flow from reporting playing until the channel has input; Main keeps playing.".to_string(),
property_type: PropertyType::Bool,
default_value: Some(PropertyValue::Bool(false)),
mapping: PropertyMapping {
element_id: "_block".to_string(),
property_name: "direct_outs".to_string(),
transform: None,
},
live: false,
persist: None,
},
// Monitor master level — drives the Monitor bus output. The Monitor
// bus follows Main when no channel has PFL or AFL active, and
// switches to the solo mix as soon as any PFL/AFL is engaged.
Expand Down
6 changes: 6 additions & 0 deletions backend/src/blocks/builtin/mixer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
//! ├→ solo_mixer
//! group_K_out_tee → group_afl_volume_K → group_afl_queue_K ─┘
//! ```
//! With `direct_outs` on, each channel also gets a direct output tapped after
//! its to-Main switch:
//! ```text
//! routing_tee_N → to_main_vol_N → direct_tee_N → to_main_queue_N → main audiomixer
//! direct_tee_N → direct_queue_N (leaky) → direct_out_tee_N
//! ```
//! `level_N` sits pre-fader so the channel meter shows the signal hitting the
//! fader regardless of fader position or mute. Bus meters (`main_level`,
//! `monitor_level`, `auxN_level`, `groupN_level`) sit on the bus output,
Expand Down
Loading
Loading