imferno-core 3.0.1

SMPTE ST 2067 IMF parser and validator
Documentation
//! Typed validation-code catalogue for SMPTE ST 2067-201 (IAB Level 0 Plug-in).
//!
//! The same 21 reason codes apply to both the 2019 and 2021 editions; a
//! `macro_rules!` generates `St2067_201_2019` and `St2067_201_2021` from a
//! single source-of-truth variant list.  Each edition enum also exposes
//! `for_code` so that shared helper functions can produce the right
//! `&'static str` without any runtime string building.

use crate::diagnostics::codes::ValidationCode;
use crate::diagnostics::{Category, Severity};

// ─────────────────────────────────────────────────────────────────────────────
// Spec-agnostic reason codes
// ─────────────────────────────────────────────────────────────────────────────

/// Spec-agnostic reason codes for IAB Level 0 Plug-in validation.
///
/// Passed to each edition's `for_code` dispatch function to get the full
/// `&'static str` code without any runtime string building.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IabCode {
    /// IABEssenceDescriptor: Codec item shall not be present (§5.9).
    CodecForbidden,
    /// IABEssenceDescriptor: ElectrospatialFormulation shall not be present (§5.9).
    ElectrospatialFormulationForbidden,
    /// IABEssenceDescriptor: QuantizationBits is missing; shall be 24 (§5.9).
    QuantizationBitsMissing,
    /// IABEssenceDescriptor: QuantizationBits is present but not 24 (§5.9).
    QuantizationBitsInvalid,
    /// IABEssenceDescriptor: ContainerFormat is missing (§5.3).
    ContainerFormatMissing,
    /// IABEssenceDescriptor: ContainerFormat is not the required IAB container UL (§5.3).
    EssenceContainerInvalid,
    /// IABEssenceDescriptor: AudioSampleRate is missing; shall be 48000/1 (§5.9).
    AudioSamplingRateMissing,
    /// IABEssenceDescriptor: AudioSampleRate is not 48000/1 (§5.9).
    AudioSamplingRateInvalid,
    /// IABEssenceDescriptor: SoundCompression is missing (§5.9).
    SoundCompressionMissing,
    /// IABEssenceDescriptor: SoundCompression is not the required IAB compression UL (§5.9).
    SoundCompressionInvalid,
    /// IABEssenceDescriptor: ChannelCount shall be the distinguished value 0 (§5.9, 2019 only).
    ChannelCountNotZero,
    /// IABEssenceDescriptor: IABSoundfieldLabelSubDescriptor shall be present (§5.9).
    SubDescriptorMissing,
    /// IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be "IAB" — missing (§5.9).
    MCATagSymbolMissing,
    /// IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be "IAB" — wrong value (§5.9).
    MCATagSymbolInvalid,
    /// IABSoundfieldLabelSubDescriptor: MCATagName shall be "IAB" — missing (§5.9).
    MCATagNameMissing,
    /// IABSoundfieldLabelSubDescriptor: MCATagName shall be "IAB" — wrong value (§5.9).
    MCATagNameInvalid,
    /// IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is missing (§5.9).
    MCALabelDictionaryIDMissing,
    /// IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is not the required IAB label UL (§5.9).
    MCALabelDictionaryIDInvalid,
    /// Segment has IABSequence but no MainAudioSequence (§6.2).
    MainAudioMissing,
    /// IABSequence shall contain at least one Resource (§6.2).
    IABSequenceNoResources,
    /// IABSequence Resource.SourceEncoding does not reference an IABEssenceDescriptor (§6.2).
    IABSequenceSourceEncodingInvalid,
}

// ─────────────────────────────────────────────────────────────────────────────
// Edition-specific enums (generated by macro)
// ─────────────────────────────────────────────────────────────────────────────

