Chapitre -114 · Sound & CFTypeRef

L'Audio et l'audio spacial

Nous voulons construire une classe qui va charger, ranger et jouer nos assets audio (.m4a, .mp3, …) pour notre application. Dans un premier temps on ajoutera ce dont on a besoin, ensuite nous l'adapterons pour C++ grâce à CFTypeRef.

.m4a est un format conteneur audio MPEG-4 (sans vidéo) souvent utilisé par Apple (iTunes/iPhone) pour la musique et les podcasts, encodé en AAC ou ALAC. Il offre une meilleure qualité sonore que le MP3 à débit binaire égal et n'est généralement pas protégé par DRM (Digital Rights Management - Gestion des droits numériques → .m4p). Il est compatible avec VLC, QuickTime, iTunes. Nous en produirons ensemble sur "GarageBand" mais aussi pour créer de l'audio spatial pour tes rendus les plus compatibles dans "Logic Pro X".

Pour ce faire on utilisera le langage Objective-C++ (combiné de Objective-C & C++) ainsi que le Framework Apple (pas de wrapper cpp disponible) PHASE (Physical Audio Spatialization Engine).

Ajoute-le : dans Xcode clique sur : MSLearn.xcodeproj (le premier), dans "Targets" → "LearnMSL MacOS" → Build Phases → "Link Binary With Libraries" → + → Apple SDKs → "PHASE.framework" & ajoute aussi "AVFAudio.framework"

Apprendre de toi-même :

C'est souvent une question de documentation, si tu veux réaliser quelque chose, tu te documentes. D'ailleurs si toi aussi tu crées un programme public, il sera préférable que tu ajoutes un fichier 'README.md' contenant l'explication, …

La classe en Objective-C

Objective-C est le langage natif d'Apple. Pas de class, pas de :: — une classe se déclare avec @interface et s'implémente avec @implementation. Les méthodes commencent par - (instance) ou + (classe). C'est ce que tu lis dans la documentation Apple.

Présente aussi pour comparer avec notre adaptation Objective-C++/C++ que l'on utilise dans nos projets, je démontre d'abord la consultation de la documentation (non-disponible en C++) puis l'équivalent C++, ensuite on adaptera encore pour l'ARC.

/// Objective-C — Exemple — Audio.h — update:18/05/26
#import <PHASE/PHASE.h>

@interface Audio : NSObject
// - désigne une méthode d'instance  (+ désignerait une méthode de classe)
- (instancetype)initWithResourcePath:(NSString*)resourcePath; // le chemin de nos fichiers .m4a

@end
/// Objective-C++ — Exemple — Audio.h — update:18/05/26
// Nous allons commencer à écrire notre propre classe dédiée comme suit :
#ifndef AUDIO_H
#define AUDIO_H

#include <string>           // std::string

#import <PHASE/PHASE.h>     // le framework Apple

#include "NonCopyable.h"    // une de nos classes (vue dans ch.canonique)

class Audio : public NonCopyable
{
public:
    Audio(const std::string& resourcePath); // le chemin de nos fichiers .m4a
    ~Audio();

Ouvre la documentation, source de vérité pour tout framework Apple, dense mais structurée : ici. Nous avons toujours accès aux classes Apple avec clic droit sur la 'classe'. Il existe un PDF en ligne sur les spécifications de l'API Metal.

Le menu de droite (sidebar) est organisé par groupes thématiques : Setup, Assets, Events, Mixers… C'est ta boussole. Quand tu cherches une fonctionnalité, commence par le groupe, pas par chercher un nom exact. [C]class [E]enumerator [P]protocol…

pourquoi adapter ensuite pour C++
Nous ne pouvons pas déclarer d'objet Obj-C dans notre fichier header .hpp car le C++ en hériterait, ni écrire les prototypes dans le fichier à l'extension .mm mais dans le cas d'une analyse propre et d'un projet Obj-C++ uniquement, ceci fonctionnerait. (notre classe compatible C++ vers le bas de page, of course)

C'est une classe, on fait comme d'habitude :

/// Objective-C++ — Exemple — Audio.h — update:18/05/26
private:
    PHASEEngine* m_engine; // l'objet nécessaire à notre tâche
};

