Swift Audio Converter

Swift Audio Converter

Introduction : Le probleme de la verification des profils AAC

Contexte normatif

Les profils AAC sont definis dans ISO/IEC 14496-3:2009 (MPEG-4 Audio, Part 3). Chaque profil correspond a un Audio Object Type (AOT) qui determine les capacites de compression et les extensions utilisees :

Audio Object Type Profil Extensions Reference ISO/IEC 14496-3
2 AAC-LC Aucune Table 1.16
5 HE-AAC (SBR) Spectral Band Replication Section 4.6.18
29 HE-AAC v2 (PS) SBR + Parametric Stereo Section 4.6.20
23 AAC-LD Low Delay Section 4.6.3
39 AAC-ELD Enhanced Low Delay Section 4.6.10

References Apple :

  • Technical Note TN2236: High-Efficiency Advanced Audio Coding (HE-AAC)
  • Technical Note TN2237: Audio Export - Encoding AAC Audio

Comportement observe avec AVAssetWriter

Probleme technique : Lors de l'encodage AAC sur macOS/iOS avec AVFoundation, il n'existe aucune API Core Audio pour verifier programmatiquement le profil AAC reellement produit dans le fichier final.

import AVFoundation

// Configuration demandee : HE-AAC v2 a 48 kbps
let outputSettings: [String: Any] = [
    AVFormatIDKey: kAudioFormatMPEG4AAC_HE_V2,
    AVEncoderBitRateKey: 48_000,
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 2
]

let assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: .m4a)
let writerInput = AVAssetWriterInput(
    mediaType: .audio,
    outputSettings: outputSettings
)

// Probleme : Aucune API pour verifier post-encodage :
// 1. Le profil effectivement encode dans le fichier
// 2. Si un fallback silencieux a eu lieu (HE-AAC v2 -> AAC-LC)
// 3. Si les extensions SBR/PS sont reellement presentes dans le bitstream

Consequence pratique : Un fichier peut etre etiquete "HE-AAC v2" dans les metadonnees de conteneur mais contenir de l'AAC-LC, resultant en une qualite audio inadequate pour le bitrate configure.

Exemple de probleme reel :

Configuration : HE-AAC v2 @ 48 kbps
Resultat attendu : Qualite acceptable (HE-AAC v2 est optimise pour bas debits)
Resultat reel : Qualite mediocre (AAC-LC @ 48 kbps est insuffisant)

Solution : Parser l'AudioSpecificConfig

Reference normative : ISO/IEC 14496-3:2009, Section 1.6.2.1 - "AudioSpecificConfig"

L'AudioSpecificConfig (ASC) est une structure binaire contenue dans le "magic cookie" du fichier AAC. Elle definit precisement les parametres de decodage conformement a la norme MPEG-4 Audio, et constitue la seule source de verite sur le profil reellement encode.

Emplacement dans le fichier MP4/M4A : L'ASC est stocke dans le Elementary Stream Descriptor (ESDS) conformement a ISO/IEC 14496-1:2004 (MPEG-4 Systems, Part 1).


Partie 1 : Structure de l'AudioSpecificConfig

Definition selon ISO/IEC 14496-3

L'AudioSpecificConfig est defini dans la section 1.6.2.1 de la norme MPEG-4 Audio :

AudioSpecificConfig() {
    audioObjectType;                    // 5 bits
    samplingFrequencyIndex;             // 4 bits
    if (samplingFrequencyIndex == 0xF) {
        samplingFrequency;              // 24 bits (explicit frequency)
    }
    channelConfiguration;               // 4 bits

    // Extensions pour profils avances (HE-AAC, etc.)
    if (audioObjectType == 5 || audioObjectType == 29) {
        extensionSamplingFrequencyIndex;  // 4 bits
        extensionAudioObjectType;         // 5 bits
    }

    // Informations additionnelles selon le profil
    // (frame length, depends on core coder, etc.)
}

Source : ISO/IEC 14496-3:2009, Table 1.13 - "Syntax of AudioSpecificConfig()"

Mapping Audio Object Type -> Profil AAC

D'apres ISO/IEC 14496-3, Table 1.16 - "Audio Object Type definition" :

/// Audio Object Type selon ISO/IEC 14496-3, Table 1.16
enum AudioObjectType: UInt8 {
    case aacMain = 1                    // AAC Main Profile (rarement utilise)
    case aacLC = 2                      // AAC Low Complexity (le plus courant)
    case aacSSR = 3                     // AAC Scalable Sample Rate (obsolete)
    case aacLTP = 4                     // AAC Long Term Prediction
    case heAAC_SBR = 5                  // HE-AAC (SBR) - aussi appele AAC+
    case aacScalable = 6                // AAC Scalable

