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; inheritsTypedReferenceCount. The factory entry point is the staticcreate_AudioManager(). Pure-virtual surface a backend must fill:is_valid(), the twoget_sound()overloads (one taking aFilename, one taking aMovieAudio *), 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 andconfigure_filters()have default (empty) implementations so a minimal backend can ignore them. EnumsSpeakerModeCategory,SpeakerId, andStreamMode(SM_heuristic,SM_sample,SM_stream) live here — themodeargument toget_sound()is how callers force streaming vs. preloading.AudioSound—panda/src/audio/audioSound.h/audioSound.cxx. Abstract; inheritsTypedReferenceCount. 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, theset_speaker_mix(FMOD-only) call, priority, andconfigure_filtersare non-pure with stub defaults. Note the header comments candidly flag which features are backend-specific:make_copy()andset_3d_direction()are “currently only implemented for OpenAL”;set_speaker_mixis “for use with FMOD”.SoundStatusis{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 sharedNullAudioSound(atomically, viapatomic<AudioSound *> _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}. InheritsTypedReferenceCount. A passive description of a DSP chain:add_lowpass/highpass/echo/flange/distort/normalize/parameq/pitchshift/chorus/sfxreverb/compress. Each call pushes aFilterConfig(aFilterTypetag + up to 14 float params_a.._n) onto aConfigVector. The backend interprets it inconfigure_filters(); the base-class default returnstrueonly if the chain is empty (i.e. “nothing requested is supported”).AudioLoadRequest—panda/src/audio/audioLoadRequest.{h,cxx,I}. InheritsAsyncTask. Wraps a single background load.do_task()simply calls_audio_manager->get_sound(_source/_filename, _positional)and stores the result viaset_result(), then returnsDS_done. This is the bridge to thepgraphLoader/ anyAsyncTaskManager.
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:
If a backend already called
register_AudioManager_creator(proc)(the statically-linked / already-loaded case), use it.Otherwise, if
audio_library_nameis non-empty and not"null",load_dso()the librarylib<audio-library-name>.so, look up the exported symbolget_audio_manager_func_<name>(with a leadingp3stripped), call it to obtain the factory function, and register it.If nothing registered, fall back to
create_NullAudioManager.After creating, if the manager is not a
NullAudioManagerand!is_valid(), it is discarded and replaced by aNullAudioManager(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 whatAudioSound::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 forAudioManager::set_volumenot to affectAudioSound::get_volume.”Per-category managers are intentional: there is no semantic difference between “sfx” and “music” — they are just separate
AudioManagerinstances so volume/active can be controlled as groups (Loader.pydocstring).loader.loadSfxalways uses the default SFX manager; for a custom manager useAudioManager.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.
OpenALAudioManager—panda/src/audiotraits/openalAudioManager.{h,cxx}.final, inheritsAudioManager. Owns the process-wide OpenAL state as statics: oneALCdevice* _device, oneALCcontext* _context, a pool of reusableALuintsources (_al_sources), and the set of all live managers. Per-instance it holds a_sample_cache(filename → decoded-into-RAMSoundData),_sounds_playing,_all_sounds, and two LRU “expiration queues” (_expiring_samples,_expiring_streams) that keep recently-usedSoundDataalive briefly for fast reuse. All OpenAL calls are serialized by a single staticReMutex _lock.select_audio_device()honors theopenal-deviceconfig string.OpenALAudioSound—panda/src/audiotraits/openalAudioSound.{h,cxx,I}.final, inheritsAudioSound. The only backend that implementsmake_copy()andset_3d_direction(). For streaming sounds it pumps PCM through OpenAL buffer queues; the manager’supdate()drives this (see below).FmodAudioManager—panda/src/audiotraits/fmodAudioManager.{h,cxx}. InheritsAudioManager. Shares a single globalFMOD::System *_systemacross all managers (“this appears to be the way FMOD wants to work”). Builds DSP chains fromFilterPropertiesviamake_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}. InheritsAudioSound. Implementsset_speaker_mix()(the FMOD-specific multi-speaker panning) andset_wavwritersupport 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, thenMovieAudio::get(path)to obtain a decoder, then constructs anOpenALAudioSound.get_sound(MovieAudio *)does the same without the file lookup — this is how a caller hands in a microphone, aUserDataAudio, or any custom stream.get_sound_data()opens theMovieAudioCursor, callscan_use_audio()(rejects anything that is not mono or stereo), thenshould_load_audio()to decide caching vs. streaming. The streaming/caching policy is concrete and worth knowing: stream ifmode == 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; inSM_heuristic, stream if the decoded byte size exceedsaudio_preload_threshold. Otherwise italGenBuffers+read_samples+alBufferDatato 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-hrtfswitch 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 PythonAudio3DManager.updatebefore 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}. InheritsTypedWritableReferenceCount+Namable. Baseopen()returns a silentMovieAudioCursor; subclasses override it to return a real decoder cursor. The staticMovieAudio::get(filename)is the universal entry point — it just callsMovieTypeRegistry::get_global_ptr()->make_audio(name).MovieAudioCursor—panda/src/movies/movieAudioCursor.{h,cxx,I}. InheritsTypedWritableReferenceCount. The streaming interface backends call:audio_rate(),audio_channels(),length(),can_seek(),seek(),ready(), and the all-importantread_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.MovieVideoCursoradditionally hassetup_texture(Texture *)and aBuffer(RAM-image holder) — this is the bridge into the scene graph:MovieTexture(inpanda/src/grutil) wraps aMovieVideoCursorand pushes frames into aTexture.MovieVideois alsoTypedWritable(it haswrite_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 aMakeAudioFuncfactory;make_audio(filename)downcases the extension (peeling a.pz/.gzcompression suffix first), lazily loads any deferred module for that extension, and calls the factory. Extension*is the catch-all. Symmetric*_video_*methods exist. AReMutexguards each registry. On total failure it returns a “Load-Failure Stub”MovieAudio(silence) rather than null.In-tree decoders, each a
MovieAudio+MovieAudioCursorpair following the same idiom (open()opens a VFS stream and constructs the cursor; staticmake()is the registry factory):WavAudio/WavAudioCursor(wavAudio.*,wavAudioCursor.*— pure in-house WAV parser),VorbisAudio/VorbisAudioCursor(libvorbisfile, guarded byHAVE_VORBIS),OpusAudio/OpusAudioCursor(libopusfile,HAVE_OPUS),FlacAudio/FlacAudioCursor(the bundled single-headerdr_flac.h). Specials:UserDataAudio/UserDataAudioCursor(userDataAudio.*) lets application codeappend()raw 16-bit samples for a programmatic/streamed source;MicrophoneAudio(microphoneAudio.*, with the DirectShow capture impl inmicrophoneAudioDS.cxx) is an abstract live-capture source enumerated byget_option().InkblotVideo/InkblotVideoCursoris a tiny procedural video generator (a cellular automaton) — useful as the minimal worked example of aMovieVideosubclass.
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
moviesitself; 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 (
AudioLoadRequeston 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? — 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.handpanda/src/audio/audioSound.htop-to-bottom — they are the entire public surface, and theNull*pair (panda/src/audio/nullAudioManager.h,nullAudioSound.cxx) shows the minimal full implementation.Understand backend selection / the default:
AudioManager::create_AudioManager()inpanda/src/audio/audioManager.cxx, theninit_libOpenALAudio()inpanda/src/audiotraits/config_openalAudio.cxx. The default backend is OpenAL;audio-library-nameswitches it.Understand a real backend:
OpenALAudioManager::get_sound/get_sound_data/should_load_audio/update()inpanda/src/audiotraits/openalAudioManager.cxx— this is where the cache-vs-stream decision and the per-frame buffer pump live.Understand decoding:
MovieTypeRegistry::make_audioinpanda/src/movies/movieTypeRegistry.cxxplus the registrations inpanda/src/movies/config_movies.cxx;wavAudio*/wavAudioCursor*is the simplest decoder to copy.Understand the Python wiring:
direct/src/showbase/Loader.py::loadSfx/loadSoundanddirect/src/showbase/ShowBase.py(sfxManagerList,musicManager, the per-frameupdate()loop), plusdirect/src/showbase/Audio3DManager.pyfor 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.