#endif // AUDIO_H

Il est dit dans PHASE > Setup, avec un exemple, pour créer un objet moteur il faut l'initialiser avec une valeur pour l'argv/paramètre de la méthode d'instance 'initWithUpdateMode:' qui sélectionne le contrôle et le temps de lecture.

Il faut aussi le démarrer (pas de shader ici, on communique avec les haut-parleurs via notre application) et l'arrêter (lorsque l'application se fermera ou que la classe sera détruite si l'on a décidé de ne plus en avoir besoin après un certain temps d'exécution).

Nous savons également qu'il faut allouer de la mémoire pour chaque objet → via les méthodes [[alloc]] mise en place par Apple.

Nous descendons vers PHASEEngine > Topics. Deux modes pour la [M]méthode d'initialisation sont trouvés dans la documentation dont 'PHASEUpdateModeAutomatic' : lire des sons sans avoir à maintenir une boucle de mise à jour stricte.

Signature de méthode (paramètre de fonction) → lorsque tu vois 'NSError**' en dernier, c'est un signe qu'Apple veut que tu gères les erreurs proprement. La méthode retourne nil en cas d'échec — vérifie toujours le retour, puis l'erreur.
/// Objective-C — Exemple — Audio.m — update:18/05/26
#import "Audio.h"

@implementation Audio
{
    PHASEEngine* _engine; // membre privé — ARC gère la durée de vie
}

- (instancetype)initWithResourcePath:(NSString*)resourcePath
{
    self = [super init];
    if (self)
    {
        _engine = [[PHASEEngine alloc] initWithUpdateMode:PHASEUpdateModeAutomatic];

        NSError* error = nil;
        if (![_engine startAndReturnError:&error])
        {
            NSLog(@"Error starting PHASE Engine: %@", error.localizedDescription);
        }
    }
    return self;
}

- (void)dealloc // équivalent du destructeur
{
    [_engine stop];
}
/// Objective-C++ — Exemple — Audio.mm — update:18/05/26
#include "Audio.h"

Audio::Audio(const std::string& resourcePath)
{   // appel objet Objective-C → 1ière étape : alloc + init
    m_engine = [[PHASEEngine alloc] initWithUpdateMode:PHASEUpdateModeAutomatic];
    
    NSError __autoreleasing* error = nil; // juste une classe qui gère les erreurs
    
    // [m_engine startAndReturnError:&error]; → 2ième étape : démarrer le moteur
    if (![m_engine startAndReturnError:&error])
    {   // gestion de l'erreur à notre sauce
        NSLog(@"Error starting PHASE Engine: %@", error.localizedDescription);
        assert(false);
    }
}

Audio::~Audio()
{   // 3ième et dernière étape : arrêter le moteur
    [m_engine stop];
}

Dans la sidebar et/ou la doc du SDK, on descend en recherchant ce que l'on souhaite d'autre de ce framework, on décide également de créer une fonction pour allouer ces classes :

PHASESoundAsset[C] regroupe les données audio source. Le framework nécessite un mélangeur pour lire un actif sonore (comme PHASESamplerNodeDefinition), et les nœuds d'événements sonores combinent l'actif avec un mélangeur. L'actif sonore à un nœud d'événement sonore fourni par l'identifiant passé dans la fonction registerSoundAssetAtURL:identifier:assetType:channelLayout:normalizationMode:error:

La fonction commence par la copie de la classe NSError.

Tout ce que nous voulons c'est PHASESoundAsset, mais la classe nécessite une NSURL, qui elle même nécessite une NSString, tout comme elle nécessite la gestion d'une erreur potentielle, le nom de ton asset individuelle qu'on enverra via l'appel de cette fonction, l'engine, le type d'asset ou pour finir, le mode de normalization.