    case twinVQ = 7                     // TwinVQ
    case celp = 8                       // CELP
    case hxvc = 9                       // HVXC

    case ttsi = 12                      // TTSI (Text-To-Speech Interface)
    case mainSynthetic = 13             // Main Synthetic
    case wavetableSynthesis = 14        // Wavetable Synthesis
    case generalMIDI = 15               // General MIDI

    case algorithmicSynthesis = 16      // Algorithmic Synthesis and Audio FX
    case erAacLC = 17                   // ER AAC LC (Error Resilient)

    case erAacLTP = 19                  // ER AAC LTP
    case erAacScalable = 20             // ER AAC Scalable
    case erTwinVQ = 21                  // ER TwinVQ
    case erBSAC = 22                    // ER BSAC (Bit-Sliced Arithmetic Coding)
    case erAacLD = 23                   // ER AAC LD (Low Delay)

    case erCELP = 24                    // ER CELP
    case erHVXC = 25                    // ER HVXC
    case erHILN = 26                    // ER HILN
    case erParametric = 27              // ER Parametric

    case ssc = 28                       // SSC (SinuSoidal Coding)
    case heAAC_PS = 29                  // HE-AAC v2 (PS) - aussi appele AAC+ v2
    case mpegSurround = 30              // MPEG Surround

    case layer1 = 32                    // Layer-1
    case layer2 = 33                    // Layer-2
    case layer3 = 34                    // Layer-3 (MP3)

    case dsr = 35                       // DSR (Direct Stream Transfer)
    case als = 36                       // ALS (Audio Lossless Coding)
    case sls = 37                       // SLS (Scalable Lossless Coding)
    case sslsNonCore = 38               // SLS non-core
    case aacELD = 39                    // AAC ELD (Enhanced Low Delay)

    case smrSimple = 40                 // SMR Simple
    case smrMain = 41                   // SMR Main

    case usacNoSBR = 42                 // USAC (Unified Speech and Audio Coding) sans SBR
    case saoc = 43                      // SAOC (Spatial Audio Object Coding)
    case ldMpegSurround = 44            // LD MPEG Surround
    case usac = 45                      // USAC (avec SBR)
}

extension AudioObjectType {
    var profileDescription: String {
        switch self {
        case .aacLC: return "AAC-LC (Low Complexity)"
        case .heAAC_SBR: return "HE-AAC (High Efficiency AAC with SBR)"
        case .heAAC_PS: return "HE-AAC v2 (with PS and SBR)"
        case .aacELD: return "AAC-ELD (Enhanced Low Delay)"
        default: return "AOT \(self.rawValue)"
        }
    }
}

Sampling Frequency Index Table

ISO/IEC 14496-3, Table 1.18 - "Sampling Frequency Index" :

/// Table de correspondance samplingFrequencyIndex -> frequence
/// Source : ISO/IEC 14496-3:2009, Table 1.18
func samplingFrequency(from index: UInt8) -> UInt32? {
    let samplingFrequencies: [UInt32] = [
        96000,  // 0x0
        88200,  // 0x1
        64000,  // 0x2
        48000,  // 0x3
        44100,  // 0x4
        32000,  // 0x5
        24000,  // 0x6
        22050,  // 0x7
        16000,  // 0x8
        12000,  // 0x9
        11025,  // 0xA
        8000,   // 0xB
        7350    // 0xC
        // 0xD, 0xE : reserved
        // 0xF : explicit frequency (24-bit field follows)
    ]

    guard index < samplingFrequencies.count else {
        return nil
    }

    return samplingFrequencies[Int(index)]
}

Channel Configuration

ISO/IEC 14496-3, Table 1.19 - "Channel Configuration" :

/// Configuration des canaux selon ISO/IEC 14496-3, Table 1.19
func channelDescription(from config: UInt8) -> String {
    switch config {
    case 0: return "Defined in AOT Specific Config"
    case 1: return "1 channel: front-center (Mono)"
    case 2: return "2 channels: front-left, front-right (Stereo)"
    case 3: return "3 channels: front-center, front-left, front-right"
    case 4: return "4 channels: front-center, front-left, front-right, back-center"
    case 5: return "5 channels: front-center, front-left, front-right, back-left, back-right"
    case 6: return "6 channels: front-center, front-left, front-right, back-left, back-right, LFE"
    case 7: return "8 channels: front-center, front-left, front-right, side-left, side-right, back-left, back-right, LFE"
    default: return "Reserved or invalid (\(config))"
    }
}

