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.

  • AudioManagerpanda/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.

  • AudioSoundpanda/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 / NullAudioSoundpanda/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<AudioSound *> _null_sound) and every backend returns it when a load fails — so callers never get a null pointer.

  • FilterPropertiespanda/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”).

  • AudioLoadRequestpanda/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<audio-library-name>.so, look up the exported symbol get_audio_manager_func_<name> (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_<name>() + 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”: “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?.

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.

  • OpenALAudioManagerpanda/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.

  • OpenALAudioSoundpanda/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).

  • FmodAudioManagerpanda/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”).

  • FmodAudioSoundpanda/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”; 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“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“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” and the DSP Effects docs.

  • HRTF / OpenAL-Soft extensions: the non-framework build path and the audio-want-hrtf switch were added in panda3d/panda3d#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’ and 3d audio sound bug?. 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.

  • MovieAudiopanda/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).

  • MovieAudioCursorpanda/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 / MovieVideoCursorpanda/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.

  • MovieTypeRegistrypanda/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?“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 + UserDataAudioforum: Microphone/voice stream on UDP? — 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

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

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, DSP Effects docs); 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.