Ensuite on vérifie que c'est opérationnel : if (!soundAsset).

/// Objective-C — Exemple — Audio.m — update:18/05/26
- (void)loadStereoSound:(NSString*)resourcePath assetName:(NSString*)assetName
{
    NSError*  error = nil;
    NSString* urlString = [NSString stringWithFormat:@"%@/%@", resourcePath, assetName];
    NSURL*    url       = [NSURL URLWithString:urlString];

    PHASESoundAsset* soundAsset = [_engine.assetRegistry
                                   registerSoundAssetAtURL:url
                                   identifier:assetName
                                   assetType:PHASEAssetTypeResident
                                   channelLayout:nil
                                   normalizationMode:PHASENormalizationModeDynamic
                                   error:&error];
    if (!soundAsset)
    {
        NSLog(@"Error registering sound asset %@: %@", urlString, error.localizedDescription);
        return;
    }

    AVAudioChannelLayout* channelLayout = [[AVAudioChannelLayout alloc]
                                           initWithLayoutTag:kAudioChannelLayoutTag_Stereo];

    PHASEChannelMixerDefinition* channelMixerDefinition = [[PHASEChannelMixerDefinition alloc]
                                                           initWithChannelLayout:channelLayout];

    PHASESamplerNodeDefinition* samplerNodeDefinition = [[PHASESamplerNodeDefinition alloc]
                                                         initWithSoundAssetIdentifier:assetName
                                                         mixerDefinition:channelMixerDefinition];

    samplerNodeDefinition.playbackMode = PHASEPlaybackModeOneShot;
    [samplerNodeDefinition setCalibrationMode:PHASECalibrationModeRelativeSpl level:0];

    NSString* eventIdentifier = [NSString stringWithFormat:@"%@_event", assetName];

    PHASESoundEventNodeAsset* soundEventAsset = [_engine.assetRegistry
                                                 registerSoundEventAssetWithRootNode:samplerNodeDefinition
                                                 identifier:eventIdentifier
                                                 error:&error];
    if (!soundEventAsset)
    {
        NSLog(@"Error creating sound event for asset: %@", error.localizedDescription);
    }
}
/// Objective-C++ — Exemple — Audio.mm — update:18/05/26
void Audio::loadStereoSound(const std::string& resourcePath, const std::string& assetName)
{
    NSError* __autoreleasing error = nil;
    NSString* urlString = [NSString stringWithFormat:@"%s/%s", resourcePath.c_str(),assetName.c_str()];
    NSURL* url = [NSURL URLWithString:urlString];
    NSString* assetIdentifier = [NSString stringWithUTF8String:assetName.c_str()];
    
    PHASESoundAsset* soundAsset = [m_engine.assetRegistry
                                   registerSoundAssetAtURL:url
                                   identifier:assetIdentifier
                                   assetType:PHASEAssetTypeResident
                                   channelLayout:nil
                                   normalizationMode:PHASENormalizationModeDynamic
                                   error:&error];

    if (!soundAsset)
    {
        NSLog(@"Error registering sound asset %@: %@", urlString, error.localizedDescription);
        assert(false);
    }

On veut également stocker plusieurs sons dans une hiérarchie nodes grâce à 'PHASESoundEventNodeAsset'

/// Objective-C++ — Exemple — Audio.mm — update:18/05/26
    // pour initialiser les paramètres de 'PHASEChannelMixerDefinition' (framework AVFAudio)
    AVAudioChannelLayout* channelLayout = [[AVAudioChannelLayout alloc]
                                           initWithLayoutTag:kAudioChannelLayoutTag_Stereo];
    
    // Prototype si tu n'as pas accès à la documentation dans l'immédiat :
    // - (instancetype)initWithChannelLayout:(AVAudioChannelLayout*)layout;
    // Pour initialiser les paramètres (signature) de 'PHASESamplerNodeDefinition' nous avons besoin de :
    PHASEChannelMixerDefinition* channelMixerDefinition = [[PHASEChannelMixerDefinition alloc]
                                                           initWithChannelLayout:channelLayout];
    
    PHASESamplerNodeDefinition* samplerNodeDefinition = [[PHASESamplerNodeDefinition alloc]
                                                         initWithSoundAssetIdentifier:[NSString
                                                         stringWithUTF8String:assetName.c_str()]
                                                         mixerDefinition:channelMixerDefinition];
    
    'OneShot' joue une fois et s'arrête — 'Looping' boucle en continu
    samplerNodeDefinition.playbackMode = PHASEPlaybackModeOneShot;
    
    [samplerNodeDefinition setCalibrationMode:PHASECalibrationModeRelativeSpl level:0];
    
    NSString* eventIdentifier = [NSString stringWithFormat:@"%s_event", assetName.c_str()];
    
    PHASESoundEventNodeAsset* soundEventAsset = [m_engine.assetRegistry
                                                 registerSoundEventAssetWithRootNode:samplerNodeDefinition
                                                 identifier:eventIdentifier
                                                 error:&error];
    if (!soundEventAsset)
    {
        NSLog(@"Error creating sound event for asset: %@", error.localizedDescription);
        assert(false);
    }
}