Apple fournit l'API CMAudioFormatDescriptionGetMagicCookie dans le framework CoreMedia pour extraire le magic cookie d'un format audio.

import AVFoundation
import CoreMedia

/// Extrait le magic cookie AAC d'un fichier audio
/// - Parameter url: URL du fichier audio (M4A, MP4, AAC, etc.)
/// - Returns: Data contenant le magic cookie ou nil si non disponible
func extractMagicCookie(from url: URL) async throws -> Data? {
    let asset = AVAsset(url: url)

    // Recuperer la piste audio (API async moderne)
    let audioTracks = try await asset.loadTracks(withMediaType: .audio)
    guard let audioTrack = audioTracks.first else {
        print("Aucune piste audio trouvee")
        return nil
    }

    // Obtenir les format descriptions
    let formatDescriptions = try await audioTrack.load(.formatDescriptions)
    guard let formatDescription = formatDescriptions.first else {
        print("Aucun format description disponible")
        return nil
    }

    // Extraire le magic cookie via l'API Core Media
    var cookieSize: Int = 0
    guard let magicCookiePointer = CMAudioFormatDescriptionGetMagicCookie(
        formatDescription,
        sizeOut: &cookieSize
    ) else {
        print("Pas de magic cookie (format PCM ?)")
        return nil
    }

    // Convertir en Data Swift
    let cookieData = Data(bytes: magicCookiePointer, count: cookieSize)

    print("Magic cookie extrait : \(cookieSize) octets")
    return cookieData
}

Note : Le code utilise les APIs async/await modernes (loadTracks, load(.formatDescriptions)) au lieu des APIs synchrones deprecees.

Documentation :

  • CMAudioFormatDescriptionGetMagicCookie - CoreMedia framework
  • AVAssetTrack - AVFoundation framework

Le magic cookie AAC peut se presenter sous deux formes :

1. Format direct : AudioSpecificConfig seul (rare)

[Octet 0] [Octet 1] [Octet 2...]
    └─────┴─────────┴──────────────> AudioSpecificConfig direct

2. Format ESDS : Elementary Stream Descriptor (courant dans MP4/M4A)

Le format ESDS est defini dans ISO/IEC 14496-1:2004, Section 8.6.6 - "Elementary Stream Descriptor"

Structure hierarchique :

ESDS (Elementary Stream Descriptor)
│
+-- ES_Descriptor [tag 0x03]
    +-- ES_ID (2 bytes)
    +-- flags (1 byte)
    │
    +-- DecoderConfigDescriptor [tag 0x04]
        +-- objectTypeIndication (1 byte) = 0x40 pour MPEG-4 Audio
        +-- streamType (1 byte)
        +-- bufferSizeDB (3 bytes)
        +-- maxBitrate (4 bytes)
        +-- avgBitrate (4 bytes)
        │
        +-- DecoderSpecificInfo [tag 0x05]
            +-- AudioSpecificConfig <-- Notre cible !
                +-- audioObjectType (5 bits)
                +-- samplingFrequencyIndex (4 bits)
                +-- channelConfiguration (4 bits)
                +-- ... (extensions)

Probleme pratique : Les encodeurs peuvent ajouter du padding ou des donnees supplementaires, rendant la position exacte de l'AudioSpecificConfig variable.


Partie 3 : Parsing bit-a-bit de l'AudioSpecificConfig

Implementation conforme ISO/IEC 14496-3

Voici l'implementation complete du parser AudioSpecificConfig :

/// Informations extraites de l'AudioSpecificConfig
/// Conforme a ISO/IEC 14496-3:2009, Section 1.6.2.1
struct AudioSpecificConfigInfo: CustomStringConvertible {
    let audioObjectType: UInt8
    let samplingFrequencyIndex: UInt8
    let samplingFrequency: UInt32?
    let channelConfiguration: UInt8
    let extensions: [String]
    let rawData: Data

    var profile: AACProfile {
        switch audioObjectType {
        case 2:  return .LC
        case 5:  return .HE
        case 29: return .HEv2
        case 23: return .LD
        case 39: return .ELD
        default: return .unknown
        }
    }

