Skip to content

Commit b24823d

Browse files
fix(loudness): distinguish side and rear surround weights
- weights of 1.41 to sides and 1.0 to 7.1 rears. resolve legacy quad surrounds to side channels while preserving negotiated mappings Signed-off-by: Maika Namuo <httpworldview@gmail.com>
1 parent 96ca37c commit b24823d

4 files changed

Lines changed: 470 additions & 132 deletions

File tree

‎src/dsp.rs‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@ impl ChannelPosition {
7272
}
7373
positions
7474
}
75+
76+
pub(crate) fn resolve_surrounds(positions: &mut [Self]) {
77+
// Legacy quad/5.x calls its surround pair RL/RR. Only layouts with
78+
// SL/SR distinguish those surrounds from the rear pair of 7.x.
79+
if !positions
80+
.iter()
81+
.any(|position| matches!(position, Self::SideLeft | Self::SideRight))
82+
{
83+
for position in positions {
84+
*position = match *position {
85+
Self::RearLeft => Self::SideLeft,
86+
Self::RearRight => Self::SideRight,
87+
other => other,
88+
};
89+
}
90+
}
91+
}
7592
}
7693

7794
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -187,9 +204,10 @@ impl<'a> AudioBlock<'a> {
187204
samples: &'a [f32],
188205
channels: usize,
189206
sample_rate: f32,
190-
positions: [ChannelPosition; MAX_AUDIO_CHANNELS],
207+
mut positions: [ChannelPosition; MAX_AUDIO_CHANNELS],
191208
) -> Self {
192209
let channels = channels.clamp(1, MAX_AUDIO_CHANNELS);
210+
ChannelPosition::resolve_surrounds(&mut positions[..channels]);
193211
let stereo_channels = (2..channels.min(samples.len()))
194212
.rfind(|&channel| {
195213
samples[channel..]

‎src/infra/pipewire/live_tests.rs‎

Lines changed: 92 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,13 @@ impl GraphDump {
131131
.count()
132132
}
133133

134-
fn ports(&self, node: u32, direction: &str) -> Vec<u32> {
135-
let mut ports: Vec<_> = self
136-
.objects("PipeWire:Interface:Port")
137-
.filter(|object| {
138-
json_u64(object, "/info/props/node.id") == Some(u64::from(node))
139-
&& json_str(object, "/info/props/port.direction") == Some(direction)
140-
})
141-
.filter_map(|object| json_u64(object, "/id")?.try_into().ok())
142-
.collect();
143-
ports.sort_unstable();
144-
ports
134+
fn port(&self, node: u32, direction: &str, channel: &str) -> Option<u32> {
135+
let port = self.objects("PipeWire:Interface:Port").find(|object| {
136+
json_u64(object, "/info/props/node.id") == Some(u64::from(node))
137+
&& json_str(object, "/info/props/port.direction") == Some(direction)
138+
&& json_str(object, "/info/props/audio.channel") == Some(channel)
139+
})?;
140+
json_u64(port, "/id")?.try_into().ok()
145141
}
146142

147143
fn inactive(&self, name: &str) -> bool {
@@ -318,13 +314,25 @@ impl IsolatedPipeWire {
318314
)
319315
}
320316

321-
fn link_nodes(&self, output_node: u32, input_node: u32, channels: usize, passive: bool) {
322-
let (outputs, inputs) = self.wait_dump("test fixture ports", |graph| {
323-
let outputs = graph.ports(output_node, "out");
324-
let inputs = graph.ports(input_node, "in");
325-
(outputs.len() >= channels && inputs.len() >= channels).then_some((outputs, inputs))
317+
fn link_nodes(
318+
&self,
319+
output_node: u32,
320+
input_node: u32,
321+
channels: &[(&str, &str)],
322+
passive: bool,
323+
) {
324+
let ports: Vec<_> = self.wait_dump("test fixture ports", |graph| {
325+
channels
326+
.iter()
327+
.map(|(output, input)| {
328+
Some((
329+
graph.port(output_node, "out", output)?,
330+
graph.port(input_node, "in", input)?,
331+
))
332+
})
333+
.collect::<Option<Vec<_>>>()
326334
});
327-
for (output, input) in outputs.into_iter().zip(inputs).take(channels) {
335+
for (output, input) in ports {
328336
let mut command = self.client_command("pw-link");
329337
command.arg("--wait");
330338
if passive {
@@ -352,14 +360,15 @@ struct ApplicationFixture {
352360

353361
impl ApplicationFixture {
354362
fn active(server: &IsolatedPipeWire, name: &str) -> Self {
355-
Self::active_with_layout(server, name, 2, "[ FL, FR ]")
363+
Self::active_with_layout(server, name, 2, "[ FL, FR ]", ["FL", "FR"])
356364
}
357365

358366
fn active_with_layout(
359367
server: &IsolatedPipeWire,
360368
name: &str,
361369
channels: usize,
362370
channel_map: &str,
371+
signal_targets: [&str; 2],
363372
) -> Self {
364373
let target = format!("{name}.sink");
365374
let capture_props = format!(
@@ -381,19 +390,13 @@ impl ApplicationFixture {
381390
let capture = server.wait_dump("application capture node", |graph| {
382391
graph.node_id(&format!("{name}.capture"))
383392
});
384-
let signal_channels = channels.min(2);
385-
let signal_map = if signal_channels == 1 {
386-
"[ MONO ]"
387-
} else {
388-
"[ FL, FR ]"
389-
};
390-
let signal = server.test_source(
391-
&format!("{name}.signal"),
392-
"Audio/Source",
393-
signal_channels,
394-
signal_map,
393+
let signal = server.test_source(&format!("{name}.signal"), "Audio/Source", 2, "[ FL, FR ]");
394+
server.link_nodes(
395+
signal.id,
396+
capture,
397+
&[("FL", signal_targets[0]), ("FR", signal_targets[1])],
398+
false,
395399
);
396-
server.link_nodes(signal.id, capture, signal_channels, false);
397400
Self {
398401
_source: source,
399402
_signal: signal,
@@ -468,7 +471,7 @@ fn wide_device(server: &IsolatedPipeWire, name: &str) -> (Process, Process, Ling
468471
graph.node_id(&format!("{source_name}.capture"))
469472
});
470473
let signal = server.test_source(&format!("{name}.tone"), "Audio/Source", 2, "[ FL, FR ]");
471-
server.link_nodes(signal.id, capture, 2, false);
474+
server.link_nodes(signal.id, capture, &[("FL", "FL"), ("FR", "FR")], false);
472475
(loopback, source, signal)
473476
}
474477

@@ -503,6 +506,7 @@ fn wait_for_mapped_signal(
503506
description: &str,
504507
audio: &mut AudioReader,
505508
positions: [ChannelPosition; MAX_CAPTURE_CHANNELS],
509+
active: &[usize],
506510
) {
507511
wait_for(description, || {
508512
let mut captured = false;
@@ -519,8 +523,13 @@ fn wait_for_mapped_signal(
519523
*peak = peak.max(sample.abs());
520524
}
521525
}
522-
captured |= peaks[..2].iter().all(|peak| *peak > 0.01)
523-
&& peaks[2..].iter().all(|peak| *peak < 0.001);
526+
captured |= peaks.iter().enumerate().all(|(channel, &peak)| {
527+
if active.contains(&channel) {
528+
peak > 0.01
529+
} else {
530+
peak < 0.001
531+
}
532+
});
524533
});
525534
captured.then_some(())
526535
});
@@ -551,6 +560,7 @@ fn live_backend_recovers_after_server_restart() {
551560
"initial recovery PCM",
552561
&mut audio,
553562
ChannelPosition::SURROUND,
563+
&[0, 1],
554564
);
555565

556566
server.stop();
@@ -574,6 +584,7 @@ fn live_backend_recovers_after_server_restart() {
574584
"recovered application PCM",
575585
&mut audio,
576586
ChannelPosition::SURROUND,
587+
&[0, 1],
577588
);
578589

579590
backend.shutdown();
@@ -626,39 +637,49 @@ fn live_capture_preserves_graph_invariants() {
626637
"captured application PCM",
627638
&mut audio,
628639
ChannelPosition::SURROUND,
640+
&[0, 1],
629641
);
630642

631-
let surround_name = format!("openmeters-live-surround-{}", std::process::id());
632-
let surround_playback = format!("{surround_name}.playback");
633-
let surround = ApplicationFixture::active_with_layout(
634-
&server,
635-
&surround_name,
636-
6,
637-
"[ FL, FR, FC, LFE, RL, RR ]",
638-
);
639-
server.wait_audio("surround application mix", &mut audio, |graph| {
640-
let tap = graph.node(&tap_name)?;
641-
let source = graph.node_id(&surround_playback)?;
642-
(json_u64(tap, "/id") == Some(u64::from(tap_id))
643-
&& graph.link_count(source, tap_id) == 6
644-
&& graph.link_count(source_id, tap_id) == 2
645-
&& json_u64(tap, "/info/params/Format/0/channels") == Some(MAX_CAPTURE_CHANNELS as u64))
646-
.then_some(())
647-
});
648-
wait_for_mapped_signal(
649-
"captured surround application mix",
650-
&mut audio,
651-
ChannelPosition::SURROUND,
652-
);
653-
drop(surround);
654-
server.wait_audio("stable application mix", &mut audio, |graph| {
655-
let tap = graph.node(&tap_name)?;
656-
(json_u64(tap, "/id") == Some(u64::from(tap_id))
657-
&& graph.node(&surround_playback).is_none()
658-
&& graph.link_count(source_id, tap_id) == 2
659-
&& json_u64(tap, "/info/params/Format/0/channels") == Some(MAX_CAPTURE_CHANNELS as u64))
660-
.then_some(())
661-
});
643+
for (channels, channel_map, active_channels) in [
644+
(6, "[ RL, RR, FL, FR, FC, LFE ]", [0, 1, 6, 7]),
645+
(8, "[ RL, RR, FL, FR, FC, LFE, SL, SR ]", [0, 1, 4, 5]),
646+
] {
647+
let surround_name = format!("openmeters-live-surround-{channels}-{}", std::process::id());
648+
let surround_playback = format!("{surround_name}.playback");
649+
let surround = ApplicationFixture::active_with_layout(
650+
&server,
651+
&surround_name,
652+
channels,
653+
channel_map,
654+
["RL", "RR"],
655+
);
656+
server.wait_audio("surround application mix", &mut audio, |graph| {
657+
let tap = graph.node(&tap_name)?;
658+
let source = graph.node_id(&surround_playback)?;
659+
(json_u64(tap, "/id") == Some(u64::from(tap_id))
660+
&& graph.link_count(source, tap_id) == channels
661+
&& graph.link_count(source_id, tap_id) == 2
662+
&& json_u64(tap, "/info/params/Format/0/channels")
663+
== Some(MAX_CAPTURE_CHANNELS as u64))
664+
.then_some(())
665+
});
666+
wait_for_mapped_signal(
667+
"captured surround application mix",
668+
&mut audio,
669+
ChannelPosition::SURROUND,
670+
&active_channels,
671+
);
672+
drop(surround);
673+
server.wait_audio("stable application mix", &mut audio, |graph| {
674+
let tap = graph.node(&tap_name)?;
675+
(json_u64(tap, "/id") == Some(u64::from(tap_id))
676+
&& graph.node(&surround_playback).is_none()
677+
&& graph.link_count(source_id, tap_id) == 2
678+
&& json_u64(tap, "/info/params/Format/0/channels")
679+
== Some(MAX_CAPTURE_CHANNELS as u64))
680+
.then_some(())
681+
});
682+
}
662683

663684
let mut disabled = HashSet::new();
664685
disabled.insert(identity);
@@ -702,7 +723,12 @@ fn live_capture_preserves_graph_invariants() {
702723
let paused_source_id = server.wait_dump("paused application", |graph| {
703724
graph.node_id(&paused_playback)
704725
});
705-
server.link_nodes(paused_source_id, target_id, 2, true);
726+
server.link_nodes(
727+
paused_source_id,
728+
target_id,
729+
&[("FL", "FL"), ("FR", "FR")],
730+
true,
731+
);
706732
server.wait_audio("passive tap of paused route", &mut audio, |graph| {
707733
(graph.link_count(paused_source_id, target_id) == 2
708734
&& graph.link_count(paused_source_id, tap_id) == 2
@@ -746,7 +772,7 @@ fn live_capture_preserves_graph_invariants() {
746772
&& json_u64(tap, "/info/params/Format/0/channels") == Some(MAX_CAPTURE_CHANNELS as u64))
747773
.then_some(())
748774
});
749-
wait_for_mapped_signal("captured device PCM", &mut audio, WIDE_POSITIONS);
775+
wait_for_mapped_signal("captured device PCM", &mut audio, WIDE_POSITIONS, &[0, 1]);
750776

751777
assert!(control.configure(CaptureConfig {
752778
mode: CaptureMode::Device,

0 commit comments

Comments
 (0)