Ne pas oublier de rajouter les prototypes des fonctions dans le header.

PHASESoundEvent[C] objet/classe qui détermine quel audio jouer.. Je vous laisse découvrir la documentation qui suit le même principe...

/// Objective-C — Exemple — Audio.m — update:18/05/26
- (void)playSoundEvent:(NSString*)identifier
{
    NSError* error = nil;

    PHASESoundEvent* soundEvent = [[PHASESoundEvent alloc]
                                   initWithEngine:_engine
                                   assetIdentifier:[NSString stringWithFormat:@"%@_event", identifier]
                                   error:&error];
    if (!soundEvent)
    {
        NSLog(@"Unable to play sound event with identifier: %@", identifier);
        return;
    }

    [soundEvent startWithCompletion:nil];
}

@end
/// Objective-C++ — Exemple — Audio.mm — update:18/05/26
void Audio::playSoundEvent(const std::string& identifier)
{
    NSError* __autoreleasing error = nil;
    
    PHASESoundEvent* soundEvent = [[PHASESoundEvent alloc]
                                   initWithEngine:m_engine
                                   assetIdentifier:[NSString stringWithFormat:@"%s_event", identifier.c_str()]
                                   error:&error];

    if (!soundEvent)
    {
        NSLog(@"Unable to play sound event with identifier: %s", identifier.c_str());
        assert(false);
    }
    
    [soundEvent startWithCompletion:nil];
}

Fichier final pour l'inclure en C++