    var description: String {
        """
        +-- AudioSpecificConfig (ISO/IEC 14496-3) ---------+
        | Audio Object Type    : \(audioObjectType) (\(profile.description))
        | Sampling Freq Index  : \(samplingFrequencyIndex)\(samplingFrequency.map { "\n| Sampling Frequency   : \($0) Hz" } ?? "")
        | Channel Config       : \(channelConfiguration) (\(channelDescription(from: channelConfiguration)))
        \(extensions.isEmpty ? "" : "| Extensions           : \(extensions.joined(separator: ", "))\n")+--------------------------------------------------+
        """
    }
}

/// Parser AudioSpecificConfig selon ISO/IEC 14496-3, Section 1.6.2.1
func parseAudioSpecificConfig(_ data: Data) -> AudioSpecificConfigInfo? {
    guard data.count >= 2 else {
        print("Magic cookie trop court (< 2 octets)")
        return nil
    }

    let firstByte = data[0]
    let secondByte = data[1]

    // ---------------------------------------------------------
    // Extraction des champs selon ISO/IEC 14496-3, Section 1.6.2.1
    // ---------------------------------------------------------

    // audioObjectType : bits [0-4] du premier octet (5 bits)
    // Lecture : (firstByte >> 3) & 0x1F
    let audioObjectType = (firstByte >> 3) & 0x1F

    // samplingFrequencyIndex : bits [5-7] du premier octet + bit [0] du second (4 bits)
    // Lecture : ((firstByte & 0x07) << 1) | ((secondByte >> 7) & 0x01)
    let samplingFreqIndex = ((firstByte & 0x07) << 1) | ((secondByte >> 7) & 0x01)

    // channelConfiguration : bits [1-4] du second octet (4 bits)
    // Lecture : (secondByte >> 3) & 0x0F
    let channelConfig = (secondByte >> 3) & 0x0F

    // Resolution de la frequence d'echantillonnage
    let samplingFreq: UInt32?
    if samplingFreqIndex == 0xF {
        // Frequence explicite sur 24 bits (rarement utilise)
        if data.count >= 6 {
            let freq = (UInt32(data[2]) << 16) | (UInt32(data[3]) << 8) | UInt32(data[4])
            samplingFreq = freq
        } else {
            samplingFreq = nil
        }
    } else {
        // Lookup dans la table ISO/IEC 14496-3, Table 1.18
        samplingFreq = samplingFrequency(from: samplingFreqIndex)
    }

    // Detection des extensions (SBR, PS, MPEG Surround)
    let extensions = detectExtensions(
        in: data,
        audioObjectType: audioObjectType
    )

    return AudioSpecificConfigInfo(
        audioObjectType: audioObjectType,
        samplingFrequencyIndex: samplingFreqIndex,
        samplingFrequency: samplingFreq,
        channelConfiguration: channelConfig,
        extensions: extensions,
        rawData: data
    )
}

enum AACProfile: String, CustomStringConvertible {
    case LC = "AAC-LC"
    case HE = "HE-AAC"
    case HEv2 = "HE-AAC v2"
    case LD = "AAC-LD"
    case ELD = "AAC-ELD"
    case unknown = "Unknown"

    var description: String { rawValue }
}

Visualisation du parsing bit-a-bit

Prenons un exemple concret de magic cookie AAC-LC :

Hex dump : 11 90 56 E5 00
Binary   : 00010001 10010000 01010110 11100101 00000000
           └──┬───┘ └──┬───┘
              │        │
           Octet 0  Octet 1

Parsing :
+-----------------------------------------------------------+
| Octet 0 : 0x11 = 0b00010001                               |
|   audioObjectType      = [0-4]  = 0b00010 = 2 (AAC-LC)    |
|   samplingFreqIndex_hi = [5-7]  = 0b001                   |
|                                                           |
| Octet 1 : 0x90 = 0b10010000                               |
|   samplingFreqIndex_lo = [0]    = 0b1                     |
|   samplingFreqIndex    = 0b0011 = 3 (48000 Hz)            |
|   channelConfiguration = [1-4]  = 0b0010 = 2 (Stereo)    |
|   frameLengthFlag      = [5]    = 0                       |
|   dependsOnCoreCoder   = [6]    = 0                       |
|   extensionFlag        = [7]    = 1 <-- Extensions ?      |
+-----------------------------------------------------------+

Note importante : Dans cet exemple, l'audioObjectType initial est 2 (AAC-LC), mais les extensions suivantes peuvent indiquer HE-AAC. C'est pourquoi il est crucial de parser les extensions !


