Skip to main content

imferno_core/validation/
iab_codes.rs

1//! Typed validation-code catalogue for SMPTE ST 2067-201 (IAB Level 0 Plug-in).
2//!
3//! The same 21 reason codes apply to both the 2019 and 2021 editions; a
4//! `macro_rules!` generates `St2067_201_2019` and `St2067_201_2021` from a
5//! single source-of-truth variant list.  Each edition enum also exposes
6//! `for_code` so that shared helper functions can produce the right
7//! `&'static str` without any runtime string building.
8
9use crate::diagnostics::codes::ValidationCode;
10use crate::diagnostics::{Category, Severity};
11
12// ─────────────────────────────────────────────────────────────────────────────
13// Spec-agnostic reason codes
14// ─────────────────────────────────────────────────────────────────────────────
15
16/// Spec-agnostic reason codes for IAB Level 0 Plug-in validation.
17///
18/// Passed to each edition's `for_code` dispatch function to get the full
19/// `&'static str` code without any runtime string building.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum IabCode {
22    /// IABEssenceDescriptor: Codec item shall not be present (§5.9).
23    CodecForbidden,
24    /// IABEssenceDescriptor: ElectrospatialFormulation shall not be present (§5.9).
25    ElectrospatialFormulationForbidden,
26    /// IABEssenceDescriptor: QuantizationBits is missing; shall be 24 (§5.9).
27    QuantizationBitsMissing,
28    /// IABEssenceDescriptor: QuantizationBits is present but not 24 (§5.9).
29    QuantizationBitsInvalid,
30    /// IABEssenceDescriptor: ContainerFormat is missing (§5.3).
31    ContainerFormatMissing,
32    /// IABEssenceDescriptor: ContainerFormat is not the required IAB container UL (§5.3).
33    EssenceContainerInvalid,
34    /// IABEssenceDescriptor: AudioSampleRate is missing; shall be 48000/1 (§5.9).
35    AudioSamplingRateMissing,
36    /// IABEssenceDescriptor: AudioSampleRate is not 48000/1 (§5.9).
37    AudioSamplingRateInvalid,
38    /// IABEssenceDescriptor: SoundCompression is missing (§5.9).
39    SoundCompressionMissing,
40    /// IABEssenceDescriptor: SoundCompression is not the required IAB compression UL (§5.9).
41    SoundCompressionInvalid,
42    /// IABEssenceDescriptor: ChannelCount shall be the distinguished value 0 (§5.9, 2019 only).
43    ChannelCountNotZero,
44    /// IABEssenceDescriptor: IABSoundfieldLabelSubDescriptor shall be present (§5.9).
45    SubDescriptorMissing,
46    /// IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be "IAB" — missing (§5.9).
47    MCATagSymbolMissing,
48    /// IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be "IAB" — wrong value (§5.9).
49    MCATagSymbolInvalid,
50    /// IABSoundfieldLabelSubDescriptor: MCATagName shall be "IAB" — missing (§5.9).
51    MCATagNameMissing,
52    /// IABSoundfieldLabelSubDescriptor: MCATagName shall be "IAB" — wrong value (§5.9).
53    MCATagNameInvalid,
54    /// IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is missing (§5.9).
55    MCALabelDictionaryIDMissing,
56    /// IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is not the required IAB label UL (§5.9).
57    MCALabelDictionaryIDInvalid,
58    /// Segment has IABSequence but no MainAudioSequence (§6.2).
59    MainAudioMissing,
60    /// IABSequence shall contain at least one Resource (§6.2).
61    IABSequenceNoResources,
62    /// IABSequence Resource.SourceEncoding does not reference an IABEssenceDescriptor (§6.2).
63    IABSequenceSourceEncodingInvalid,
64}
65
66// ─────────────────────────────────────────────────────────────────────────────
67// Edition-specific enums (generated by macro)
68// ─────────────────────────────────────────────────────────────────────────────
69
70macro_rules! define_iab_enum {
71    // No identical predecessor — trait default `None`.
72    ($name:ident, $prefix:literal) => {
73        define_iab_enum!(@inner $name, $prefix, None);
74    };
75    // Identical-to-prior-edition annotation (verified via snapshot
76    // diff, docs/catalogue-todos.md Item 2).
77    ($name:ident, $prefix:literal, $previous:literal) => {
78        define_iab_enum!(@inner $name, $prefix, Some($previous));
79    };
80    (@inner $name:ident, $prefix:literal, $previous:expr) => {
81        /// IAB Level 0 Plug-in validation codes, edition
82        #[doc = $prefix]
83        #[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
84        pub enum $name {
85            /// IABEssenceDescriptor: Codec item shall not be present (§5.9).
86            CodecForbidden,
87            /// IABEssenceDescriptor: ElectrospatialFormulation shall not be present (§5.9).
88            ElectrospatialFormulationForbidden,
89            /// IABEssenceDescriptor: QuantizationBits is missing; shall be 24 (§5.9).
90            QuantizationBitsMissing,
91            /// IABEssenceDescriptor: QuantizationBits is present but not 24 (§5.9).
92            QuantizationBitsInvalid,
93            /// IABEssenceDescriptor: ContainerFormat is missing (§5.3).
94            ContainerFormatMissing,
95            /// IABEssenceDescriptor: ContainerFormat is not the required IAB container UL (§5.3).
96            EssenceContainerInvalid,
97            /// IABEssenceDescriptor: AudioSampleRate is missing; shall be 48000/1 (§5.9).
98            AudioSamplingRateMissing,
99            /// IABEssenceDescriptor: AudioSampleRate is not 48000/1 (§5.9).
100            AudioSamplingRateInvalid,
101            /// IABEssenceDescriptor: SoundCompression is missing (§5.9).
102            SoundCompressionMissing,
103            /// IABEssenceDescriptor: SoundCompression is not the required IAB compression UL (§5.9).
104            SoundCompressionInvalid,
105            /// IABEssenceDescriptor: ChannelCount shall be the distinguished value 0 (§5.9, 2019 only).
106            ChannelCountNotZero,
107            /// IABEssenceDescriptor: IABSoundfieldLabelSubDescriptor shall be present (§5.9).
108            SubDescriptorMissing,
109            /// IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be "IAB" — missing (§5.9).
110            MCATagSymbolMissing,
111            /// IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be "IAB" — wrong value (§5.9).
112            MCATagSymbolInvalid,
113            /// IABSoundfieldLabelSubDescriptor: MCATagName shall be "IAB" — missing (§5.9).
114            MCATagNameMissing,
115            /// IABSoundfieldLabelSubDescriptor: MCATagName shall be "IAB" — wrong value (§5.9).
116            MCATagNameInvalid,
117            /// IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is missing (§5.9).
118            MCALabelDictionaryIDMissing,
119            /// IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is not the required IAB label UL (§5.9).
120            MCALabelDictionaryIDInvalid,
121            /// Segment has IABSequence but no MainAudioSequence (§6.2).
122            MainAudioMissing,
123            /// IABSequence shall contain at least one Resource (§6.2).
124            IABSequenceNoResources,
125            /// IABSequence Resource.SourceEncoding does not reference an IABEssenceDescriptor (§6.2).
126            IABSequenceSourceEncodingInvalid,
127        }
128
129        impl ValidationCode for $name {
130            fn code(&self) -> &'static str {
131                match self {
132                    Self::CodecForbidden                 => concat!($prefix, ":5.9/CodecForbidden"),
133                    Self::ElectrospatialFormulationForbidden => concat!($prefix, ":5.9/ElectrospatialFormulationForbidden"),
134                    Self::QuantizationBitsMissing        => concat!($prefix, ":5.9/QuantizationBitsMissing"),
135                    Self::QuantizationBitsInvalid        => concat!($prefix, ":5.9/QuantizationBitsInvalid"),
136                    Self::ContainerFormatMissing         => concat!($prefix, ":5.3/ContainerFormatMissing"),
137                    Self::EssenceContainerInvalid        => concat!($prefix, ":5.3/EssenceContainerInvalid"),
138                    Self::AudioSamplingRateMissing       => concat!($prefix, ":5.9/AudioSamplingRateMissing"),
139                    Self::AudioSamplingRateInvalid       => concat!($prefix, ":5.9/AudioSamplingRateInvalid"),
140                    Self::SoundCompressionMissing        => concat!($prefix, ":5.9/SoundCompressionMissing"),
141                    Self::SoundCompressionInvalid        => concat!($prefix, ":5.9/SoundCompressionInvalid"),
142                    Self::ChannelCountNotZero            => concat!($prefix, ":5.9/ChannelCountNotZero"),
143                    Self::SubDescriptorMissing           => concat!($prefix, ":5.9/SubDescriptorMissing"),
144                    Self::MCATagSymbolMissing            => concat!($prefix, ":5.9/MCATagSymbolMissing"),
145                    Self::MCATagSymbolInvalid            => concat!($prefix, ":5.9/MCATagSymbolInvalid"),
146                    Self::MCATagNameMissing              => concat!($prefix, ":5.9/MCATagNameMissing"),
147                    Self::MCATagNameInvalid              => concat!($prefix, ":5.9/MCATagNameInvalid"),
148                    Self::MCALabelDictionaryIDMissing    => concat!($prefix, ":5.9/MCALabelDictionaryIDMissing"),
149                    Self::MCALabelDictionaryIDInvalid    => concat!($prefix, ":5.9/MCALabelDictionaryIDInvalid"),
150                    Self::MainAudioMissing               => concat!($prefix, ":6.2/MainAudioMissing"),
151                    Self::IABSequenceNoResources         => concat!($prefix, ":6.2/IABSequenceNoResources"),
152                    Self::IABSequenceSourceEncodingInvalid => concat!($prefix, ":6.2/IABSequenceSourceEncodingInvalid"),
153                }
154            }
155            fn description(&self) -> &'static str {
156                match self {
157                    Self::CodecForbidden                 => "IABEssenceDescriptor: Codec item shall not be present (§5.9).",
158                    Self::ElectrospatialFormulationForbidden => "IABEssenceDescriptor: ElectrospatialFormulation shall not be present (§5.9).",
159                    Self::QuantizationBitsMissing        => "IABEssenceDescriptor: QuantizationBits is missing; shall be 24.",
160                    Self::QuantizationBitsInvalid        => "IABEssenceDescriptor: QuantizationBits shall be 24.",
161                    Self::ContainerFormatMissing         => "IABEssenceDescriptor: ContainerFormat is missing.",
162                    Self::EssenceContainerInvalid        => "IABEssenceDescriptor: ContainerFormat is not the required IAB container UL.",
163                    Self::AudioSamplingRateMissing       => "IABEssenceDescriptor: AudioSampleRate is missing; shall be 48000/1.",
164                    Self::AudioSamplingRateInvalid       => "IABEssenceDescriptor: AudioSampleRate shall be 48000/1.",
165                    Self::SoundCompressionMissing        => "IABEssenceDescriptor: SoundCompression is missing.",
166                    Self::SoundCompressionInvalid        => "IABEssenceDescriptor: SoundCompression is not the required IAB compression UL.",
167                    Self::ChannelCountNotZero            => "IABEssenceDescriptor: ChannelCount shall be the distinguished value 0 (2019 edition).",
168                    Self::SubDescriptorMissing           => "IABEssenceDescriptor: IABSoundfieldLabelSubDescriptor shall be present.",
169                    Self::MCATagSymbolMissing            => "IABSoundfieldLabelSubDescriptor: MCATagSymbol is missing; shall be \"IAB\".",
170                    Self::MCATagSymbolInvalid            => "IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be \"IAB\".",
171                    Self::MCATagNameMissing              => "IABSoundfieldLabelSubDescriptor: MCATagName is missing; shall be \"IAB\".",
172                    Self::MCATagNameInvalid              => "IABSoundfieldLabelSubDescriptor: MCATagName shall be \"IAB\".",
173                    Self::MCALabelDictionaryIDMissing    => "IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is missing.",
174                    Self::MCALabelDictionaryIDInvalid    => "IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is not the required IAB label UL.",
175                    Self::MainAudioMissing               => "Segment has IABSequence but no MainAudioSequence (§6.2).",
176                    Self::IABSequenceNoResources         => "IABSequence shall contain at least one Resource (§6.2).",
177                    Self::IABSequenceSourceEncodingInvalid => "IABSequence Resource.SourceEncoding does not reference an IABEssenceDescriptor (§6.2).",
178                }
179            }
180            fn default_severity(&self) -> Severity {
181                match self {
182                    Self::QuantizationBitsMissing
183                    | Self::ContainerFormatMissing
184                    | Self::AudioSamplingRateMissing
185                    | Self::SoundCompressionMissing => Severity::Warning,
186                    _ => Severity::Error,
187                }
188            }
189            fn category(&self) -> Category {
190                Category::Audio
191            }
192            fn previous_identical_edition(&self) -> Option<&'static str> {
193                $previous
194            }
195            fn example(&self) -> Option<&'static str> {
196                Some(match self {
197                    Self::CodecForbidden =>
198                        "IABEssenceDescriptor with a non-empty Codec UL — ST 2067-201 forbids the item entirely.",
199                    Self::ElectrospatialFormulationForbidden =>
200                        "IABEssenceDescriptor sets ElectrospatialFormulation; the item shall be absent.",
201                    Self::QuantizationBitsMissing =>
202                        "IABEssenceDescriptor with no QuantizationBits item.",
203                    Self::QuantizationBitsInvalid =>
204                        "IABEssenceDescriptor with QuantizationBits = 16 instead of 24.",
205                    Self::ContainerFormatMissing =>
206                        "IABEssenceDescriptor with no ContainerFormat (EssenceContainer) UL.",
207                    Self::EssenceContainerInvalid =>
208                        "IABEssenceDescriptor with ContainerFormat = `0d010301.020c0900` (WAV) instead of the IAB frame-wrapped UL.",
209                    Self::AudioSamplingRateMissing =>
210                        "IABEssenceDescriptor with no AudioSampleRate.",
211                    Self::AudioSamplingRateInvalid =>
212                        "IABEssenceDescriptor with AudioSampleRate = 96000/1 instead of 48000/1.",
213                    Self::SoundCompressionMissing =>
214                        "IABEssenceDescriptor with no SoundCompression UL.",
215                    Self::SoundCompressionInvalid =>
216                        "IABEssenceDescriptor with SoundCompression = uncompressed PCM UL instead of the IAB compression UL.",
217                    Self::ChannelCountNotZero =>
218                        "IABEssenceDescriptor with ChannelCount = 2 — the 2019 edition mandates the distinguished value 0.",
219                    Self::SubDescriptorMissing =>
220                        "IABEssenceDescriptor with no IABSoundfieldLabelSubDescriptor in its SubDescriptors strong-ref list.",
221                    Self::MCATagSymbolMissing =>
222                        "IABSoundfieldLabelSubDescriptor with no MCATagSymbol item.",
223                    Self::MCATagSymbolInvalid =>
224                        "IABSoundfieldLabelSubDescriptor with MCATagSymbol = \"71\" instead of \"IAB\".",
225                    Self::MCATagNameMissing =>
226                        "IABSoundfieldLabelSubDescriptor with no MCATagName item.",
227                    Self::MCATagNameInvalid =>
228                        "IABSoundfieldLabelSubDescriptor with MCATagName = \"Immersive Audio Bitstream\" instead of \"IAB\".",
229                    Self::MCALabelDictionaryIDMissing =>
230                        "IABSoundfieldLabelSubDescriptor with no MCALabelDictionaryID UL.",
231                    Self::MCALabelDictionaryIDInvalid =>
232                        "IABSoundfieldLabelSubDescriptor with MCALabelDictionaryID set to a 5.1 surround UL instead of the IAB label UL.",
233                    Self::MainAudioMissing =>
234                        "Segment contains an IABSequence but no MainAudioSequence — required pairing per §6.2.",
235                    Self::IABSequenceNoResources =>
236                        "An IABSequence with an empty `<ResourceList>`.",
237                    Self::IABSequenceSourceEncodingInvalid =>
238                        "An IABSequence Resource whose SourceEncoding references a WAVEPCMDescriptor instead of an IABEssenceDescriptor.",
239                })
240            }
241        }
242
243        impl $name {
244            pub const ALL: &'static [Self] = &[
245                Self::CodecForbidden,
246                Self::ElectrospatialFormulationForbidden,
247                Self::QuantizationBitsMissing,
248                Self::QuantizationBitsInvalid,
249                Self::ContainerFormatMissing,
250                Self::EssenceContainerInvalid,
251                Self::AudioSamplingRateMissing,
252                Self::AudioSamplingRateInvalid,
253                Self::SoundCompressionMissing,
254                Self::SoundCompressionInvalid,
255                Self::ChannelCountNotZero,
256                Self::SubDescriptorMissing,
257                Self::MCATagSymbolMissing,
258                Self::MCATagSymbolInvalid,
259                Self::MCATagNameMissing,
260                Self::MCATagNameInvalid,
261                Self::MCALabelDictionaryIDMissing,
262                Self::MCALabelDictionaryIDInvalid,
263                Self::MainAudioMissing,
264                Self::IABSequenceNoResources,
265                Self::IABSequenceSourceEncodingInvalid,
266            ];
267
268            /// Dispatch from the spec-agnostic [`IabCode`] to this
269            /// edition's static code string.  Used by the shared validator helpers.
270            pub fn for_code(r: IabCode) -> &'static str {
271                match r {
272                    IabCode::CodecForbidden                    => concat!($prefix, ":5.9/CodecForbidden"),
273                    IabCode::ElectrospatialFormulationForbidden => concat!($prefix, ":5.9/ElectrospatialFormulationForbidden"),
274                    IabCode::QuantizationBitsMissing           => concat!($prefix, ":5.9/QuantizationBitsMissing"),
275                    IabCode::QuantizationBitsInvalid           => concat!($prefix, ":5.9/QuantizationBitsInvalid"),
276                    IabCode::ContainerFormatMissing            => concat!($prefix, ":5.3/ContainerFormatMissing"),
277                    IabCode::EssenceContainerInvalid           => concat!($prefix, ":5.3/EssenceContainerInvalid"),
278                    IabCode::AudioSamplingRateMissing          => concat!($prefix, ":5.9/AudioSamplingRateMissing"),
279                    IabCode::AudioSamplingRateInvalid          => concat!($prefix, ":5.9/AudioSamplingRateInvalid"),
280                    IabCode::SoundCompressionMissing           => concat!($prefix, ":5.9/SoundCompressionMissing"),
281                    IabCode::SoundCompressionInvalid           => concat!($prefix, ":5.9/SoundCompressionInvalid"),
282                    IabCode::ChannelCountNotZero               => concat!($prefix, ":5.9/ChannelCountNotZero"),
283                    IabCode::SubDescriptorMissing              => concat!($prefix, ":5.9/SubDescriptorMissing"),
284                    IabCode::MCATagSymbolMissing               => concat!($prefix, ":5.9/MCATagSymbolMissing"),
285                    IabCode::MCATagSymbolInvalid               => concat!($prefix, ":5.9/MCATagSymbolInvalid"),
286                    IabCode::MCATagNameMissing                 => concat!($prefix, ":5.9/MCATagNameMissing"),
287                    IabCode::MCATagNameInvalid                 => concat!($prefix, ":5.9/MCATagNameInvalid"),
288                    IabCode::MCALabelDictionaryIDMissing       => concat!($prefix, ":5.9/MCALabelDictionaryIDMissing"),
289                    IabCode::MCALabelDictionaryIDInvalid       => concat!($prefix, ":5.9/MCALabelDictionaryIDInvalid"),
290                    IabCode::MainAudioMissing                  => concat!($prefix, ":6.2/MainAudioMissing"),
291                    IabCode::IABSequenceNoResources            => concat!($prefix, ":6.2/IABSequenceNoResources"),
292                    IabCode::IABSequenceSourceEncodingInvalid  => concat!($prefix, ":6.2/IABSequenceSourceEncodingInvalid"),
293                }
294            }
295        }
296
297        impl From<$name> for String {
298            fn from(c: $name) -> String {
299                c.code().to_string()
300            }
301        }
302    };
303}
304
305define_iab_enum!(St2067_201_2019, "ST2067-201:2019");
306// 2021 catalogue is bit-for-bit identical to 2019 (audit, 2026-06-04).
307define_iab_enum!(St2067_201_2021, "ST2067-201:2021", "ST2067-201:2019");
308
309// ─────────────────────────────────────────────────────────────────────────────
310// ST 2067-201:2026 delta-catalogue
311// ─────────────────────────────────────────────────────────────────────────────
312//
313// The 2026 revision inherits every 2021 rule verbatim (use the
314// `St2067_201_2021` enum for those) and adds exactly one
315// recommendation, Annex E §E.2.
316//
317// Carrying this as a standalone enum (rather than expanding the
318// `define_iab_enum!` macro to all editions) keeps the 2019↔2021
319// `previous_identical_edition` annotation honest — those catalogues
320// remain bit-identical, and the 2026 plugin selects this delta enum
321// on top of the 2021 base.
322
323/// ST 2067-201:2026-only delta codes (over and above the 2021 catalogue).
324#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
325pub enum St2067_201_2026Delta {
326    /// Annex E §E.2 — Track Files **should** contain one
327    /// `IABChannelSubDescriptor` for each channel of each
328    /// `BedDefinition`.
329    IabChannelSubDescriptorRecommended,
330}
331
332impl crate::diagnostics::codes::ValidationCode for St2067_201_2026Delta {
333    fn code(&self) -> &'static str {
334        match self {
335            Self::IabChannelSubDescriptorRecommended => {
336                "ST2067-201:2026:Annex-E/IabChannelSubDescriptorRecommended"
337            }
338        }
339    }
340    fn description(&self) -> &'static str {
341        match self {
342            Self::IabChannelSubDescriptorRecommended =>
343                "Track File should carry an IABChannelSubDescriptor for each channel of each BedDefinition (Annex E).",
344        }
345    }
346    fn default_severity(&self) -> crate::diagnostics::Severity {
347        // `should` strength per Annex E §E.2 — Warning, not Error.
348        crate::diagnostics::Severity::Warning
349    }
350    fn category(&self) -> crate::diagnostics::Category {
351        crate::diagnostics::Category::Audio
352    }
353    fn example(&self) -> Option<&'static str> {
354        Some(
355            "<IABEssenceDescriptor><SubDescriptors><IABSoundfieldLabelSubDescriptor/></SubDescriptors></IABEssenceDescriptor>  \
356             <!-- no <IABChannelSubDescriptor> entries -->",
357        )
358    }
359}
360
361impl St2067_201_2026Delta {
362    pub const ALL: &'static [Self] = &[Self::IabChannelSubDescriptorRecommended];
363}
364
365impl From<St2067_201_2026Delta> for String {
366    fn from(c: St2067_201_2026Delta) -> String {
367        use crate::diagnostics::codes::ValidationCode;
368        c.code().to_string()
369    }
370}