use std::path::Path;
use crate::diagnostics::{Location, ValidationIssue};
use crate::mxf::codes::{St2067_2_2016, St377_4_2012};
pub fn check_audio_mca(regxml: &str, path: &Path) -> Vec<ValidationIssue> {
let mut issues = Vec::new();
let has_audio_sample_rate = regxml.contains("AudioSampleRate");
let has_wave_pcm = regxml.contains("WAVEPCMDescriptor");
if has_audio_sample_rate && !has_wave_pcm {
issues.push(
ValidationIssue::from_code(St2067_2_2016::SoundDescriptorNotWAVEPCM,
format!(
"MXF {} carries audio essence but its descriptor is not a WAVEPCMDescriptor — ST 2067-2 §5.3.4.1 requires WAVE PCM",
path.display()
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
if !has_wave_pcm {
return issues;
}
if let Some(rate) = extract_field(regxml, "AudioSampleRate") {
if !is_acceptable_audio_rate(&rate) {
issues.push(
ValidationIssue::from_code(St2067_2_2016::AudioSampleRateUnsupported,
format!(
"MXF {} declares AudioSampleRate = {} — ST 2067-2 §5.3.2.2 requires 48000 Hz or 96000 Hz",
path.display(),
rate
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
if let Some(qb) = extract_field(regxml, "QuantizationBits") {
if qb.trim() != "24" {
issues.push(
ValidationIssue::from_code(St2067_2_2016::QuantizationBitsNot24,
format!(
"MXF {} declares QuantizationBits = {} — ST 2067-2 §5.3.2.3 requires 24-bit audio",
path.display(),
qb
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
let channel_count =
extract_field(regxml, "ChannelCount").and_then(|c| c.trim().parse::<u32>().ok());
let channel_labels = count_elements(regxml, "AudioChannelLabelSubDescriptor");
if let Some(cc) = channel_count {
if (channel_labels as u32) != cc {
issues.push(
ValidationIssue::from_code(St2067_2_2016::ChannelLabelCountMismatch,
format!(
"MXF {} declares ChannelCount = {} but carries {} AudioChannelLabelSubDescriptor(s) — \
ST 2067-2 §5.3.6.2 requires one label per channel",
path.display(),
cc,
channel_labels,
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
let soundfield_count = count_elements(regxml, "SoundfieldGroupLabelSubDescriptor");
if soundfield_count != 1 {
issues.push(
ValidationIssue::from_code(St2067_2_2016::SoundFieldGroupLabelCount,
format!(
"MXF {} carries {} SoundfieldGroupLabelSubDescriptor(s) — ST 2067-2 §5.3.6.3 requires exactly one",
path.display(),
soundfield_count,
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
let mca_link_count = count_elements(regxml, "MCALinkID");
let expected_link_count = channel_labels + soundfield_count;
if mca_link_count < expected_link_count {
issues.push(
ValidationIssue::from_code(
St377_4_2012::MCALinkIDMissing,
format!(
"MXF {} carries {} MCALinkID(s) but expected {} ({} channel-label + {} \
soundfield-group). ST 377-4 §6.3.2 requires every MCA sub-descriptor to \
carry an MCALinkID.",
path.display(),
mca_link_count,
expected_link_count,
channel_labels,
soundfield_count,
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
if let Some(sg_link) = extract_field(regxml, "MCALinkID") {
for sf_link in extract_all_fields(regxml, "SoundfieldGroupLinkID") {
if sf_link.trim() != sg_link.trim() {
issues.push(
ValidationIssue::from_code(
St377_4_2012::SoundfieldGroupLinkIDMismatch,
format!(
"MXF {} carries an AudioChannelLabelSubDescriptor with \
SoundfieldGroupLinkID '{}' that doesn't match the SoundfieldGroup \
MCALinkID '{}' (ST 377-4 §6.3.2).",
path.display(),
sf_link.trim(),
sg_link.trim(),
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
break; }
}
}
if let Some(cc) = channel_count {
let channel_ids: std::collections::HashSet<u32> =
extract_all_fields(regxml, "MCAChannelID")
.into_iter()
.filter_map(|s| s.trim().parse::<u32>().ok())
.collect();
for expected in 1..=cc {
if !channel_ids.contains(&expected) {
issues.push(
ValidationIssue::from_code(
St2067_2_2016::MCAChannelIDMissing,
format!(
"MXF {} declares ChannelCount = {} but no \
AudioChannelLabelSubDescriptor carries MCAChannelID = {} — \
every channel 1..N must have a label per ST 2067-2 §5.3.6.2.",
path.display(),
cc,
expected,
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
}
if let Some(ca) = extract_field(regxml, "ChannelAssignment") {
let ca = ca.trim();
let prefix_ok =
ca.starts_with("urn:smpte:ul:060e2b34.0401010") && ca.contains(".04020210.");
if !prefix_ok {
issues.push(
ValidationIssue::from_code(
St2067_2_2016::ChannelAssignmentNotMCA,
format!(
"MXF {} declares ChannelAssignment = {} — ST 2067-2 §5.3.4.2 \
requires a SMPTE 428-12 MCA channel-layout UL.",
path.display(),
ca
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
if let Some(cf) = extract_field(regxml, "ContainerFormat") {
if let Some(bytes) = parse_ul_bytes(&cf) {
if bytes[14] != 0x02 {
issues.push(
ValidationIssue::from_code(
St2067_2_2016::AudioNotClipWrapped,
format!(
"MXF {} audio ContainerFormat UL byte 15 = 0x{:02x} \
— ST 2067-2 §5.3.3 / ST 382 §10 require Clip-Wrapped (0x02). \
ContainerFormat = {}",
path.display(),
bytes[14],
cf.trim(),
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
}
if !regxml.contains(":RFC5646SpokenLanguage") {
issues.push(
ValidationIssue::from_code(
St2067_2_2016::RFC5646SpokenLanguageMissing,
format!(
"MXF {} sound descriptor is missing RFC5646SpokenLanguage — ST 2067-2 \
§5.3 recommends declaring the spoken-language BCP-47 tag.",
path.display(),
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
if soundfield_count > 0 {
for (required, code) in &[
("MCATitle", St2067_2_2016::SoundfieldGroupMissingMCATitle),
(
"MCATitleVersion",
St2067_2_2016::SoundfieldGroupMissingMCATitleVersion,
),
(
"MCAAudioContentKind",
St2067_2_2016::SoundfieldGroupMissingMCAAudioContentKind,
),
(
"MCAAudioElementKind",
St2067_2_2016::SoundfieldGroupMissingMCAAudioElementKind,
),
] {
if !regxml.contains(&format!(":{required}")) {
issues.push(
ValidationIssue::from_code(
*code,
format!(
"MXF {} SoundfieldGroupLabelSubDescriptor is missing {} — \
ST 2067-2 §5.3.6.5 recommends this field for delivery-grade \
audio MCA.",
path.display(),
required,
),
)
.with_location(Location::new().with_file(path.to_path_buf())),
);
}
}
}
issues
}
pub(crate) fn extract_all_fields(xml: &str, local_name: &str) -> Vec<String> {
let mut out = Vec::new();
let probe = format!(":{local_name}");
let close_form = format!(":{local_name}>");
let mut cursor = 0;
while let Some(rel) = xml[cursor..].find(&probe) {
let abs = cursor + rel;
let next = xml.as_bytes().get(abs + probe.len()).copied();
if !matches!(
next,
Some(b'>') | Some(b' ') | Some(b'/') | Some(b'\t') | Some(b'\n')
) {
cursor = abs + probe.len();
continue;
}
let Some(tag_start) = xml[..abs].rfind('<') else {
break;
};
let is_close = xml[tag_start..].starts_with("</");
if is_close {
cursor = abs + close_form.len();
continue;
}
let Some(open_end_rel) = xml[abs..].find('>') else {
break;
};
let body_start = abs + open_end_rel + 1;
let Some(close_rel) = xml[body_start..].find(&close_form) else {
break;
};
let close_abs = body_start + close_rel;
let Some(close_tag_start) = xml[..close_abs].rfind('<') else {
break;
};
if !xml[close_tag_start..].starts_with("</") {
cursor = close_abs + close_form.len();
continue;
}
let body = xml[body_start..close_tag_start].trim();
out.push(body.to_string());
cursor = close_abs + close_form.len();
}
out
}
pub(crate) fn extract_field(xml: &str, local_name: &str) -> Option<String> {
extract_all_fields(xml, local_name).into_iter().next()
}
pub(crate) fn count_elements(xml: &str, local_name: &str) -> usize {
let open_token = format!(":{local_name}");
let mut count = 0;
let mut search_from = 0;
while let Some(rel) = xml[search_from..].find(&open_token) {
let abs = search_from + rel;
let next_char = xml.as_bytes().get(abs + open_token.len()).copied();
if matches!(
next_char,
Some(b'>') | Some(b' ') | Some(b'/') | Some(b'\t') | Some(b'\n')
) {
if let Some(prefix_start) = xml[..abs].rfind('<') {
if !xml[prefix_start..].starts_with("</") {
count += 1;
}
}
}
search_from = abs + open_token.len();
}
count
}
pub(crate) fn parse_ul_bytes(urn: &str) -> Option<[u8; 16]> {
let body = urn.trim().strip_prefix("urn:smpte:ul:")?;
let hex: String = body.chars().filter(|c| *c != '.').collect();
if hex.len() != 32 {
return None;
}
let mut out = [0u8; 16];
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let hi = (chunk[0] as char).to_digit(16)?;
let lo = (chunk[1] as char).to_digit(16)?;
out[i] = ((hi as u8) << 4) | (lo as u8);
}
Some(out)
}
fn is_acceptable_audio_rate(rate_text: &str) -> bool {
let parts: Vec<&str> = rate_text.trim().split('/').collect();
let (num, den) = match parts.as_slice() {
[n, d] => (n.trim().parse::<i64>().ok(), d.trim().parse::<i64>().ok()),
[n] => (n.trim().parse::<i64>().ok(), Some(1)),
_ => return false,
};
let (Some(n), Some(d)) = (num, den) else {
return false;
};
if d == 0 {
return false;
}
let hz = n / d;
matches!(hz, 48_000 | 96_000)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::Severity;
use crate::mxf::metadata::parse_mxf_to_regxml;
use std::path::PathBuf;
fn fixture(name: &str) -> PathBuf {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest.join("tests/fixtures/mxf").join(name)
}
fn audio1_regxml() -> (String, PathBuf) {
let path = fixture("audio1.mxf");
let opts = regxml::MxfFragmentOptions {
partition: regxml::PartitionTarget::Header,
..Default::default()
};
let xml = parse_mxf_to_regxml(&path, opts).expect("audio1 → RegXML");
(xml, path)
}
#[test]
fn audio1_clean_fixture_passes_all_error_level_audio_mca_checks() {
let (xml, path) = audio1_regxml();
let issues = check_audio_mca(&xml, &path);
let errors: Vec<_> = issues
.iter()
.filter(|i| i.severity == Severity::Error || i.severity == Severity::Critical)
.collect();
assert!(
errors.is_empty(),
"audio1.mxf should pass all Error-level §5.3 checks. Got Errors: {:#?}",
errors
);
}
#[test]
fn audio1_fires_warnings_for_missing_netflix_grade_mca_fields() {
let (xml, path) = audio1_regxml();
let issues = check_audio_mca(&xml, &path);
for field in &[
"MCATitle",
"MCATitleVersion",
"MCAAudioContentKind",
"MCAAudioElementKind",
] {
assert!(
issues
.iter()
.any(|i| i.code.contains(&format!("SoundfieldGroupMissing/{field}"))),
"expected SoundfieldGroupMissing/{field} warning on audio1.mxf, got: {:#?}",
issues
);
}
}
#[test]
fn extract_all_fields_returns_every_occurrence() {
let xml = r#"
<ns1:MCAChannelID>1</ns1:MCAChannelID>
<ns1:MCAChannelID>2</ns1:MCAChannelID>
<ns1:MCAChannelID>3</ns1:MCAChannelID>
"#;
let vs = extract_all_fields(xml, "MCAChannelID");
assert_eq!(vs, vec!["1", "2", "3"]);
}
#[test]
fn flags_soundfield_group_link_id_mismatch() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>48000/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>24</ns2:QuantizationBits>
<ns2:ChannelCount>1</ns2:ChannelCount>
<ns1:SoundfieldGroupLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
</ns1:SoundfieldGroupLabelSubDescriptor>
<ns1:AudioChannelLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:99999999-0000-0000-0000-000000000099</ns2:MCALinkID>
<ns2:MCAChannelID>1</ns2:MCAChannelID>
<ns2:SoundfieldGroupLinkID>urn:uuid:99999999-0000-0000-0000-000000000099</ns2:SoundfieldGroupLinkID>
</ns1:AudioChannelLabelSubDescriptor>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("SoundfieldGroupLinkIDMismatch")),
"expected SoundfieldGroupLinkIDMismatch, got: {:#?}",
issues
);
}
#[test]
fn flags_missing_mca_channel_id_in_range() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>48000/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>24</ns2:QuantizationBits>
<ns2:ChannelCount>3</ns2:ChannelCount>
<ns1:SoundfieldGroupLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
</ns1:SoundfieldGroupLabelSubDescriptor>
<ns1:AudioChannelLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
<ns2:MCAChannelID>1</ns2:MCAChannelID>
<ns2:SoundfieldGroupLinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:SoundfieldGroupLinkID>
</ns1:AudioChannelLabelSubDescriptor>
<ns1:AudioChannelLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
<ns2:MCAChannelID>3</ns2:MCAChannelID>
<ns2:SoundfieldGroupLinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:SoundfieldGroupLinkID>
</ns1:AudioChannelLabelSubDescriptor>
<ns1:AudioChannelLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
<ns2:MCAChannelID>3</ns2:MCAChannelID>
<ns2:SoundfieldGroupLinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:SoundfieldGroupLinkID>
</ns1:AudioChannelLabelSubDescriptor>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
let missing_ids: Vec<_> = issues
.iter()
.filter(|i| i.code.contains("MCAChannelIDMissing"))
.collect();
assert!(
!missing_ids.is_empty(),
"expected MCAChannelIDMissing for id 2, got: {:#?}",
issues
);
assert!(
missing_ids
.iter()
.any(|i| i.message.contains("MCAChannelID = 2")),
"expected diagnostic to name channel id 2, got: {:#?}",
missing_ids
);
}
#[test]
fn extract_field_handles_namespaced_tags() {
let xml = r#"
<ns2:ChannelCount xmlns:ns2="x">2</ns2:ChannelCount>
<ns2:QuantizationBits xmlns:ns2="x">24</ns2:QuantizationBits>
"#;
assert_eq!(extract_field(xml, "ChannelCount").as_deref(), Some("2"));
assert_eq!(
extract_field(xml, "QuantizationBits").as_deref(),
Some("24")
);
assert_eq!(extract_field(xml, "AbsentField"), None);
}
#[test]
fn count_elements_respects_local_name_boundary() {
let xml = r#"
<ns1:AudioChannelLabelSubDescriptor/>
<ns1:AudioChannelLabelSubDescriptor></ns1:AudioChannelLabelSubDescriptor>
<ns1:ChannelCount>2</ns1:ChannelCount>
"#;
assert_eq!(count_elements(xml, "AudioChannelLabelSubDescriptor"), 2);
assert_eq!(count_elements(xml, "ChannelCount"), 1);
assert_eq!(count_elements(xml, "Channel"), 0);
}
#[test]
fn extract_field_handles_open_tag_with_attribute() {
let xml = r#"<ns2:ChannelCount xmlns:ns2="http://example/ns">2</ns2:ChannelCount>"#;
assert_eq!(extract_field(xml, "ChannelCount").as_deref(), Some("2"));
}
#[test]
fn extract_field_skips_self_closing_form() {
let xml = r#"<ns1:SoundfieldGroupLabelSubDescriptor/>
<ns1:ChannelCount>5</ns1:ChannelCount>"#;
assert_eq!(
extract_field(xml, "SoundfieldGroupLabelSubDescriptor"),
None
);
assert_eq!(extract_field(xml, "ChannelCount").as_deref(), Some("5"));
assert_eq!(count_elements(xml, "SoundfieldGroupLabelSubDescriptor"), 1);
}
#[test]
fn extract_field_trims_whitespace() {
let xml = "<ns1:ChannelCount> 42\n </ns1:ChannelCount>";
assert_eq!(extract_field(xml, "ChannelCount").as_deref(), Some("42"));
}
#[test]
fn extract_all_fields_does_not_concatenate_siblings() {
let xml = r#"
<ns1:MCAChannelID>1</ns1:MCAChannelID>
<ns1:MCAChannelID>2</ns1:MCAChannelID>
<ns1:MCAChannelID>3</ns1:MCAChannelID>
"#;
assert_eq!(
extract_all_fields(xml, "MCAChannelID"),
vec!["1".to_string(), "2".to_string(), "3".to_string()]
);
}
#[test]
fn extract_field_does_not_collide_on_prefix() {
let xml = r#"<ns1:Channel>X</ns1:Channel>
<ns1:ChannelCount>2</ns1:ChannelCount>"#;
assert_eq!(extract_field(xml, "Channel").as_deref(), Some("X"));
assert_eq!(extract_field(xml, "ChannelCount").as_deref(), Some("2"));
}
#[test]
fn flags_quantization_other_than_24() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>48000/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>16</ns2:QuantizationBits>
<ns2:ChannelCount>1</ns2:ChannelCount>
<ns1:SoundfieldGroupLabelSubDescriptor/>
<ns1:AudioChannelLabelSubDescriptor/>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("QuantizationBitsNot24")),
"expected QuantizationBitsNot24, got: {:#?}",
issues
);
}
#[test]
fn flags_unsupported_sample_rate() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>44100/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>24</ns2:QuantizationBits>
<ns2:ChannelCount>1</ns2:ChannelCount>
<ns1:SoundfieldGroupLabelSubDescriptor/>
<ns1:AudioChannelLabelSubDescriptor/>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("AudioSampleRateUnsupported")),
"expected AudioSampleRateUnsupported, got: {:#?}",
issues
);
}
#[test]
fn flags_channel_label_count_mismatch() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>48000/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>24</ns2:QuantizationBits>
<ns2:ChannelCount>2</ns2:ChannelCount>
<ns1:SoundfieldGroupLabelSubDescriptor/>
<ns1:AudioChannelLabelSubDescriptor/>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("ChannelLabelCountMismatch")),
"expected ChannelLabelCountMismatch, got: {:#?}",
issues
);
}
#[test]
fn flags_soundfield_group_label_count_not_one() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>48000/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>24</ns2:QuantizationBits>
<ns2:ChannelCount>2</ns2:ChannelCount>
<ns1:SoundfieldGroupLabelSubDescriptor/>
<ns1:SoundfieldGroupLabelSubDescriptor/>
<ns1:AudioChannelLabelSubDescriptor/>
<ns1:AudioChannelLabelSubDescriptor/>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("SoundFieldGroupLabelCount")),
"expected SoundFieldGroupLabelCount, got: {:#?}",
issues
);
}
#[test]
fn flags_missing_mca_link_ids() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>48000/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>24</ns2:QuantizationBits>
<ns2:ChannelCount>2</ns2:ChannelCount>
<ns1:SoundfieldGroupLabelSubDescriptor/>
<ns1:AudioChannelLabelSubDescriptor/>
<ns1:AudioChannelLabelSubDescriptor/>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues.iter().any(|i| i.code.contains("MCALinkIDMissing")),
"expected MCALinkIDMissing, got: {:#?}",
issues
);
}
#[test]
fn parse_ul_bytes_decodes_canonical_urn() {
let ul = "urn:smpte:ul:060e2b34.04010101.0d010301.02060200";
let bytes = parse_ul_bytes(ul).unwrap();
assert_eq!(bytes[0], 0x06);
assert_eq!(
bytes[14], 0x02,
"byte 15 (1-indexed) is the wrapping octet — 0x02 = clip"
);
assert_eq!(bytes[15], 0x00);
}
#[test]
fn parse_ul_bytes_rejects_malformed_input() {
assert!(parse_ul_bytes("not-a-urn").is_none());
assert!(parse_ul_bytes("urn:smpte:ul:short").is_none());
assert!(parse_ul_bytes("urn:smpte:ul:zzzzzzzz.zzzzzzzz.zzzzzzzz.zzzzzzzz").is_none());
}
#[test]
fn flags_audio_not_clip_wrapped() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>48000/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>24</ns2:QuantizationBits>
<ns2:ChannelCount>1</ns2:ChannelCount>
<ns2:ChannelAssignment>urn:smpte:ul:060e2b34.0401010d.04020210.04010000</ns2:ChannelAssignment>
<ns2:ContainerFormat>urn:smpte:ul:060e2b34.04010101.0d010301.02060001</ns2:ContainerFormat>
<ns1:SoundfieldGroupLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
</ns1:SoundfieldGroupLabelSubDescriptor>
<ns1:AudioChannelLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
<ns2:MCAChannelID>1</ns2:MCAChannelID>
<ns2:SoundfieldGroupLinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:SoundfieldGroupLinkID>
</ns1:AudioChannelLabelSubDescriptor>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("AudioNotClipWrapped")),
"expected AudioNotClipWrapped, got: {:#?}",
issues
);
}
#[test]
fn flags_non_mca_channel_assignment_ul() {
let xml = r#"<ns1:WAVEPCMDescriptor>
<ns2:AudioSampleRate>48000/1</ns2:AudioSampleRate>
<ns2:QuantizationBits>24</ns2:QuantizationBits>
<ns2:ChannelCount>1</ns2:ChannelCount>
<ns2:ChannelAssignment>urn:smpte:ul:060e2b34.0401010d.04020110.04010000</ns2:ChannelAssignment>
<ns1:SoundfieldGroupLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
</ns1:SoundfieldGroupLabelSubDescriptor>
<ns1:AudioChannelLabelSubDescriptor>
<ns2:MCALinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:MCALinkID>
<ns2:MCAChannelID>1</ns2:MCAChannelID>
<ns2:SoundfieldGroupLinkID>urn:uuid:11111111-0000-0000-0000-000000000001</ns2:SoundfieldGroupLinkID>
</ns1:AudioChannelLabelSubDescriptor>
</ns1:WAVEPCMDescriptor>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues
.iter()
.any(|i| i.code.contains("ChannelAssignmentNotMCA")),
"expected ChannelAssignmentNotMCA, got: {:#?}",
issues
);
}
#[test]
fn video1_fixture_produces_zero_audio_mca_diagnostics() {
let path = fixture("video1.mxf");
let opts = regxml::MxfFragmentOptions {
partition: regxml::PartitionTarget::Header,
..Default::default()
};
let xml = parse_mxf_to_regxml(&path, opts).expect("video1 → RegXML");
let issues = check_audio_mca(&xml, &path);
assert!(
issues.is_empty(),
"audio MCA pipeline must be silent on video1.mxf (no audio descriptor), got: {:#?}",
issues
);
}
#[test]
fn fires_warning_when_rfc5646_spoken_language_missing() {
let (xml, path) = audio1_regxml();
let issues = check_audio_mca(&xml, &path);
assert!(
issues
.iter()
.any(|i| i.code.contains("RFC5646SpokenLanguageMissing")),
"expected RFC5646SpokenLanguageMissing on audio1, got: {:#?}",
issues
);
}
#[test]
fn skips_when_no_sound_descriptor() {
let xml = r#"<ns1:Preface>
<ns1:CDCIDescriptor>
<ns2:SampleRate>24000/1001</ns2:SampleRate>
</ns1:CDCIDescriptor>
</ns1:Preface>"#;
let issues = check_audio_mca(xml, std::path::Path::new("/synth.mxf"));
assert!(
issues.is_empty(),
"video-only RegXML should produce no audio diagnostics, got: {:#?}",
issues
);
}
}