Partie 4 : Detection des extensions (SBR, PS)

Extensions AAC selon ISO/IEC 14496-3

Les extensions comme SBR (Spectral Band Replication) et PS (Parametric Stereo) transforment un profil AAC-LC en HE-AAC :

  • AAC-LC = Audio Object Type 2
  • AAC-LC + SBR = HE-AAC (AOT 5)
  • AAC-LC + SBR + PS = HE-AAC v2 (AOT 29)

Les extensions sont signalees par des sync words definis dans la norme :

Extension Sync Word Reference ISO/IEC 14496-3
SBR (Spectral Band Replication) 0x2B7 Section 4.6.18.3
PS (Parametric Stereo) 0x548 Section 4.6.20.3
MPEG Surround 0x2E5 Section 4.6.15

Implementation de la detection des extensions

/// Detecte les extensions AAC (SBR, PS, MPEG Surround)
/// selon ISO/IEC 14496-3, Sections 4.6.18 et 4.6.20
func detectExtensions(in data: Data, audioObjectType: UInt8) -> [String] {
    var extensions: [String] = []

    // Sync words definis dans ISO/IEC 14496-3
    let sbrSyncWord: UInt16 = 0x2B7    // Section 4.6.18.3
    let psSyncWord: UInt16 = 0x548     // Section 4.6.20.3
    let mpegSurroundSyncWord: UInt16 = 0x2E5

    // Scan du bitstream a la recherche des sync words
    // Note : Les sync words peuvent apparaitre n'importe ou apres l'ASC de base
    for i in 2..<(data.count - 1) {
        let syncWord = (UInt16(data[i]) << 8) | UInt16(data[i + 1])

        switch syncWord {
        case sbrSyncWord:
            if !extensions.contains("SBR") {
                extensions.append("SBR (Spectral Band Replication)")
            }

        case psSyncWord:
            if !extensions.contains("PS") {
                extensions.append("PS (Parametric Stereo)")
            }

        case mpegSurroundSyncWord:
            if !extensions.contains("MPEG Surround") {
                extensions.append("MPEG Surround")
            }

        default:
            break
        }
    }

    // Verification de coherence avec l'audioObjectType
    if audioObjectType == 5 && !extensions.contains("SBR") {
        print("Warning: AOT=5 (HE-AAC) mais sync word SBR non trouve")
    }

    if audioObjectType == 29 && (!extensions.contains("SBR") || !extensions.contains("PS")) {
        print("Warning: AOT=29 (HE-AAC v2) mais extensions SBR/PS non trouvees")
    }

    return extensions
}

Detection PS : Limitation importante

Avertissement : La detection du Parametric Stereo est simplifiee dans l'implementation actuelle.

// DETECTION SIMPLIFIEE (implementation actuelle)
// Cette approche verifie uniquement le bit indicateur simplifie
func detectPS_Simplified(in data: Data) -> Bool {
    // ISO/IEC 14496-3, Section 4.6.20 definit une structure complexe
    // Cette implementation utilise une heuristique simplifiee
    if data.count >= 3 {
        let thirdByte = data[2]
        return (thirdByte & 0x80) != 0  // Bit flag simplifie
    }
    return false
}

// Note : Une detection complete necessite un parser bitstream complet
// pour gerer les structures de taille variable definies dans
// ISO/IEC 14496-3, Section 4.6.20 - "Parametric Stereo"

Limitation connue : Cette heuristique peut manquer certaines configurations PS legitimes ou produire des faux positifs.

Nuance importante : AOT 29 != toujours HE-AAC v2

Point technique critique : L'Audio Object Type 29 peut indiquer HE-AAC v1 OU HE-AAC v2 selon la presence effective du Parametric Stereo :

// Implementation correcte : verifier la presence PS
func determineDetailedAACProfile(audioObjectType: UInt8, cookieData: Data) -> AACProfile {
    switch audioObjectType {
    case 2:
        return .LC
    case 5:
        return .HE  // HE-AAC v1 (SBR seulement)
    case 29:
        // AOT 29 peut etre HE-AAC v1 ou v2 selon presence PS
        let hasPS = detectPS_Simplified(in: cookieData)
        if hasPS {
            return .HEv2  // HE-AAC v2 (SBR + PS)
        } else {
            return .HE    // HE-AAC v1 (SBR seulement, malgre AOT 29)
        }
    case 39:
        return .ELD
    default:
        return .unknown
    }
}

Reference : ISO/IEC 14496-3:2009, Section 4.6.20


