use crate::pce::{ElementSelect, Pce};
use oxideav_core::{ChannelLayout, ChannelPosition};
#[must_use]
pub fn layout_for_config(channel_configuration: u8) -> Option<ChannelLayout> {
Some(match channel_configuration {
1 => ChannelLayout::Mono,
2 => ChannelLayout::Stereo,
3 => ChannelLayout::Surround30,
4 => ChannelLayout::Surround40,
5 => ChannelLayout::Surround50,
6 => ChannelLayout::Surround51,
_ => return None,
})
}
#[must_use]
pub fn element_speaker_order(channel_configuration: u8) -> Option<&'static [ChannelPosition]> {
use ChannelPosition::*;
Some(match channel_configuration {
1 => &[FrontCenter],
2 => &[FrontLeft, FrontRight],
3 => &[FrontCenter, FrontLeft, FrontRight],
4 => &[FrontCenter, FrontLeft, FrontRight, BackCenter],
5 => &[FrontCenter, FrontLeft, FrontRight, SideLeft, SideRight],
6 => &[
FrontCenter,
FrontLeft,
FrontRight,
SideLeft,
SideRight,
LowFrequency,
],
7 => &[
FrontCenter,
FrontLeftOfCenter,
FrontRightOfCenter,
FrontLeft,
FrontRight,
SideLeft,
SideRight,
LowFrequency,
],
_ => return None,
})
}
#[must_use]
pub fn reorder_permutation(channel_configuration: u8) -> Option<Vec<usize>> {
let element_order = element_speaker_order(channel_configuration)?;
let mut perm: Vec<usize> = (0..element_order.len()).collect();
let ranks: Vec<usize> = element_order
.iter()
.map(|&p| canonical_rank(p))
.collect::<Option<Vec<usize>>>()?;
perm.sort_by_key(|&i| ranks[i]);
Some(perm)
}
#[must_use]
pub fn reorder_channels<T>(channel_configuration: u8, channels: Vec<Vec<T>>) -> Vec<Vec<T>> {
let Some(perm) = reorder_permutation(channel_configuration) else {
return channels;
};
if perm.len() != channels.len() {
return channels;
}
apply_permutation(&perm, channels)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PceElementKind {
Sce,
Cpe,
Lfe,
}
fn canonical_rank(pos: ChannelPosition) -> Option<usize> {
use ChannelPosition::*;
Some(match pos {
FrontLeft => 0,
FrontRight => 1,
FrontCenter => 2,
LowFrequency => 3,
BackLeft => 4,
BackRight => 5,
FrontLeftOfCenter => 6,
FrontRightOfCenter => 7,
BackCenter => 8,
SideLeft => 9,
SideRight => 10,
_ => return None,
})
}
type PceAssignment = (PceElementKind, u8, Vec<ChannelPosition>);
#[allow(clippy::type_complexity)]
fn pair_up(list: &[ElementSelect], lone_first: bool) -> Option<(Vec<[(bool, u8); 2]>, Option<u8>)> {
let sce_count = list.iter().filter(|e| !e.is_cpe).count();
let mut lone: Option<u8> = None;
let mut expect_lone = sce_count % 2 == 1;
let mut pairs: Vec<[(bool, u8); 2]> = Vec::new();
let mut pending_sce: Option<u8> = None;
let sce_positions: Vec<usize> = (0..list.len()).filter(|&i| !list[i].is_cpe).collect();
let lone_index = if expect_lone {
if lone_first {
sce_positions.first().copied()
} else {
sce_positions.last().copied()
}
} else {
None
};
for (i, e) in list.iter().enumerate() {
if e.is_cpe {
pairs.push([(true, e.tag_select), (true, e.tag_select)]);
} else if expect_lone && Some(i) == lone_index {
lone = Some(e.tag_select);
expect_lone = false;
} else if let Some(left) = pending_sce.take() {
pairs.push([(false, left), (false, e.tag_select)]);
} else {
pending_sce = Some(e.tag_select);
}
}
if pending_sce.is_some() {
return None; }
Some((pairs, lone))
}
fn push_pair(
out: &mut Vec<PceAssignment>,
pair: [(bool, u8); 2],
left: ChannelPosition,
right: ChannelPosition,
) {
let [(l_cpe, l_tag), (r_cpe, r_tag)] = pair;
if l_cpe {
debug_assert!(r_cpe && l_tag == r_tag);
out.push((PceElementKind::Cpe, l_tag, vec![left, right]));
} else {
out.push((PceElementKind::Sce, l_tag, vec![left]));
out.push((PceElementKind::Sce, r_tag, vec![right]));
}
}
pub fn pce_speaker_assignment(pce: &Pce) -> Option<Vec<PceAssignment>> {
use ChannelPosition::*;
let mut out: Vec<PceAssignment> = Vec::new();
let (front_pairs, front_center) = pair_up(&pce.front_elements, true)?;
if let Some(tag) = front_center {
out.push((PceElementKind::Sce, tag, vec![FrontCenter]));
}
match front_pairs.len() {
0 => {}
1 => push_pair(&mut out, front_pairs[0], FrontLeft, FrontRight),
2 => {
push_pair(
&mut out,
front_pairs[0],
FrontLeftOfCenter,
FrontRightOfCenter,
);
push_pair(&mut out, front_pairs[1], FrontLeft, FrontRight);
}
_ => return None,
}
let (side_pairs, side_lone) = pair_up(&pce.side_elements, false)?;
if side_lone.is_some() || side_pairs.len() > 1 {
return None;
}
let have_side = side_pairs.len() == 1;
if have_side {
push_pair(&mut out, side_pairs[0], SideLeft, SideRight);
}
let (back_pairs, back_center) = pair_up(&pce.back_elements, false)?;
match back_pairs.len() {
0 => {}
1 => {
if have_side || back_center.is_some() {
push_pair(&mut out, back_pairs[0], BackLeft, BackRight);
} else {
push_pair(&mut out, back_pairs[0], SideLeft, SideRight);
}
}
2 => {
if have_side {
return None; }
push_pair(&mut out, back_pairs[0], SideLeft, SideRight);
push_pair(&mut out, back_pairs[1], BackLeft, BackRight);
}
_ => return None,
}
if let Some(tag) = back_center {
out.push((PceElementKind::Sce, tag, vec![BackCenter]));
}
match pce.lfe_element_tag_selects.len() {
0 => {}
1 => out.push((
PceElementKind::Lfe,
pce.lfe_element_tag_selects[0],
vec![LowFrequency],
)),
_ => return None, }
let mut seen = [false; 11];
for (_, _, positions) in &out {
for &p in positions {
let r = canonical_rank(p)?;
if seen[r] {
return None;
}
seen[r] = true;
}
}
Some(out)
}
pub fn pce_reorder_permutation(
pce: &Pce,
elements: &[(PceElementKind, u8, usize)],
) -> Option<Vec<usize>> {
let mut assignment = pce_speaker_assignment(pce)?;
let mut ranks: Vec<usize> = Vec::new();
for &(kind, tag, n_ch) in elements {
let idx = assignment
.iter()
.position(|&(k, t, _)| k == kind && t == tag)?;
let (_, _, positions) = assignment.swap_remove(idx);
if positions.len() != n_ch {
return None; }
for p in positions {
ranks.push(canonical_rank(p)?);
}
}
if !assignment.is_empty() {
return None; }
let mut perm: Vec<usize> = (0..ranks.len()).collect();
perm.sort_by_key(|&i| ranks[i]);
Some(perm)
}
#[must_use]
pub fn apply_permutation<T>(perm: &[usize], channels: Vec<Vec<T>>) -> Vec<Vec<T>> {
if perm.len() != channels.len() {
return channels;
}
let mut slots: Vec<Option<Vec<T>>> = channels.into_iter().map(Some).collect();
let mut out = Vec::with_capacity(perm.len());
for &src in perm {
out.push(
slots[src]
.take()
.expect("permutation is a bijection over the channel slots"),
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::ChannelPosition::*;
#[test]
fn mono_and_stereo_are_identity() {
assert_eq!(reorder_permutation(1), Some(vec![0]));
assert_eq!(reorder_permutation(2), Some(vec![0, 1]));
}
#[test]
fn surround30_moves_center_to_third_slot() {
assert_eq!(reorder_permutation(3), Some(vec![1, 2, 0]));
}
#[test]
fn surround40_keeps_back_center_last() {
assert_eq!(reorder_permutation(4), Some(vec![1, 2, 0, 3]));
}
#[test]
fn surround50_orders_front_then_surround() {
assert_eq!(reorder_permutation(5), Some(vec![1, 2, 0, 3, 4]));
}
#[test]
fn surround51_interleaves_lfe_before_surround() {
assert_eq!(reorder_permutation(6), Some(vec![1, 2, 0, 5, 3, 4]));
}
#[test]
fn config_zero_and_reserved_are_unmapped() {
assert_eq!(reorder_permutation(0), None);
assert_eq!(reorder_permutation(8), None);
assert_eq!(reorder_permutation(15), None);
assert_eq!(layout_for_config(0), None);
assert_eq!(layout_for_config(7), None);
}
#[test]
fn config_seven_lands_wave_rank_order() {
assert_eq!(reorder_permutation(7), Some(vec![3, 4, 0, 7, 1, 2, 5, 6]));
}
#[test]
fn permutation_matches_layout_positions() {
for cfg in 1..=6u8 {
let perm = reorder_permutation(cfg).unwrap();
let elem = element_speaker_order(cfg).unwrap();
let layout = layout_for_config(cfg).unwrap();
let canonical = layout.positions();
assert_eq!(perm.len(), canonical.len(), "cfg {cfg} length");
assert_eq!(canonical.len(), elem.len(), "cfg {cfg} element count");
for (out_slot, &src) in perm.iter().enumerate() {
assert_eq!(
elem[src], canonical[out_slot],
"cfg {cfg}: output slot {out_slot} mismatched speaker"
);
}
}
}
#[test]
fn layout_channel_counts_agree_with_element_order() {
for cfg in 1..=6u8 {
let layout = layout_for_config(cfg).unwrap();
let elem = element_speaker_order(cfg).unwrap();
assert_eq!(
usize::from(layout.channel_count()),
elem.len(),
"cfg {cfg} channel count"
);
}
}
#[test]
fn reorder_channels_permutes_buffers() {
let channels: Vec<Vec<i16>> = vec![
vec![0], vec![1], vec![2], vec![3], vec![4], vec![5], ];
let out = reorder_channels(6, channels);
let got: Vec<i16> = out.iter().map(|c| c[0]).collect();
assert_eq!(got, vec![1, 2, 0, 5, 3, 4]);
}
#[test]
fn reorder_channels_passthrough_on_unmapped_config() {
let channels: Vec<Vec<i16>> = vec![vec![9], vec![8]];
let out = reorder_channels(0, channels.clone());
assert_eq!(out, channels);
}
#[test]
fn reorder_channels_passthrough_on_count_mismatch() {
let channels: Vec<Vec<i16>> = vec![vec![0], vec![1], vec![2], vec![3]];
let out = reorder_channels(6, channels.clone());
assert_eq!(out, channels);
}
fn sce(tag: u8) -> ElementSelect {
ElementSelect {
is_cpe: false,
tag_select: tag,
}
}
fn cpe(tag: u8) -> ElementSelect {
ElementSelect {
is_cpe: true,
tag_select: tag,
}
}
fn pce_with(
front: Vec<ElementSelect>,
side: Vec<ElementSelect>,
back: Vec<ElementSelect>,
lfe: Vec<u8>,
) -> Pce {
Pce {
element_instance_tag: 0,
object_type: 1,
sampling_frequency_index: 3,
front_elements: front,
side_elements: side,
back_elements: back,
lfe_element_tag_selects: lfe,
assoc_data_tag_selects: vec![],
valid_cc_elements: vec![],
mono_mixdown_element_number: None,
stereo_mixdown_element_number: None,
matrix_mixdown: None,
comment_field: vec![],
}
}
#[test]
fn pce_5_1_matches_config_6_order() {
let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![cpe(1)], vec![0]);
use PceElementKind::*;
let perm =
pce_reorder_permutation(&pce, &[(Sce, 0, 1), (Cpe, 0, 2), (Cpe, 1, 2), (Lfe, 0, 1)])
.expect("5.1 PCE maps");
assert_eq!(perm, vec![1, 2, 0, 5, 3, 4]);
}
#[test]
fn pce_7_1_two_back_pairs_outside_in() {
let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![cpe(1), cpe(2)], vec![0]);
use PceElementKind::*;
let perm = pce_reorder_permutation(
&pce,
&[
(Sce, 0, 1),
(Cpe, 0, 2),
(Cpe, 1, 2),
(Cpe, 2, 2),
(Lfe, 0, 1),
],
)
.expect("7.1 PCE maps");
assert_eq!(perm, vec![1, 2, 0, 7, 5, 6, 3, 4]);
}
#[test]
fn pce_hexagonal_lone_sces_are_centers() {
let pce = pce_with(vec![cpe(0), sce(0)], vec![], vec![cpe(1), sce(1)], vec![]);
use PceElementKind::*;
let perm =
pce_reorder_permutation(&pce, &[(Cpe, 0, 2), (Sce, 0, 1), (Cpe, 1, 2), (Sce, 1, 1)])
.expect("hexagonal PCE maps");
assert_eq!(perm, vec![0, 1, 2, 3, 4, 5], "already canonical order");
let perm =
pce_reorder_permutation(&pce, &[(Sce, 1, 1), (Sce, 0, 1), (Cpe, 1, 2), (Cpe, 0, 2)])
.unwrap();
assert_eq!(perm, vec![4, 5, 1, 2, 3, 0]);
}
#[test]
fn pce_sce_pair_forms_lr() {
let pce = pce_with(vec![sce(0), sce(1)], vec![], vec![], vec![]);
let assign = pce_speaker_assignment(&pce).unwrap();
assert_eq!(
assign,
vec![
(PceElementKind::Sce, 0, vec![FrontLeft]),
(PceElementKind::Sce, 1, vec![FrontRight]),
]
);
}
#[test]
fn pce_side_pair_moves_single_back_pair_to_rear() {
let pce = pce_with(vec![sce(0), cpe(0)], vec![cpe(1)], vec![cpe(2)], vec![]);
let assign = pce_speaker_assignment(&pce).unwrap();
let find = |tag: u8| {
assign
.iter()
.find(|&&(k, t, _)| k == PceElementKind::Cpe && t == tag)
.map(|(_, _, p)| p.clone())
.unwrap()
};
assert_eq!(find(1), vec![SideLeft, SideRight]);
assert_eq!(find(2), vec![BackLeft, BackRight]);
}
#[test]
fn pce_unmappable_layouts_fall_back() {
let pce = pce_with(vec![cpe(0), cpe(1), cpe(2)], vec![], vec![], vec![]);
assert!(pce_speaker_assignment(&pce).is_none());
let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![], vec![0, 1]);
assert!(pce_speaker_assignment(&pce).is_none());
}
#[test]
fn pce_permutation_rejects_mismatches() {
use PceElementKind::*;
let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![], vec![]);
assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 2), (Cpe, 0, 2)]).is_none());
assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 1), (Cpe, 0, 2), (Cpe, 5, 2)]).is_none());
assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 1)]).is_none());
}
#[test]
fn every_speaker_in_canonical_appears_in_element_order() {
for cfg in 1..=6u8 {
let elem = element_speaker_order(cfg).unwrap();
let layout = layout_for_config(cfg).unwrap();
for &pos in layout.positions() {
assert!(
elem.contains(&pos),
"cfg {cfg}: canonical speaker {pos:?} missing from element order"
);
}
}
let elem5 = element_speaker_order(5).unwrap();
assert!(!elem5.contains(&LowFrequency));
}
}