# Audio Panda3D's audio support is a three-layer stack. `panda/src/audio` defines a thin, backend-agnostic abstraction (`AudioManager` + `AudioSound`) plus a do-nothing `Null*` fallback and the async-load glue. `panda/src/audiotraits` provides the two real backends, OpenAL (the default) and FMOD, each compiled into its own dynamically-loadable shared library and selected at runtime by the `audio-library-name` config var. `panda/src/movies` is an independent decoder abstraction (`MovieAudio`/`MovieAudioCursor`, mirrored by `MovieVideo`/`MovieVideoCursor`) that turns files into raw PCM sample streams; the audio backends consume those streams. The key insight for a new contributor: **OpenAL/FMOD do not decode files — the `movies` layer does** (Vorbis/Opus/FLAC/WAV in-tree, everything else via the optional `ffmpeg` module), and the manager decides per-sound whether to fully decode into a RAM "sample" or to stream the cursor. ## audio **What it is.** The portable interface layer. It declares the two central abstract classes every backend must implement — `AudioManager` (a factory + global mixer/3D-listener controller) and `AudioSound` (a single playable handle) — plus a complete no-op implementation (`NullAudioManager`/`NullAudioSound`) that is always available so that code can run with sound disabled and never crash. It also owns the backend-selection/loader machinery and the DSP-filter description object (`FilterProperties`). It does **not** link against OpenAL, FMOD, or any codec; it only knows `MovieAudio` (from `movies`) as the alternative sound source. **Key classes and roles.** - `AudioManager` — `panda/src/audio/audioManager.h` / `audioManager.cxx`. Abstract; inherits `TypedReferenceCount`. The factory entry point is the static `create_AudioManager()`. Pure-virtual surface a backend must fill: `is_valid()`, the two `get_sound()` overloads (one taking a `Filename`, one taking a `MovieAudio *`), the cache controls (`uncache_sound`/`clear_cache`/`set_cache_limit`/`get_cache_limit`), volume, active flag, concurrent-sound limit, `reduce_sounds_playing_to`, `stop_all_sounds`. The 3D-listener / doppler / distance / drop-off methods and `configure_filters()` have default (empty) implementations so a minimal backend can ignore them. Enums `SpeakerModeCategory`, `SpeakerId`, and `StreamMode` (`SM_heuristic`, `SM_sample`, `SM_stream`) live here — the `mode` argument to `get_sound()` is how callers force streaming vs. preloading. - `AudioSound` — `panda/src/audio/audioSound.h` / `audioSound.cxx`. Abstract; inherits `TypedReferenceCount`. Pure-virtual playback surface (`play`/`stop`/`set_loop`/`set_loop_count`/`set_time`/`set_volume`/`set_balance`/`set_play_rate`/`set_active`/`status`/`length`/`get_name`). 3D-positional methods, the `set_speaker_mix` (FMOD-only) call, priority, and `configure_filters` are non-pure with stub defaults. Note the header comments candidly flag which features are backend-specific: `make_copy()` and `set_3d_direction()` are "currently only implemented for OpenAL"; `set_speaker_mix` is "for use with FMOD". `SoundStatus` is `{BAD, READY, PLAYING}`. - `NullAudioManager` / `NullAudioSound` — `panda/src/audio/nullAudioManager.{h,cxx}`, `nullAudioSound.{h,cxx}`. Silent stubs. The header explicitly says: *"If you're looking for a starting place for a new AudioManager, please consider looking at the openalAudioManager."* `AudioManager::get_null_sound()` lazily allocates a single shared `NullAudioSound` (atomically, via `patomic _null_sound`) and every backend returns it when a load fails — so callers never get a null pointer. - `FilterProperties` — `panda/src/audio/filterProperties.{h,cxx,I}`. Inherits `TypedReferenceCount`. A passive description of a DSP chain: `add_lowpass/highpass/echo/flange/distort/normalize/parameq/pitchshift/chorus/sfxreverb/compress`. Each call pushes a `FilterConfig` (a `FilterType` tag + up to 14 float params `_a.._n`) onto a `ConfigVector`. The backend interprets it in `configure_filters()`; the base-class default returns `true` only if the chain is empty (i.e. "nothing requested is supported"). - `AudioLoadRequest` — `panda/src/audio/audioLoadRequest.{h,cxx,I}`. Inherits `AsyncTask`. Wraps a single background load. `do_task()` simply calls `_audio_manager->get_sound(_source/_filename, _positional)` and stores the result via `set_result()`, then returns `DS_done`. This is the bridge to the `pgraph` `Loader` / any `AsyncTaskManager`. **Central abstraction & inheritance.** `TypedReferenceCount → AudioManager → {NullAudioManager, OpenALAudioManager, FmodAudioManager}` and `TypedReferenceCount → AudioSound → {NullAudioSound, OpenALAudioSound, FmodAudioSound}`. `AudioManager` deliberately holds almost no data members ("Avoid adding data members ... This allows implementors of various sound systems the best flexibility") — only the static creator pointer and the cached null sound. **How backend selection works (read this first).** `AudioManager::create_AudioManager()` in `audioManager.cxx`: 1. If a backend already called `register_AudioManager_creator(proc)` (the statically-linked / already-loaded case), use it. 2. Otherwise, if `audio_library_name` is non-empty and not `"null"`, `load_dso()` the library `lib.so`, look up the exported symbol `get_audio_manager_func_` (with a leading `p3` stripped), call it to obtain the factory function, and register it. 3. If nothing registered, fall back to `create_NullAudioManager`. 4. After creating, if the manager is not a `NullAudioManager` and `!is_valid()`, it is discarded and replaced by a `NullAudioManager` (so a broken OpenAL/FMOD init degrades to silence instead of crashing). **How it plugs into the engine.** `direct/src/showbase/ShowBase.py` creates one or more SFX managers plus a music manager via `AudioManager.createAudioManager()` (`ShowBase.py` ~line 2067–2070) and stores them in `self.sfxManagerList` / `self.musicManager`. `direct/src/showbase/Loader.py::loadSfx()` picks `sfxManagerList[0]`, `loadMusic()` picks `musicManager`, and both funnel into `loadSound()`, which either calls `manager.getSound(soundPath, positional)` synchronously or, when a callback is supplied, constructs an `AudioLoadRequest` and submits it with `self.loader.loadAsync(request)` (`Loader.py` ~line 1054 / 1070). `ShowBase` calls each manager's `update()` once per frame (the `audioLoop` task) — failing to call `update()` every frame is explicitly warned against in `AudioManager::update()`. **Where to start.** To add/modify the abstract API, edit `audioManager.h` + `audioSound.h` and then implement the same change in **both** traits and in the `Null*` stubs (the `Null*` classes are the cheapest place to see the full method list). To write a brand-new backend, copy the `Null*` pair, model the real logic on OpenAL, and add a `get_audio_manager_func_()` + a `ConfigureFn` that calls `register_AudioManager_creator()`. **Gotchas / rationale (community).** - *Volume semantics:* `AudioManager::set_volume()` is a master gain that must **not** change what `AudioSound::get_volume()` returns. A PR that grabbed the manager volume into the sound was rejected for exactly this reason — see [panda3d/panda3d#64 "openal: Grab volume from manager instead of assuming max volume"](https://github.com/panda3d/panda3d/pull/64): *"the API clearly intends for `AudioManager::set_volume` not to affect `AudioSound::get_volume`."* - *Per-category managers are intentional:* there is no semantic difference between "sfx" and "music" — they are just separate `AudioManager` instances so volume/active can be controlled as groups (`Loader.py` docstring). `loader.loadSfx` always uses the default SFX manager; for a custom manager use `AudioManager.createAudioManager()` + `manager.getSound()` directly — see [forum: custom audioManagers?](https://discourse.panda3d.org/t/12029). **Config variables (`config_audio.cxx`).** `audio-active` (bool, default true); `audio-library-name` (string, default `"null"`); `audio-volume` (double, 1.0); `audio-cache-limit` (int, 15); `audio-preload-threshold` (int, 1000000 — decompressed-size cutoff above which a sound is streamed not cached); `audio-buffering-seconds` (double, 3.0 — streaming buffer depth, "favoring correctness over efficiency"); `audio-doppler-factor`, `audio-distance-factor`, `audio-drop-off-factor` (doubles, 1.0; consumed by OpenAL/FMOD); `audio-want-hrtf` (bool, false; OpenAL-Soft HRTF); `audio-min-hw-channels` (int, 15); `audio-dls-file` (filename; MIDI instrument set); and the FMOD-tuning vars `fmod-number-of-sound-channels` (128), `fmod-use-surround-sound` (deprecated), and `fmod-speaker-mode` (enum `FmodSpeakerMode`). ## audiotraits **What it is.** The two concrete audio backends, each its own loadable component library: OpenAL (component `p3openal_audio`, the default, fully open-source via OpenAL-Soft) and FMOD (component `p3fmod_audio`, links the proprietary FMOD-EX SDK). Each provides an `AudioManager` subclass and an `AudioSound` subclass and nothing else — there is no shared code between the two beyond the abstract base. They are mutually exclusive at runtime: whichever `audio-library-name` names is the one loaded. **Key classes and roles.** - `OpenALAudioManager` — `panda/src/audiotraits/openalAudioManager.{h,cxx}`. `final`, inherits `AudioManager`. Owns the process-wide OpenAL state as statics: one `ALCdevice* _device`, one `ALCcontext* _context`, a pool of reusable `ALuint` sources (`_al_sources`), and the set of all live managers. Per-instance it holds a `_sample_cache` (filename → decoded-into-RAM `SoundData`), `_sounds_playing`, `_all_sounds`, and two LRU "expiration queues" (`_expiring_samples`, `_expiring_streams`) that keep recently-used `SoundData` alive briefly for fast reuse. All OpenAL calls are serialized by a single static `ReMutex _lock`. `select_audio_device()` honors the `openal-device` config string. - `OpenALAudioSound` — `panda/src/audiotraits/openalAudioSound.{h,cxx,I}`. `final`, inherits `AudioSound`. The only backend that implements `make_copy()` and `set_3d_direction()`. For streaming sounds it pumps PCM through OpenAL buffer queues; the manager's `update()` drives this (see below). - `FmodAudioManager` — `panda/src/audiotraits/fmodAudioManager.{h,cxx}`. Inherits `AudioManager`. Shares a single global `FMOD::System *_system` across all managers ("this appears to be the way FMOD wants to work"). Builds DSP chains from `FilterProperties` via `make_dsp()` / `update_dsp_chain()`. The class-top comment block (lines 17–63) is a genuinely useful primer on FMOD-EX written by the original ETC/CMU author. Note many cache methods are explicit stubs here ("THESE ARE NOT USED ANYMORE ... still needed by Miles"). - `FmodAudioSound` — `panda/src/audiotraits/fmodAudioSound.{h,cxx,I}`. Inherits `AudioSound`. Implements `set_speaker_mix()` (the FMOD-specific multi-speaker panning) and `set_wavwriter` support via the manager. **The decode/stream decision (the heart of OpenAL's `get_sound`).** In `openalAudioManager.cxx`: - `get_sound(const Filename &)` resolves the path against the model path, then `MovieAudio::get(path)` to obtain a decoder, then constructs an `OpenALAudioSound`. `get_sound(MovieAudio *)` does the same without the file lookup — this is how a caller hands in a microphone, a `UserDataAudio`, or any custom stream. - `get_sound_data()` opens the `MovieAudioCursor`, calls `can_use_audio()` (rejects anything that is not mono or stereo), then `should_load_audio()` to decide caching vs. streaming. The streaming/caching policy is concrete and worth knowing: stream if `mode == SM_stream`; stream if the source has no filename (microphones, user data); stream if the cursor isn't fully ready (`ready() != 0x40000000`); stream anything longer than 3600 s; in `SM_heuristic`, stream if the decoded byte size exceeds `audio_preload_threshold`. Otherwise it `alGenBuffers` + `read_samples` + `alBufferData` to load the whole thing into one OpenAL buffer and inserts it into `_sample_cache`. **Per-frame update (a real footgun area).** `OpenALAudioManager::update()` walks `_sounds_playing`; for each sound it calls `pull_used_buffers()`, `push_fresh_buffers()`, `restart_stalled_audio()`, `cache_time()`, and `finished()` when the stream is exhausted. The loop uses post-increment iteration *on purpose* because `pull_used_buffers()`/`finished()` can erase the current sound from the set — the comment at line 1110 spells this out. This exact class of bug was fixed in [panda3d/panda3d#1452 "openal: fix potential iterator invalidation in OpenALAudioManager::update"](https://github.com/panda3d/panda3d/pull/1452); if you touch this loop, preserve the `*(i++)` pattern and the `PT(OpenALAudioSound)` ref-hold. **How it plugs in.** Each backend self-registers from its `ConfigureFn`: `config_openalAudio.cxx`'s `init_libOpenALAudio()` and `config_fmodAudio.cxx`'s `init_libFmodAudio()` call `AudioManager::register_AudioManager_creator(&Create_*AudioManager)` and tag `PandaSystem` ("audio" → implementation "OpenAL"/"FMOD"). Each also exports the dlopen entry point (`get_audio_manager_func_openal_audio` / `get_audio_manager_func_fmod_audio`) used by `create_AudioManager()` when the library is loaded dynamically. The backends depend **downstream** on `movies` (they consume `MovieAudio`/`MovieAudioCursor`) and on the abstract `audio` library; nothing in core depends on them directly. **Where to start.** Bug in playback/looping/3D? Start in `openalAudioSound.cxx` (`play`, `set_loop_count`, `set_3d_attributes`) and `openalAudioManager.cxx::update()`. Bug in "wrong sounds get streamed / memory blows up"? `should_load_audio()` + `get_sound_data()`. Adding a DSP effect to FMOD? `FmodAudioManager::make_dsp()` / `configure_filters()`. OpenAL DSP/EFX is comparatively underdeveloped — see the note below. **Gotchas / rationale (community).** - *OpenAL on Linux historically weak:* [forum: errors with fedora 10](https://discourse.panda3d.org/t/4929) — *"Support for OpenAL on Linux is extremely poor. Try switching to FMOD or no sound..."* (`audio-library-name p3fmod_audio`). Modern OpenAL-Soft is much better, but this is why FMOD still exists. - *The default flipped to OpenAL:* [forum: Frame Per Second](https://discourse.panda3d.org/t/3384) — *"the default audio library changed from FMod to OpenAL ... in the switch to 1.4. You can change it back by editing"* the config. - *DSP is backend-asymmetric:* per the docs and PRs, DSP effects were FMOD-first; mapping them to OpenAL uses EFX and is partial — see [panda3d/panda3d#1032 "Update the FMOD audio system, DSP system"](https://github.com/panda3d/panda3d/pull/1032) and the [DSP Effects docs](https://docs.panda3d.org/1.10/python/programming/audio/dsp-effects). - *HRTF / OpenAL-Soft extensions:* the non-framework build path and the `audio-want-hrtf` switch were added in [panda3d/panda3d#1620](https://github.com/panda3d/panda3d/pull/1620). - *3D-audio listener update lives in Python:* the per-frame listener position math is in `direct/src/showbase/Audio3DManager.py`, and there are long-standing coordinate-system bugs reported there — [forum: 3d audio bug with 'coordinate-system yup'](https://discourse.panda3d.org/t/8037) and [3d audio sound bug?](https://discourse.panda3d.org/t/2060). When debugging "3D sound is in the wrong place," check the Python `Audio3DManager.update` before the C++ `audio_3d_set_listener_attributes`. **Config variables.** OpenAL (`config_openalAudio.cxx`): `openal-device` (string; empty = default device), `openal-buffer-delete-retries` (int, 5) and `openal-buffer-delete-delay` (double, 0.001) — these exist because deleting an OpenAL buffer that is "still in use" can fail and must be retried with exponential backoff. FMOD (`config_fmodAudio.cxx`): `fmod-audio-preload-threshold` (int, 1048576; `-1` preloads everything). The shared/3D vars (`audio-doppler-factor` etc.) are declared in `config_audio.cxx` and read by both backends. ## movies **What it is.** A codec-abstraction and raw-media-streaming layer that is *independent of the audio backends* — it produces decoded PCM samples (and decoded video frames) and knows nothing about OpenAL/FMOD or the scene graph. Every loadable sound ultimately becomes a `MovieAudio` here; video for textures becomes a `MovieVideo`. The split between `MovieAudio` and `MovieAudioCursor` is *"like the difference between a filename and a file handle"* — the `MovieAudio` names a source, the `MovieAudioCursor` is a single-threaded read handle into it. In-tree decoders cover WAV, Ogg Vorbis, Opus, and FLAC; anything else is delegated to the optional `ffmpeg` module, which registers itself as the catch-all. **Key classes and roles.** - `MovieAudio` — `panda/src/movies/movieAudio.{h,cxx,I}`. Inherits `TypedWritableReferenceCount` + `Namable`. Base `open()` returns a silent `MovieAudioCursor`; subclasses override it to return a real decoder cursor. The static `MovieAudio::get(filename)` is the universal entry point — it just calls `MovieTypeRegistry::get_global_ptr()->make_audio(name)`. - `MovieAudioCursor` — `panda/src/movies/movieAudioCursor.{h,cxx,I}`. Inherits `TypedWritableReferenceCount`. The streaming interface backends call: `audio_rate()`, `audio_channels()`, `length()`, `can_seek()`, `seek()`, `ready()`, and the all-important `read_samples(int n, int16_t *data)` that fills a 16-bit-signed interleaved PCM buffer. **Thread-safety contract (header):** *"each individual MovieAudioCursor must be owned and accessed by a single thread"* — two threads may decode the same file only via separate cursors. - `MovieVideo` / `MovieVideoCursor` — `panda/src/movies/movieVideo.{h,cxx,I}`, `movieVideoCursor.{h,cxx,I}`. The exact video analog. `MovieVideoCursor` additionally has `setup_texture(Texture *)` and a `Buffer` (RAM-image holder) — this is the bridge into the scene graph: `MovieTexture` (in `panda/src/grutil`) wraps a `MovieVideoCursor` and pushes frames into a `Texture`. `MovieVideo` is also `TypedWritable` (it has `write_datagram`/`fillin`) so a movie reference can be stored in a Bam file. - `MovieTypeRegistry` — `panda/src/movies/movieTypeRegistry.{h,cxx,I}`. The dispatcher. `register_audio_type(func, "ext1 ext2 ...")` maps file extensions to a `MakeAudioFunc` factory; `make_audio(filename)` downcases the extension (peeling a `.pz`/`.gz` compression suffix first), lazily loads any deferred module for that extension, and calls the factory. Extension `*` is the catch-all. Symmetric `*_video_*` methods exist. A `ReMutex` guards each registry. On total failure it returns a *"Load-Failure Stub"* `MovieAudio` (silence) rather than null. - In-tree decoders, each a `MovieAudio`+`MovieAudioCursor` pair following the same idiom (`open()` opens a VFS stream and constructs the cursor; static `make()` is the registry factory): `WavAudio`/`WavAudioCursor` (`wavAudio.*`, `wavAudioCursor.*` — pure in-house WAV parser), `VorbisAudio`/`VorbisAudioCursor` (libvorbisfile, guarded by `HAVE_VORBIS`), `OpusAudio`/`OpusAudioCursor` (libopusfile, `HAVE_OPUS`), `FlacAudio`/`FlacAudioCursor` (the bundled single-header `dr_flac.h`). Specials: `UserDataAudio`/`UserDataAudioCursor` (`userDataAudio.*`) lets application code `append()` raw 16-bit samples for a programmatic/streamed source; `MicrophoneAudio` (`microphoneAudio.*`, with the DirectShow capture impl in `microphoneAudioDS.cxx`) is an abstract live-capture source enumerated by `get_option()`. `InkblotVideo`/`InkblotVideoCursor` is a tiny procedural video generator (a cellular automaton) — useful as the minimal worked example of a `MovieVideo` subclass. **Central abstraction & inheritance.** `TypedWritableReferenceCount(+Namable) → MovieAudio → {WavAudio, VorbisAudio, OpusAudio, FlacAudio, UserDataAudio, MicrophoneAudio, (ffmpeg) FfmpegAudio}` and `TypedWritableReferenceCount → MovieAudioCursor → {WavAudioCursor, ...}`. Video mirrors this exactly. **How it plugs in.** `config_movies.cxx::init_libmovies()` `init_type()`s every class and registers the in-tree audio factories: `FlacAudio::make` for `flac`, `WavAudio::make` for `wav wave`, `OpusAudio::make` for `opus` (if `HAVE_OPUS`), `VorbisAudio::make` for `ogg oga` (if `HAVE_VORBIS`). The optional `panda/src/ffmpeg` module's `config_ffmpeg.cxx` registers `FfmpegAudio::make` and `FfmpegVideo::make` under extension `*` — **this is why an MP3/AAC/MP4/etc. plays even though OpenAL/FMOD can't decode it: ffmpeg is the catch-all decoder.** Upward, the audio backends (`audiotraits`) consume cursors; the `grutil` `MovieTexture` consumes video cursors. The registry's deferred-loading mechanism is driven by the `load-audio-type` / `load-video-type` config lists, so codec modules can be pulled in on first use of a matching extension. **Where to start.** Adding a new audio codec is the canonical "good first contribution" here: copy `wavAudio.{h,cxx}` + `wavAudioCursor.{h,cxx}` (the simplest, dependency-free pair), implement `open()` (open VFS stream, construct cursor) and the cursor's `read_samples()` / `audio_rate()` / `audio_channels()` / `length()` / `seek()`, add a static `make()`, then register it in `config_movies.cxx` with `reg->register_audio_type(&YourAudio::make, "ext")`. For seeking/looping issues, the cursor's `seek()` + `tell()` + `_samples_read` bookkeeping is the place. For a programmatic source (network audio, synth), subclass `UserDataAudio` or follow its pattern. **Gotchas / rationale (community).** - *Backends don't decode — movies does:* the most load-bearing fact in this cluster. [forum: use OGG sounds not saved to disk?](https://discourse.panda3d.org/t/11405) — *"OpenAL includes no code to decode vorbis (or any other sound file format); ffmpeg is used to decode the audio when you're using the [OpenAL] backend."* (Today Vorbis/Opus/FLAC/WAV are decoded in-tree by `movies` itself; the catch-all path still routes everything else through ffmpeg.) - *Single-thread-per-cursor:* both the audio and video cursor headers state the same ownership rule. Async loading (`AudioLoadRequest` on a task thread) is fine because each request opens its own cursor; sharing a cursor across threads is not. - *Microphone/live streaming is supported but niche:* community examples exist for UDP voice using `MicrophoneAudio` + `UserDataAudio` — [forum: Microphone/voice stream on UDP?](https://discourse.panda3d.org/t/11256) — useful if you're extending live-capture. **Config variables (`config_movies.cxx`).** `load-audio-type` and `load-video-type` (config lists mapping extensions → loadable module names, consumed by `MovieTypeRegistry::load_audio_types`/`load_video_types`); `opus-enable-seek` (bool, true — disable if Opus seeking misbehaves); `vorbis-enable-seek` (bool, true); `vorbis-seek-lap` (bool, false — crosslap on Vorbis seek to suppress click/boundary artifacts). The category proxy is `movies_cat`. ## Where to start (this cluster) - **Understand the contract:** read `panda/src/audio/audioManager.h` and `panda/src/audio/audioSound.h` top-to-bottom — they are the entire public surface, and the `Null*` pair (`panda/src/audio/nullAudioManager.h`, `nullAudioSound.cxx`) shows the minimal full implementation. - **Understand backend selection / the default:** `AudioManager::create_AudioManager()` in `panda/src/audio/audioManager.cxx`, then `init_libOpenALAudio()` in `panda/src/audiotraits/config_openalAudio.cxx`. The default backend is OpenAL; `audio-library-name` switches it. - **Understand a real backend:** `OpenALAudioManager::get_sound` / `get_sound_data` / `should_load_audio` / `update()` in `panda/src/audiotraits/openalAudioManager.cxx` — this is where the cache-vs-stream decision and the per-frame buffer pump live. - **Understand decoding:** `MovieTypeRegistry::make_audio` in `panda/src/movies/movieTypeRegistry.cxx` plus the registrations in `panda/src/movies/config_movies.cxx`; `wavAudio*`/`wavAudioCursor*` is the simplest decoder to copy. - **Understand the Python wiring:** `direct/src/showbase/Loader.py::loadSfx`/`loadSound` and `direct/src/showbase/ShowBase.py` (`sfxManagerList`, `musicManager`, the per-frame `update()` loop), plus `direct/src/showbase/Audio3DManager.py` for positional audio. ## Known shortcomings & footguns The audio stack works as described above, but several long-standing rough edges trip up newcomers — most of them flowing from the OpenAL-vs-FMOD backend split (see the `audiotraits` discussion above) and from 3D audio's hard requirement on the input format. ### FMOD requires a separate commercial license **Severity: major · Status: by-design (use OpenAL to avoid)** Panda historically recommended FMOD (the `p3fmod_audio` backend that links the proprietary FMOD-EX SDK — see the `audiotraits` section above), which needs a paid license for commercial release — repeatedly blindsiding users mid-project. The free alternative (OpenAL, the default since 1.4) was long the feature-poorer path. > "If you distribute a game using FMOD with Panda, you will need to buy an FMOD license." — drwr *(maintainer)*, [t/1504](https://discourse.panda3d.org/t/1504) ### 3D audio silently won't spatialize stereo files (must be mono) **Severity: major (silent footgun) · Status: by-design (warning only)** Load a stereo file into `Audio3DManager` and you get one warning line — then the sound just plays centered with no panning/falloff. Users think 3D audio is broken. (The format check is `can_use_audio()` in `OpenALAudioManager::get_sound_data()`, which only accepts mono or stereo at all; the *spatialization* path additionally needs mono. See the decode/stream discussion and the 3D-listener note in the `audiotraits` section above.) > ":audio(warning): stereo sound ... will not be spatialized" — [t/3344](https://discourse.panda3d.org/t/3344) ### OpenAL backend has weaker 3D/DSP features than FMOD **Severity: minor · Status: still-open** DSP effects are FMOD-only (per the manual); doppler was flaky; surround is FMOD-specific — so the license-free path is also feature-poor. This is the same backend asymmetry noted under `audiotraits` above (DSP effects were FMOD-first, and the OpenAL EFX mapping is partial — [panda3d/panda3d#1032](https://github.com/panda3d/panda3d/pull/1032), [DSP Effects docs](https://docs.panda3d.org/1.10/python/programming/audio/dsp-effects)); the practical upshot is that avoiding FMOD's license (above) also means giving up its richer 3D/DSP feature set. For cross-cutting backend-selection and reference-counting context, see [Cross-cutting concepts](../cross-cutting-concepts.md).