Partie 5 : Extraction de l'AudioSpecificConfig depuis ESDS

Dans les fichiers MP4/M4A, l'AudioSpecificConfig est encapsule dans une structure ESDS (Elementary Stream Descriptor) definie par ISO/IEC 14496-1.

Implementation du parser ESDS avec scoring heuristique

Note technique : L'implementation utilise un systeme de scoring pour gerer les variations de format ESDS produites par differents encodeurs.

/// Extrait l'AudioSpecificConfig d'un magic cookie au format ESDS
/// Utilise un scoring heuristique pour gerer les variations de format
func extractAudioSpecificConfigFromESDS(_ cookieData: Data) -> Data? {
    // Verification format ESDS (commence par tag 0x03)
    guard cookieData.count > 10 && cookieData[0] == 0x03 else {
        // Pas un format ESDS, tenter parsing direct
        return tryDirectASC(cookieData)
    }

    // Scan heuristique pour trouver l'AudioSpecificConfig
    var candidates: [(data: Data, score: Int, debug: String)] = []

    for i in 0..<(cookieData.count - 1) {
        let firstByte = cookieData[i]
        let secondByte = cookieData[i + 1]

        let audioObjectType = (firstByte >> 3) & 0x1F
        let samplingFreqIndex = ((firstByte & 0x07) << 1) | ((secondByte >> 7) & 0x01)
        let channelConfig = (secondByte >> 3) & 0x0F

        // Verifier si ca ressemble a un AudioSpecificConfig valide
        if audioObjectType >= 1 && audioObjectType <= 42 &&
           samplingFreqIndex <= 12 && channelConfig <= 7 {

            // Ignorer les patterns de padding ESDS
            if firstByte == 0x80 && (secondByte == 0x80 || secondByte == 0x22) {
                continue
            }

            // Scoring base sur la probabilite
            var score = 0

            // Preferer les profils courants
            if audioObjectType == 2 { score += 10 }  // AAC-LC tres courant
            if audioObjectType == 5 { score += 8 }   // HE-AAC courant
            if audioObjectType == 29 { score += 8 }  // HE-AAC v2 courant

            // Preferer les sample rates courants
            if samplingFreqIndex == 3 || samplingFreqIndex == 4 { score += 5 }  // 48/44.1 kHz

            // Preferer mono/stereo
            if channelConfig == 1 || channelConfig == 2 { score += 5 }

            let candidate = (
                data: cookieData.subdata(in: i..<min(i + 6, cookieData.count)),
                score: score,
                debug: "AOT:\(audioObjectType) SR:\(samplingFreqIndex) CH:\(channelConfig)"
            )
            candidates.append(candidate)
        }
    }

    // Retourner le candidat avec le meilleur score
    guard let best = candidates.max(by: { $0.score < $1.score }) else {
        return nil
    }

    return Data(best.data.prefix(2))
}

/// Fallback : Tenter de parser directement comme AudioSpecificConfig
private func tryDirectASC(_ data: Data) -> Data? {
    guard data.count >= 2 else { return nil }

    let audioObjectType = (data[0] >> 3) & 0x1F

    // Valider que l'AOT est dans une plage raisonnable (1-42)
    if audioObjectType >= 1 && audioObjectType <= 42 {
        return data
    }

    return nil
}

Limitation du scoring heuristique :

  • Assume que les configurations courantes (44.1/48 kHz, stereo) sont correctes
  • Peut echouer sur configurations inhabituelles (96 kHz, 7.1 channels)
  • Un parser ESDS strict selon ISO/IEC 14496-1 serait plus robuste

Partie 6 : Validation du profil encode

Cas d'usage : Verification post-encodage

enum ProfileValidationResult {
    case match(AACProfile, details: AudioSpecificConfigInfo)
    case mismatch(requested: AACProfile, actual: AACProfile, details: AudioSpecificConfigInfo)
    case cannotVerify(reason: String)
}

