Validation des profils AAC via AudioSpecificConfig : Parsing du Magic Cookie en Swift
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))"
}
}
Partie 2 : Extraction du Magic Cookie avec Core Audio
Localisation du magic cookie dans un fichier audio
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
Format du magic cookie : ESDS vs AudioSpecificConfig direct
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
Probleme : Magic cookie au format 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 :
- AVFoundation (AVAssetWriter) : Mature, large support, mais limitations HE-AAC
- 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 :
- CMAudioFormatDescription : Indique le format conteneur, pas le profil bitstream reel
- AVAssetTrack.formatDescriptions : Peut indiquer profil demande, pas profil produit
- AudioFileGetProperty : Retourne metadata du conteneur, pas analyse bitstream
Solution unique : Parser manuellement l'AudioSpecificConfig selon ISO/IEC 14496-3.
Ce que nous avons appris
- Structure normative : AudioSpecificConfig defini dans ISO/IEC 14496-3, Section 1.6.2.1
- Extensions AAC : SBR et PS transforment AAC-LC en HE-AAC via sync words
- Format ESDS : Magic cookie AAC encapsule dans Elementary Stream Descriptor
- Fallback silencieux : AVAssetWriter produit AAC-LC sans avertissement pour HE-AAC v2
- AOT 29 nuance : Peut etre HE-AAC v1 ou v2 selon presence effective du PS
- 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.