macro_rules! define_iab_enum {
    // No identical predecessor — trait default `None`.
    ($name:ident, $prefix:literal) => {
        define_iab_enum!(@inner $name, $prefix, None);
    };
    // Identical-to-prior-edition annotation (verified via snapshot
    // diff, docs/catalogue-todos.md Item 2).
    ($name:ident, $prefix:literal, $previous:literal) => {
        define_iab_enum!(@inner $name, $prefix, Some($previous));
    };
    (@inner $name:ident, $prefix:literal, $previous:expr) => {
        /// IAB Level 0 Plug-in validation codes, edition
        #[doc = $prefix]
        #[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
        pub enum $name {
            /// IABEssenceDescriptor: Codec item shall not be present (§5.9).
            CodecForbidden,
            /// IABEssenceDescriptor: ElectrospatialFormulation shall not be present (§5.9).
            ElectrospatialFormulationForbidden,
            /// IABEssenceDescriptor: QuantizationBits is missing; shall be 24 (§5.9).
            QuantizationBitsMissing,
            /// IABEssenceDescriptor: QuantizationBits is present but not 24 (§5.9).
            QuantizationBitsInvalid,
            /// IABEssenceDescriptor: ContainerFormat is missing (§5.3).
            ContainerFormatMissing,
            /// IABEssenceDescriptor: ContainerFormat is not the required IAB container UL (§5.3).
            EssenceContainerInvalid,
            /// IABEssenceDescriptor: AudioSampleRate is missing; shall be 48000/1 (§5.9).
            AudioSamplingRateMissing,
            /// IABEssenceDescriptor: AudioSampleRate is not 48000/1 (§5.9).
            AudioSamplingRateInvalid,
            /// IABEssenceDescriptor: SoundCompression is missing (§5.9).
            SoundCompressionMissing,
            /// IABEssenceDescriptor: SoundCompression is not the required IAB compression UL (§5.9).
            SoundCompressionInvalid,
            /// IABEssenceDescriptor: ChannelCount shall be the distinguished value 0 (§5.9, 2019 only).
            ChannelCountNotZero,
            /// IABEssenceDescriptor: IABSoundfieldLabelSubDescriptor shall be present (§5.9).
            SubDescriptorMissing,
            /// IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be "IAB" — missing (§5.9).
            MCATagSymbolMissing,
            /// IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be "IAB" — wrong value (§5.9).
            MCATagSymbolInvalid,
            /// IABSoundfieldLabelSubDescriptor: MCATagName shall be "IAB" — missing (§5.9).
            MCATagNameMissing,
            /// IABSoundfieldLabelSubDescriptor: MCATagName shall be "IAB" — wrong value (§5.9).
            MCATagNameInvalid,
            /// IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is missing (§5.9).
            MCALabelDictionaryIDMissing,
            /// IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is not the required IAB label UL (§5.9).
            MCALabelDictionaryIDInvalid,
            /// Segment has IABSequence but no MainAudioSequence (§6.2).
            MainAudioMissing,
            /// IABSequence shall contain at least one Resource (§6.2).
            IABSequenceNoResources,
            /// IABSequence Resource.SourceEncoding does not reference an IABEssenceDescriptor (§6.2).
            IABSequenceSourceEncodingInvalid,
        }

        impl ValidationCode for $name {
            fn code(&self) -> &'static str {
                match self {
                    Self::CodecForbidden                 => concat!($prefix, ":5.9/CodecForbidden"),
                    Self::ElectrospatialFormulationForbidden => concat!($prefix, ":5.9/ElectrospatialFormulationForbidden"),
                    Self::QuantizationBitsMissing        => concat!($prefix, ":5.9/QuantizationBitsMissing"),
                    Self::QuantizationBitsInvalid        => concat!($prefix, ":5.9/QuantizationBitsInvalid"),
                    Self::ContainerFormatMissing         => concat!($prefix, ":5.3/ContainerFormatMissing"),
                    Self::EssenceContainerInvalid        => concat!($prefix, ":5.3/EssenceContainerInvalid"),
                    Self::AudioSamplingRateMissing       => concat!($prefix, ":5.9/AudioSamplingRateMissing"),
                    Self::AudioSamplingRateInvalid       => concat!($prefix, ":5.9/AudioSamplingRateInvalid"),
                    Self::SoundCompressionMissing        => concat!($prefix, ":5.9/SoundCompressionMissing"),
                    Self::SoundCompressionInvalid        => concat!($prefix, ":5.9/SoundCompressionInvalid"),
                    Self::ChannelCountNotZero            => concat!($prefix, ":5.9/ChannelCountNotZero"),
                    Self::SubDescriptorMissing           => concat!($prefix, ":5.9/SubDescriptorMissing"),
                    Self::MCATagSymbolMissing            => concat!($prefix, ":5.9/MCATagSymbolMissing"),
                    Self::MCATagSymbolInvalid            => concat!($prefix, ":5.9/MCATagSymbolInvalid"),
                    Self::MCATagNameMissing              => concat!($prefix, ":5.9/MCATagNameMissing"),
                    Self::MCATagNameInvalid              => concat!($prefix, ":5.9/MCATagNameInvalid"),
                    Self::MCALabelDictionaryIDMissing    => concat!($prefix, ":5.9/MCALabelDictionaryIDMissing"),
                    Self::MCALabelDictionaryIDInvalid    => concat!($prefix, ":5.9/MCALabelDictionaryIDInvalid"),
                    Self::MainAudioMissing               => concat!($prefix, ":6.2/MainAudioMissing"),
                    Self::IABSequenceNoResources         => concat!($prefix, ":6.2/IABSequenceNoResources"),
                    Self::IABSequenceSourceEncodingInvalid => concat!($prefix, ":6.2/IABSequenceSourceEncodingInvalid"),
                }
            }
            fn description(&self) -> &'static str {
                match self {
                    Self::CodecForbidden                 => "IABEssenceDescriptor: Codec item shall not be present (§5.9).",
                    Self::ElectrospatialFormulationForbidden => "IABEssenceDescriptor: ElectrospatialFormulation shall not be present (§5.9).",
                    Self::QuantizationBitsMissing        => "IABEssenceDescriptor: QuantizationBits is missing; shall be 24.",
                    Self::QuantizationBitsInvalid        => "IABEssenceDescriptor: QuantizationBits shall be 24.",
                    Self::ContainerFormatMissing         => "IABEssenceDescriptor: ContainerFormat is missing.",
                    Self::EssenceContainerInvalid        => "IABEssenceDescriptor: ContainerFormat is not the required IAB container UL.",
                    Self::AudioSamplingRateMissing       => "IABEssenceDescriptor: AudioSampleRate is missing; shall be 48000/1.",
                    Self::AudioSamplingRateInvalid       => "IABEssenceDescriptor: AudioSampleRate shall be 48000/1.",
                    Self::SoundCompressionMissing        => "IABEssenceDescriptor: SoundCompression is missing.",
                    Self::SoundCompressionInvalid        => "IABEssenceDescriptor: SoundCompression is not the required IAB compression UL.",
                    Self::ChannelCountNotZero            => "IABEssenceDescriptor: ChannelCount shall be the distinguished value 0 (2019 edition).",
                    Self::SubDescriptorMissing           => "IABEssenceDescriptor: IABSoundfieldLabelSubDescriptor shall be present.",
                    Self::MCATagSymbolMissing            => "IABSoundfieldLabelSubDescriptor: MCATagSymbol is missing; shall be \"IAB\".",
                    Self::MCATagSymbolInvalid            => "IABSoundfieldLabelSubDescriptor: MCATagSymbol shall be \"IAB\".",
                    Self::MCATagNameMissing              => "IABSoundfieldLabelSubDescriptor: MCATagName is missing; shall be \"IAB\".",
                    Self::MCATagNameInvalid              => "IABSoundfieldLabelSubDescriptor: MCATagName shall be \"IAB\".",
                    Self::MCALabelDictionaryIDMissing    => "IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is missing.",
                    Self::MCALabelDictionaryIDInvalid    => "IABSoundfieldLabelSubDescriptor: MCALabelDictionaryID is not the required IAB label UL.",
                    Self::MainAudioMissing               => "Segment has IABSequence but no MainAudioSequence (§6.2).",
                    Self::IABSequenceNoResources         => "IABSequence shall contain at least one Resource (§6.2).",
                    Self::IABSequenceSourceEncodingInvalid => "IABSequence Resource.SourceEncoding does not reference an IABEssenceDescriptor (§6.2).",
                }
            }
            fn default_severity(&self) -> Severity {
                match self {
                    Self::QuantizationBitsMissing
                    | Self::ContainerFormatMissing
                    | Self::AudioSamplingRateMissing
                    | Self::SoundCompressionMissing => Severity::Warning,
                    _ => Severity::Error,
                }
            }
            fn category(&self) -> Category {
                Category::Audio
            }
            fn previous_identical_edition(&self) -> Option<&'static str> {
                $previous
            }
            fn example(&self) -> Option<&'static str> {
                Some(match self {
                    Self::CodecForbidden =>
                        "IABEssenceDescriptor with a non-empty Codec UL — ST 2067-201 forbids the item entirely.",
                    Self::ElectrospatialFormulationForbidden =>
                        "IABEssenceDescriptor sets ElectrospatialFormulation; the item shall be absent.",
                    Self::QuantizationBitsMissing =>
                        "IABEssenceDescriptor with no QuantizationBits item.",
                    Self::QuantizationBitsInvalid =>
                        "IABEssenceDescriptor with QuantizationBits = 16 instead of 24.",
                    Self::ContainerFormatMissing =>
                        "IABEssenceDescriptor with no ContainerFormat (EssenceContainer) UL.",
                    Self::EssenceContainerInvalid =>
                        "IABEssenceDescriptor with ContainerFormat = `0d010301.020c0900` (WAV) instead of the IAB frame-wrapped UL.",
                    Self::AudioSamplingRateMissing =>
                        "IABEssenceDescriptor with no AudioSampleRate.",
                    Self::AudioSamplingRateInvalid =>
                        "IABEssenceDescriptor with AudioSampleRate = 96000/1 instead of 48000/1.",
                    Self::SoundCompressionMissing =>
                        "IABEssenceDescriptor with no SoundCompression UL.",
                    Self::SoundCompressionInvalid =>
                        "IABEssenceDescriptor with SoundCompression = uncompressed PCM UL instead of the IAB compression UL.",
                    Self::ChannelCountNotZero =>
                        "IABEssenceDescriptor with ChannelCount = 2 — the 2019 edition mandates the distinguished value 0.",
                    Self::SubDescriptorMissing =>
                        "IABEssenceDescriptor with no IABSoundfieldLabelSubDescriptor in its SubDescriptors strong-ref list.",
                    Self::MCATagSymbolMissing =>
                        "IABSoundfieldLabelSubDescriptor with no MCATagSymbol item.",
                    Self::MCATagSymbolInvalid =>
                        "IABSoundfieldLabelSubDescriptor with MCATagSymbol = \"71\" instead of \"IAB\".",
                    Self::MCATagNameMissing =>
                        "IABSoundfieldLabelSubDescriptor with no MCATagName item.",
                    Self::MCATagNameInvalid =>
                        "IABSoundfieldLabelSubDescriptor with MCATagName = \"Immersive Audio Bitstream\" instead of \"IAB\".",
                    Self::MCALabelDictionaryIDMissing =>
                        "IABSoundfieldLabelSubDescriptor with no MCALabelDictionaryID UL.",
                    Self::MCALabelDictionaryIDInvalid =>
                        "IABSoundfieldLabelSubDescriptor with MCALabelDictionaryID set to a 5.1 surround UL instead of the IAB label UL.",
                    Self::MainAudioMissing =>
                        "Segment contains an IABSequence but no MainAudioSequence — required pairing per §6.2.",
                    Self::IABSequenceNoResources =>
                        "An IABSequence with an empty `<ResourceList>`.",
                    Self::IABSequenceSourceEncodingInvalid =>
                        "An IABSequence Resource whose SourceEncoding references a WAVEPCMDescriptor instead of an IABEssenceDescriptor.",
                })
            }
        }

        impl $name {
            pub const ALL: &'static [Self] = &[
                Self::CodecForbidden,
                Self::ElectrospatialFormulationForbidden,
                Self::QuantizationBitsMissing,
                Self::QuantizationBitsInvalid,
                Self::ContainerFormatMissing,
                Self::EssenceContainerInvalid,
                Self::AudioSamplingRateMissing,
                Self::AudioSamplingRateInvalid,
                Self::SoundCompressionMissing,
                Self::SoundCompressionInvalid,
                Self::ChannelCountNotZero,
                Self::SubDescriptorMissing,
                Self::MCATagSymbolMissing,
                Self::MCATagSymbolInvalid,
                Self::MCATagNameMissing,
                Self::MCATagNameInvalid,
                Self::MCALabelDictionaryIDMissing,
                Self::MCALabelDictionaryIDInvalid,
                Self::MainAudioMissing,
                Self::IABSequenceNoResources,
                Self::IABSequenceSourceEncodingInvalid,
            ];

            /// Dispatch from the spec-agnostic [`IabCode`] to this
            /// edition's static code string.  Used by the shared validator helpers.
            pub fn for_code(r: IabCode) -> &'static str {
                match r {
                    IabCode::CodecForbidden                    => concat!($prefix, ":5.9/CodecForbidden"),
                    IabCode::ElectrospatialFormulationForbidden => concat!($prefix, ":5.9/ElectrospatialFormulationForbidden"),
                    IabCode::QuantizationBitsMissing           => concat!($prefix, ":5.9/QuantizationBitsMissing"),
                    IabCode::QuantizationBitsInvalid           => concat!($prefix, ":5.9/QuantizationBitsInvalid"),
                    IabCode::ContainerFormatMissing            => concat!($prefix, ":5.3/ContainerFormatMissing"),
                    IabCode::EssenceContainerInvalid           => concat!($prefix, ":5.3/EssenceContainerInvalid"),
                    IabCode::AudioSamplingRateMissing          => concat!($prefix, ":5.9/AudioSamplingRateMissing"),
                    IabCode::AudioSamplingRateInvalid          => concat!($prefix, ":5.9/AudioSamplingRateInvalid"),
                    IabCode::SoundCompressionMissing           => concat!($prefix, ":5.9/SoundCompressionMissing"),
                    IabCode::SoundCompressionInvalid           => concat!($prefix, ":5.9/SoundCompressionInvalid"),
                    IabCode::ChannelCountNotZero               => concat!($prefix, ":5.9/ChannelCountNotZero"),
                    IabCode::SubDescriptorMissing              => concat!($prefix, ":5.9/SubDescriptorMissing"),
                    IabCode::MCATagSymbolMissing               => concat!($prefix, ":5.9/MCATagSymbolMissing"),
                    IabCode::MCATagSymbolInvalid               => concat!($prefix, ":5.9/MCATagSymbolInvalid"),
                    IabCode::MCATagNameMissing                 => concat!($prefix, ":5.9/MCATagNameMissing"),
                    IabCode::MCATagNameInvalid                 => concat!($prefix, ":5.9/MCATagNameInvalid"),
                    IabCode::MCALabelDictionaryIDMissing       => concat!($prefix, ":5.9/MCALabelDictionaryIDMissing"),
                    IabCode::MCALabelDictionaryIDInvalid       => concat!($prefix, ":5.9/MCALabelDictionaryIDInvalid"),
                    IabCode::MainAudioMissing                  => concat!($prefix, ":6.2/MainAudioMissing"),
                    IabCode::IABSequenceNoResources            => concat!($prefix, ":6.2/IABSequenceNoResources"),
                    IabCode::IABSequenceSourceEncodingInvalid  => concat!($prefix, ":6.2/IABSequenceSourceEncodingInvalid"),
                }
            }
        }

        impl From<$name> for String {
            fn from(c: $name) -> String {
                c.code().to_string()
            }
        }
    };
}