/// Valide que le profil AAC demande correspond au profil reellement encode
func validateEncodedProfile(
    requestedProfile: AACProfile,
    encodedFileURL: URL
) async -> ProfileValidationResult {

    // Etape 1 : Extraire le magic cookie
    guard let cookieData = try? await extractMagicCookie(from: encodedFileURL) else {
        return .cannotVerify(reason: "Impossible d'extraire le magic cookie")
    }

    // Etape 2 : Extraire l'ASC (gerer format ESDS ou direct)
    let ascData: Data
    if cookieData.count > 2 && cookieData[0] == 0x03 {
        guard let extracted = extractAudioSpecificConfigFromESDS(cookieData) else {
            return .cannotVerify(reason: "Impossible de parser l'ESDS")
        }
        ascData = extracted
    } else {
        ascData = cookieData
    }

    // Etape 3 : Parser l'AudioSpecificConfig
    guard let ascInfo = parseAudioSpecificConfig(ascData) else {
        return .cannotVerify(reason: "Impossible de parser l'AudioSpecificConfig")
    }

    let actualProfile = ascInfo.profile

    // Etape 4 : Comparer les profils
    if actualProfile == requestedProfile {
        return .match(actualProfile, details: ascInfo)
    } else {
        return .mismatch(
            requested: requestedProfile,
            actual: actualProfile,
            details: ascInfo
        )
    }
}

Diagnostic des raisons de fallback

/// Diagnostic des raisons de fallback
func diagnoseFallbackReason(
    requested: AACProfile,
    actual: AACProfile,
    ascInfo: AudioSpecificConfigInfo
) -> String {

    // HE-AAC v2 -> AAC-LC : Limitation AVAssetWriter
    if requested == .HEv2 && actual == .LC {
        return """
        AVAssetWriter ne supporte pas HE-AAC v2 de maniere fiable.
        Solution : Utiliser AudioToolbox avec ExtAudioFile API.
        """
    }

    // HE-AAC -> AAC-LC : Probleme de sample rate
    if requested == .HE && actual == .LC {
        if let freq = ascInfo.samplingFrequency, freq < 32000 {
            return """
            Frequence d'echantillonnage trop basse pour HE-AAC (\(freq) Hz).
            HE-AAC necessite generalement >= 32 kHz.
            Les sample rates problematiques : 22050, 11025, 8000 Hz
            """
        }
        return "Encodeur ne supporte pas HE-AAC pour cette configuration."
    }

    return "Fallback non documente vers \(actual)."
}

Partie 7 : Application pratique - Selection d'engine de conversion

Architecture dual-engine

Le projet audio-converter-cli utilise deux engines de conversion :

  1. AVFoundation (AVAssetWriter) : Mature, large support, mais limitations HE-AAC
  2. AudioToolbox (ExtAudioFile) : Support complet HE-AAC v2, FLAC

Selection intelligente d'engine

// Sources/ConversionRouter.swift

/// Determine l'engine optimal pour les settings demandes
func selectEngine(for settings: AudioSettings, sourceInfo: AudioFileInfo) -> AudioConversionEngine {

    // Cas necessitant AudioToolbox obligatoirement
    if settings.requiresAudioToolbox {
        return AudioToolboxConverterEngine()
    }

    // Par defaut, utiliser AVFoundation (plus mature)
    return AVFoundationConverterEngine()
}

extension AudioSettings {
    /// Determine si AudioToolbox est requis pour ces settings
    var requiresAudioToolbox: Bool {
        switch formatSettings {
        case .aac(let aacSettings):
            // HE-AAC v2 necessite AudioToolbox
            // AVAssetWriter produit silencieusement AAC-LC
            return aacSettings.profile == .HEv2

        case .flac:
            // AVAssetWriter ne supporte pas FLAC nativement
            return true

        default:
            return false
        }
    }
}

Point important documente dans le projet :

CRITICAL: HE-AAC v2 requires AudioToolbox engine (AVFoundation silently falls back to AAC-LC)

Workflow complet avec validation

/// Conversion avec validation automatique du profil
func convertWithValidation(
    input: URL,
    output: URL,
    settings: AudioSettings
) async throws {

    // Etape 1 : Selection de l'engine
    let sourceInfo = try await AudioAnalyzer.analyzeSource(from: input)
    let engine = ConversionRouter.shared.selectEngine(for: settings, sourceInfo: sourceInfo)

    // Etape 2 : Conversion
    try await engine.convert(input: input, output: output, settings: settings)

    // Etape 3 : Validation post-encodage (AAC uniquement)
    if case .aac(let aacSettings) = settings.formatSettings {
        let result = await validateEncodedProfile(
            requestedProfile: aacSettings.profile.toAACProfile(),
            encodedFileURL: output
        )

        switch result {
        case .match(let profile, _):
            print("Profil AAC valide : \(profile)")

        case .mismatch(let requested, let actual, _):
            print("Warning: Fallback detecte : \(requested) -> \(actual)")
            // Le ConversionRouter aurait du prevenir ce cas
            // Si on arrive ici, c'est un bug dans la selection d'engine

        case .cannotVerify(let reason):
            print("Warning: Validation impossible : \(reason)")
        }
    }
}

