#![allow(dead_code)]
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GroupType {
StereoPair,
Surround51,
Surround71,
MultiCam,
AvLink,
Custom,
}
impl GroupType {
#[must_use]
pub const fn expected_channel_count(self) -> Option<usize> {
match self {
Self::StereoPair => Some(2),
Self::Surround51 => Some(6),
Self::Surround71 => Some(8),
_ => None,
}
}
#[must_use]
pub const fn has_fixed_size(self) -> bool {
matches!(self, Self::StereoPair | Self::Surround51 | Self::Surround71)
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::StereoPair => "Stereo",
Self::Surround51 => "5.1 Surround",
Self::Surround71 => "7.1 Surround",
Self::MultiCam => "Multi-Camera",
Self::AvLink => "A/V Link",
Self::Custom => "Custom",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChannelRole {
Left,
Right,
Center,
Lfe,
LeftSurround,
RightSurround,
LeftBack,
RightBack,
Mono,
PrimaryVideo,
CameraAngle(u32),
Unassigned,
}
impl ChannelRole {
#[must_use]
pub fn surround_51_layout() -> Vec<Self> {
vec![
Self::Left,
Self::Right,
Self::Center,
Self::Lfe,
Self::LeftSurround,
Self::RightSurround,
]
}
#[must_use]
pub fn surround_71_layout() -> Vec<Self> {
vec![
Self::Left,
Self::Right,
Self::Center,
Self::Lfe,
Self::LeftSurround,
Self::RightSurround,
Self::LeftBack,
Self::RightBack,
]
}
#[must_use]
pub const fn is_surround(self) -> bool {
matches!(
self,
Self::LeftSurround | Self::RightSurround | Self::LeftBack | Self::RightBack
)
}
}
#[derive(Debug, Clone)]
pub struct GroupMember {
pub track_id: u32,
pub mob_id: Uuid,
pub role: ChannelRole,
pub display_name: Option<String>,
pub solo: bool,
pub muted: bool,
}
impl GroupMember {
#[must_use]
pub fn new(track_id: u32, mob_id: Uuid, role: ChannelRole) -> Self {
Self {
track_id,
mob_id,
role,
display_name: None,
solo: false,
muted: false,
}
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.display_name = Some(name.into());
self
}
}
#[derive(Debug, Clone)]
pub struct TrackGroup {
pub group_id: Uuid,
pub name: String,
pub group_type: GroupType,
members: Vec<GroupMember>,
pub gang_editing: bool,
pub color_tag: Option<u32>,
}
impl TrackGroup {
#[must_use]
pub fn new(name: impl Into<String>, group_type: GroupType) -> Self {
Self {
group_id: Uuid::new_v4(),
name: name.into(),
group_type,
members: Vec::new(),
gang_editing: true,
color_tag: None,
}
}
#[must_use]
pub fn stereo_pair(
name: impl Into<String>,
left_track: u32,
right_track: u32,
mob_id: Uuid,
) -> Self {
let mut group = Self::new(name, GroupType::StereoPair);
group
.members
.push(GroupMember::new(left_track, mob_id, ChannelRole::Left));
group
.members
.push(GroupMember::new(right_track, mob_id, ChannelRole::Right));
group
}
pub fn add_member(&mut self, member: GroupMember) {
self.members.push(member);
}
pub fn remove_member(&mut self, track_id: u32) -> bool {
let before = self.members.len();
self.members.retain(|m| m.track_id != track_id);
self.members.len() < before
}
#[must_use]
pub fn members(&self) -> &[GroupMember] {
&self.members
}
#[must_use]
pub fn member_count(&self) -> usize {
self.members.len()
}
#[must_use]
pub fn find_member(&self, track_id: u32) -> Option<&GroupMember> {
self.members.iter().find(|m| m.track_id == track_id)
}
#[must_use]
pub fn is_complete(&self) -> bool {
match self.group_type.expected_channel_count() {
Some(expected) => self.members.len() == expected,
None => !self.members.is_empty(),
}
}
#[must_use]
pub fn validate(&self) -> Vec<String> {
let mut issues = Vec::new();
if self.members.is_empty() {
issues.push("Group has no members".to_string());
}
if let Some(expected) = self.group_type.expected_channel_count() {
if self.members.len() != expected {
issues.push(format!(
"Expected {} members for {}, got {}",
expected,
self.group_type.label(),
self.members.len()
));
}
}
let mut seen = std::collections::HashSet::new();
for member in &self.members {
if !seen.insert(member.track_id) {
issues.push(format!("Duplicate track ID: {}", member.track_id));
}
}
issues
}
pub fn mute_all(&mut self) {
for member in &mut self.members {
member.muted = true;
}
}
pub fn unmute_all(&mut self) {
for member in &mut self.members {
member.muted = false;
}
}
pub fn solo_member(&mut self, track_id: u32) {
for member in &mut self.members {
member.solo = member.track_id == track_id;
}
}
#[must_use]
pub fn muted_tracks(&self) -> Vec<u32> {
self.members
.iter()
.filter(|m| m.muted)
.map(|m| m.track_id)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct TrackGroupRegistry {
groups: HashMap<Uuid, TrackGroup>,
track_index: HashMap<u32, Vec<Uuid>>,
}
impl TrackGroupRegistry {
#[must_use]
pub fn new() -> Self {
Self {
groups: HashMap::new(),
track_index: HashMap::new(),
}
}
pub fn register(&mut self, group: TrackGroup) {
let gid = group.group_id;
for member in group.members() {
self.track_index
.entry(member.track_id)
.or_default()
.push(gid);
}
self.groups.insert(gid, group);
}
pub fn remove(&mut self, group_id: &Uuid) -> Option<TrackGroup> {
if let Some(group) = self.groups.remove(group_id) {
for member in group.members() {
if let Some(ids) = self.track_index.get_mut(&member.track_id) {
ids.retain(|id| id != group_id);
}
}
Some(group)
} else {
None
}
}
#[must_use]
pub fn get(&self, group_id: &Uuid) -> Option<&TrackGroup> {
self.groups.get(group_id)
}
#[must_use]
pub fn groups_for_track(&self, track_id: u32) -> Vec<&TrackGroup> {
self.track_index
.get(&track_id)
.map(|ids| ids.iter().filter_map(|id| self.groups.get(id)).collect())
.unwrap_or_default()
}
#[must_use]
pub fn group_count(&self) -> usize {
self.groups.len()
}
#[must_use]
pub fn all_groups(&self) -> Vec<&TrackGroup> {
self.groups.values().collect()
}
}
impl Default for TrackGroupRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_group_type_channel_count() {
assert_eq!(GroupType::StereoPair.expected_channel_count(), Some(2));
assert_eq!(GroupType::Surround51.expected_channel_count(), Some(6));
assert_eq!(GroupType::Surround71.expected_channel_count(), Some(8));
assert_eq!(GroupType::MultiCam.expected_channel_count(), None);
assert_eq!(GroupType::Custom.expected_channel_count(), None);
}
#[test]
fn test_group_type_has_fixed_size() {
assert!(GroupType::StereoPair.has_fixed_size());
assert!(GroupType::Surround51.has_fixed_size());
assert!(!GroupType::MultiCam.has_fixed_size());
assert!(!GroupType::Custom.has_fixed_size());
}
#[test]
fn test_group_type_label() {
assert_eq!(GroupType::StereoPair.label(), "Stereo");
assert_eq!(GroupType::Surround51.label(), "5.1 Surround");
assert_eq!(GroupType::Surround71.label(), "7.1 Surround");
assert_eq!(GroupType::MultiCam.label(), "Multi-Camera");
}
#[test]
fn test_channel_role_surround_layouts() {
assert_eq!(ChannelRole::surround_51_layout().len(), 6);
assert_eq!(ChannelRole::surround_71_layout().len(), 8);
}
#[test]
fn test_channel_role_is_surround() {
assert!(ChannelRole::LeftSurround.is_surround());
assert!(ChannelRole::RightSurround.is_surround());
assert!(ChannelRole::LeftBack.is_surround());
assert!(!ChannelRole::Left.is_surround());
assert!(!ChannelRole::Center.is_surround());
}
#[test]
fn test_group_member_creation() {
let mob_id = Uuid::new_v4();
let member = GroupMember::new(1, mob_id, ChannelRole::Left).with_name("Left Speaker");
assert_eq!(member.track_id, 1);
assert_eq!(member.role, ChannelRole::Left);
assert_eq!(member.display_name.as_deref(), Some("Left Speaker"));
assert!(!member.solo);
assert!(!member.muted);
}
#[test]
fn test_stereo_pair_creation() {
let mob_id = Uuid::new_v4();
let group = TrackGroup::stereo_pair("Dialogue", 1, 2, mob_id);
assert_eq!(group.group_type, GroupType::StereoPair);
assert_eq!(group.member_count(), 2);
assert!(group.is_complete());
assert!(group.validate().is_empty());
}
#[test]
fn test_track_group_add_remove() {
let mob_id = Uuid::new_v4();
let mut group = TrackGroup::new("Custom Group", GroupType::Custom);
group.add_member(GroupMember::new(1, mob_id, ChannelRole::Mono));
group.add_member(GroupMember::new(2, mob_id, ChannelRole::Mono));
assert_eq!(group.member_count(), 2);
assert!(group.remove_member(1));
assert_eq!(group.member_count(), 1);
assert!(!group.remove_member(99));
}
#[test]
fn test_track_group_find_member() {
let mob_id = Uuid::new_v4();
let group = TrackGroup::stereo_pair("Stereo", 1, 2, mob_id);
let left = group.find_member(1).expect("left should be valid");
assert_eq!(left.role, ChannelRole::Left);
assert!(group.find_member(99).is_none());
}
#[test]
fn test_track_group_validate_incomplete() {
let mob_id = Uuid::new_v4();
let mut group = TrackGroup::new("Bad 5.1", GroupType::Surround51);
group.add_member(GroupMember::new(1, mob_id, ChannelRole::Left));
let issues = group.validate();
assert!(!issues.is_empty());
assert!(!group.is_complete());
}
#[test]
fn test_track_group_mute_unmute() {
let mob_id = Uuid::new_v4();
let mut group = TrackGroup::stereo_pair("Stereo", 1, 2, mob_id);
group.mute_all();
assert_eq!(group.muted_tracks().len(), 2);
group.unmute_all();
assert_eq!(group.muted_tracks().len(), 0);
}
#[test]
fn test_track_group_solo() {
let mob_id = Uuid::new_v4();
let mut group = TrackGroup::stereo_pair("Stereo", 1, 2, mob_id);
group.solo_member(1);
let members = group.members();
assert!(members[0].solo);
assert!(!members[1].solo);
}
#[test]
fn test_registry_register_and_get() {
let mut registry = TrackGroupRegistry::new();
let mob_id = Uuid::new_v4();
let group = TrackGroup::stereo_pair("Stereo", 1, 2, mob_id);
let gid = group.group_id;
registry.register(group);
assert_eq!(registry.group_count(), 1);
assert!(registry.get(&gid).is_some());
}
#[test]
fn test_registry_groups_for_track() {
let mut registry = TrackGroupRegistry::new();
let mob_id = Uuid::new_v4();
let group = TrackGroup::stereo_pair("Stereo", 1, 2, mob_id);
registry.register(group);
let groups = registry.groups_for_track(1);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].group_type, GroupType::StereoPair);
let empty = registry.groups_for_track(99);
assert!(empty.is_empty());
}
#[test]
fn test_registry_remove() {
let mut registry = TrackGroupRegistry::new();
let mob_id = Uuid::new_v4();
let group = TrackGroup::stereo_pair("Stereo", 1, 2, mob_id);
let gid = group.group_id;
registry.register(group);
assert_eq!(registry.group_count(), 1);
let removed = registry.remove(&gid);
assert!(removed.is_some());
assert_eq!(registry.group_count(), 0);
}
#[test]
fn test_validate_duplicate_tracks() {
let mob_id = Uuid::new_v4();
let mut group = TrackGroup::new("Bad Group", GroupType::Custom);
group.add_member(GroupMember::new(1, mob_id, ChannelRole::Left));
group.add_member(GroupMember::new(1, mob_id, ChannelRole::Right));
let issues = group.validate();
assert!(issues.iter().any(|i| i.contains("Duplicate")));
}
}