define_iab_enum!(St2067_201_2019, "ST2067-201:2019");
// 2021 catalogue is bit-for-bit identical to 2019 (audit, 2026-06-04).
define_iab_enum!(St2067_201_2021, "ST2067-201:2021", "ST2067-201:2019");

// ─────────────────────────────────────────────────────────────────────────────
// ST 2067-201:2026 delta-catalogue
// ─────────────────────────────────────────────────────────────────────────────
//
// The 2026 revision inherits every 2021 rule verbatim (use the
// `St2067_201_2021` enum for those) and adds exactly one
// recommendation, Annex E §E.2.
//
// Carrying this as a standalone enum (rather than expanding the
// `define_iab_enum!` macro to all editions) keeps the 2019↔2021
// `previous_identical_edition` annotation honest — those catalogues
// remain bit-identical, and the 2026 plugin selects this delta enum
// on top of the 2021 base.

/// ST 2067-201:2026-only delta codes (over and above the 2021 catalogue).
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
pub enum St2067_201_2026Delta {
    /// Annex E §E.2 — Track Files **should** contain one
    /// `IABChannelSubDescriptor` for each channel of each
    /// `BedDefinition`.
    IabChannelSubDescriptorRecommended,
}

impl crate::diagnostics::codes::ValidationCode for St2067_201_2026Delta {
    fn code(&self) -> &'static str {
        match self {
            Self::IabChannelSubDescriptorRecommended => {
                "ST2067-201:2026:Annex-E/IabChannelSubDescriptorRecommended"
            }
        }
    }
    fn description(&self) -> &'static str {
        match self {
            Self::IabChannelSubDescriptorRecommended =>
                "Track File should carry an IABChannelSubDescriptor for each channel of each BedDefinition (Annex E).",
        }
    }
    fn default_severity(&self) -> crate::diagnostics::Severity {
        // `should` strength per Annex E §E.2 — Warning, not Error.
        crate::diagnostics::Severity::Warning
    }
    fn category(&self) -> crate::diagnostics::Category {
        crate::diagnostics::Category::Audio
    }
    fn example(&self) -> Option<&'static str> {
        Some(
            "<IABEssenceDescriptor><SubDescriptors><IABSoundfieldLabelSubDescriptor/></SubDescriptors></IABEssenceDescriptor>  \
             <!-- no <IABChannelSubDescriptor> entries -->",
        )
    }
}

impl St2067_201_2026Delta {
    pub const ALL: &'static [Self] = &[Self::IabChannelSubDescriptorRecommended];
}

impl From<St2067_201_2026Delta> for String {
    fn from(c: St2067_201_2026Delta) -> String {
        use crate::diagnostics::codes::ValidationCode;
        c.code().to_string()
    }
}