Partie 8 : Limitations et ameliorations futures

1. Code duplique dans le projet (corrige)

Observation lors de la review : Le code de parsing AAC etait duplique entre AudioConverter.swift et AudioAnalyzer.swift (~330 lignes).

Impact :

  • Violation du principe DRY
  • Risque d'incohérence entre les deux implementations
  • Double maintenance

Action : Consolider dans AudioAnalyzer.swift uniquement.

2. Detection PS simplifiee

Probleme : La detection du Parametric Stereo utilise une heuristique simplifiee.

Limitation : Peut manquer certaines configurations PS legitimes.

Solution ideale : Parser la structure GASpecificConfig et PSExtension selon ISO/IEC 14496-3, Section 4.6.20.

3. Scoring heuristique pour ESDS

Probleme : Le code utilise un systeme de scoring pour trouver l'AudioSpecificConfig dans l'ESDS.

Limitations :

  • Assume que les configurations courantes sont correctes
  • Peut echouer sur configurations inhabituelles (96 kHz, 7.1 channels)
  • Aucun seuil de confiance minimum

Amelioration proposee : Parser ESDS strict selon ISO/IEC 14496-1 au lieu d'un scan heuristique.

4. Sample rates problematiques pour HE-AAC

Certains sample rates causent des problemes avec HE-AAC sur AVFoundation :

let problematicSampleRates: [Double] = [22050, 11025, 8000]

// Validation dans AudioValidator.swift
if problematicSampleRates.contains(sampleRate) &&
   [.HE, .HEv2].contains(aacProfile) {
    // Warning : considerer 44100 ou 48000 Hz
}

Conclusion

Pourquoi ce parsing est necessaire

Constat technique : Core Audio sur macOS/iOS ne fournit aucune API publique pour verifier le profil AAC reellement encode. Les methodes disponibles :

  1. CMAudioFormatDescription : Indique le format conteneur, pas le profil bitstream reel
  2. AVAssetTrack.formatDescriptions : Peut indiquer profil demande, pas profil produit
  3. AudioFileGetProperty : Retourne metadata du conteneur, pas analyse bitstream

Solution unique : Parser manuellement l'AudioSpecificConfig selon ISO/IEC 14496-3.

Ce que nous avons appris

  1. Structure normative : AudioSpecificConfig defini dans ISO/IEC 14496-3, Section 1.6.2.1
  2. Extensions AAC : SBR et PS transforment AAC-LC en HE-AAC via sync words
  3. Format ESDS : Magic cookie AAC encapsule dans Elementary Stream Descriptor
  4. Fallback silencieux : AVAssetWriter produit AAC-LC sans avertissement pour HE-AAC v2
  5. AOT 29 nuance : Peut etre HE-AAC v1 ou v2 selon presence effective du PS
  6. Validation necessaire : Seul moyen fiable = parsing post-encodage

Applications pratiques

  • Validation qualite : Confirmer profil demande vs produit
  • Selection d'engine : Choisir AudioToolbox si AVFoundation ne supporte pas
  • Detection de fallback : Identifier degradation de profil
  • Reporting : Verifier presence extensions SBR/PS
  • Debugging : Comprendre qualite audio insuffisante

Ressources

Normes ISO/IEC

  • ISO/IEC 14496-3:2009 - MPEG-4 Audio (profils AAC, AudioSpecificConfig)
  • ISO/IEC 14496-1:2004 - MPEG-4 Systems (structure ESDS)

Documentation Apple

  • Technical Note TN2236 - High-Efficiency Advanced Audio Coding
  • Technical Note TN2237 - Audio Export - Encoding AAC Audio
  • Technical Note TN2258 - AAC Audio - Encoder Delay
  • CMAudioFormatDescriptionGetMagicCookie - Core Media API
  • Core Audio Overview - Architecture Core Audio

Ressources complementaires

  • Advanced Audio Coding (Wikipedia) - Historique variations AAC
  • MPEG-4 Part 3 (Wikipedia) - Vue d'ensemble MPEG-4 Audio

A propos

Cet article est base sur l'implementation reelle du projet audio-converter-cli, convertisseur audio haute performance pour macOS.

Auteur : Matthieu Zisswiller
Projet : audio-converter-cli


Article technique redige en 2025. APIs Core Audio et normes ISO/IEC a jour a publication.