On déclare nos fonctions en public pour les appeler en C++

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                        +       +          */
/*      File: Audio.hpp (Compatible-C++)         +++     +++ */
/*                                        +       +          */
/*      By: Laboitederemdal                +       +         */
/*                                       +           +       */
/*      Created: 27/10/2025 15:45:19      + + + + + +        */
/*      Updated: 18/05/2026                                  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef AUDIO_HPP
#define AUDIO_HPP

#include <string>
#include <vector>
#include <CoreFoundation/CFBase.h> // pour CFTypeRef
#include "NonCopyable.hpp"

class Audio : public NonCopyable
{
public:
    Audio(const std::string& resourcePath);
    ~Audio();
    
    void loadStereoSound(const std::string& resourcePath, const std::string& assetName);
    void playSoundEvent(const std::string& identifier); // init donc renvoi void

private:
    struct SoundData // Track backing data to release:
    {
        CFTypeRef   asset;
        CFTypeRef   event;
        std::string assetName;
        std::string eventName;
    };

    std::vector<SoundData>  m_soundData;
    // Une base « générique » non typée à tout objet Core Foundation et des fonctions polymorphes sur eux
    CFTypeRef m_phaseEngine; // Remplace PHASEEngine et fait le pont
};

#endif // AUDIO_HPP
CFTypeRef — le pont entre Objective-C et C++

Un header .hpp inclus par du C++ pur ne peut pas contenir de types Objective-C (PHASEEngine*, LongRunningHaptics*…). Le compilateur ne les connaît pas.

CFTypeRef est un pointeur générique de CoreFoundation — typedef const void* — compatible avec n'importe quel objet ObjC. Il joue le rôle de conteneur opaque : le header C++ voit un pointeur void, et le .mm sait ce qu'il contient réellement.

Trois opérations suffisent :

// 1. Stocker — sortir l'objet de l'ARC et en confier la propriété au C++
m_phaseEngine = CFBridgingRetain(engine);   // ARC → manuel
_haptics      = CFBridgingRetain(lrh);

// 2. Récupérer — cast sans transfert de propriété (__bridge = "juste regarder")
PHASEEngine*        engine = (__bridge PHASEEngine*)m_phaseEngine;
LongRunningHaptics* lrh    = (__bridge LongRunningHaptics*)_haptics;

// 3. Libérer — dans le destructeur, à la place de l'ARC
CFRelease(m_phaseEngine);
CFRelease(_haptics);
On retrouve exactement le même schéma dans les chapitres sur la gestion des Frameworks Apple GameController & CoreMotion — deux classes C++ qui pilotent des objets ObjC sans les exposer dans leur header.
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                        +       +          */
/*      File: Audio.mm (Compatible-C++)         +++     +++  */
/*                                        +       +          */
/*      By: Laboitederemdal                +       +         */
/*                                       +           +       */
/*      Created: 27/10/2025 15:45:19      + + + + + +        */
/*      Updated: 18/05/2026                                  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#import <PHASE/PHASE.h> // on inclut pas dans le header partagé C++
#include "Audio.hpp"

Audio::Audio(const std::string& resourcePath)
{
    NSError __autoreleasing* error = nil;
    PHASEEngine* engine = [[PHASEEngine alloc] initWithUpdateMode:PHASEUpdateModeAutomatic];
    
    // démarre le moteur :
    if (![engine startAndReturnError:&error])
    {
        NSLog(@"Error starting PHASE engine: %@", error.localizedDescription);
        assert(false);
    }
    
    m_phaseEngine = CFBridgingRetain(engine); // ARC → manuel
}

Audio::~Audio()
{
    PHASEEngine* engine = (__bridge PHASEEngine*)m_phaseEngine;
    [engine stop];
    
    for (auto&& soundData : m_soundData) // On libère manuellement et proprement
    {
        CFRelease(soundData.event);
        CFRelease(soundData.asset);
        [engine.assetRegistry unregisterAssetWithIdentifier:
         [NSString stringWithUTF8String:soundData.assetName.c_str()] completion:nil];
        [engine.assetRegistry unregisterAssetWithIdentifier:
         [NSString stringWithUTF8String:soundData.eventName.c_str()] completion:nil];
    }
    CFRelease(m_phaseEngine);
}

void Audio::loadStereoSound(const std::string& resourcePath, const std::string& assetName)
{
    NSError* __autoreleasing error = nil;
    
    PHASEEngine* engine = (__bridge PHASEEngine*)m_phaseEngine;
    
    NSString* urlString = [NSString stringWithFormat:@"%s/%s", resourcePath.c_str(),assetName.c_str()];

    NSURL* url = [NSURL URLWithString:urlString];
    
    NSString* assetIdentifier = [NSString stringWithUTF8String:assetName.c_str()];
    
    PHASESoundAsset* soundAsset = [engine.assetRegistry
                                   registerSoundAssetAtURL:url
                                   identifier:assetIdentifier
                                   assetType:PHASEAssetTypeResident
                                   channelLayout:nil
                                   normalizationMode:PHASENormalizationModeDynamic
                                   error:&error];

    if (!soundAsset)
    {
        NSLog(@"Error registering sound asset %@: %@", urlString, error.localizedDescription);
        assert(false);
    }

    AVAudioChannelLayout* channelLayout = [[AVAudioChannelLayout alloc]
                                           initWithLayoutTag:kAudioChannelLayoutTag_Stereo];

    PHASEChannelMixerDefinition* channelMixerDefinition = [[PHASEChannelMixerDefinition alloc]
                                                           initWithChannelLayout:channelLayout];
    
    PHASESamplerNodeDefinition* samplerNodeDefinition = [[PHASESamplerNodeDefinition alloc]
                                                         initWithSoundAssetIdentifier:[NSString
                                                         stringWithUTF8String:assetName.c_str()]
                                                         mixerDefinition:channelMixerDefinition];
    
    samplerNodeDefinition.playbackMode = PHASEPlaybackModeOneShot;
    
    [samplerNodeDefinition setCalibrationMode:PHASECalibrationModeRelativeSpl level:0];
    
    NSString* eventIdentifier = [NSString stringWithFormat:@"%s_event", assetName.c_str()];
    
    PHASESoundEventNodeAsset* soundEventAsset = [engine.assetRegistry
                                                 registerSoundEventAssetWithRootNode:samplerNodeDefinition
                                                 identifier:eventIdentifier
                                                 error:&error];
    if (!soundEventAsset)
    {
        NSLog(@"Error creating sound event for asset: %@", error.localizedDescription);
        assert(false);
    }
    
    // Save sound asset and event for releasing durning shutdown
    // CFBridgingRetain transfers ownership from ARC to manual management,
    // so the destructor can release these assets deliberately.
    m_soundData.emplace_back( SoundData {
        CFBridgingRetain(soundAsset),       // asset
        CFBridgingRetain(soundEventAsset),  // event
        assetIdentifier.UTF8String,         // assetName
        eventIdentifier.UTF8String          // eventName
    });
}

void Audio::playSoundEvent(const std::string& identifier)
{
    NSError* __autoreleasing error = nil;
    
    PHASEEngine* engine = (__bridge PHASEEngine*)m_phaseEngine;
    
    PHASESoundEvent* soundEvent = [[PHASESoundEvent alloc]
                                   initWithEngine:engine
                                   assetIdentifier:[NSString stringWithFormat:@"%s_event", identifier.c_str()]
                                   error:&error];

    if (!soundEvent)
    {
        NSLog(@"Unable to play sound event with identifier: %s", identifier.c_str());
        assert(false);
    }
    
    [soundEvent startWithCompletion:nil];
}

Utilisation immédiate

// Usage en Objective-C pour comparer
Audio* audio = [[Audio alloc] initWithResourcePath:@"/path/to/sounds"];
[audio loadStereoSound:@"/path/to/sounds" assetName:@"victoire.m4a"];
[audio playSoundEvent:@"victoire.m4a"];
/// C++ — Renderer.hpp — update:18/05/26
#include "Audio.hpp"

private:
    std::unique_ptr<Audio> m_audioEngine;
/// C++ — Renderer.cpp — update:18/05/26 — constructeur, peu importe
{
    m_audioEngine = std::make_unique<Audio>(resourcePath);
    m_audioEngine->loadStereoSound(resourcePath, "mrSpoiler.mp3");
    m_audioEngine->loadStereoSound(resourcePath, "Test.m4a");
}
        /// C++ — Renderer.cpp — update:18/05/26 — update(), où tu veux
        m_globalUniforms.cameraUniforms = m_camera.updateUniforms();
        if (frame % 5000 == 10)
            m_audioEngine->playSoundEvent("mrSpoiler.mp3");
        if (m_input.keySpace)
            m_audioEngine->playSoundEvent("Test.m4a");
⬡CheckPoint Compilation — prochaine étape, l'audio spacial

Notre classe nous permet de loader des fichiers externes au format .mp3 & .m4a dès qu'on le décide. Nous pouvons commencer à jouer les morceaux à la demande (spam).