Songs, the media library and video: the public contract
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Checked by reading modules/media, the Song content reader and the tag parser at 009d40f5; the two fragments were syntax-checked with g++ -fsyntax-only against TARGET headers. Nothing was executed; decoding on the excluded video targets and real audio output remain unverified.
Microsoft::Xna::Framework::Media in CNA is no longer the thin shell older counts described: it holds file-backed songs and libraries, playlists, a queued music player, visualization data, picture libraries and video that decodes wherever the optional FFmpeg backend is built. This page states the public contract a game can rely on, with the cases where CNA follows XNA against FNA, where a file name resolving is not the same as a file playing, and where the platform decides. The scan, queue and decoder internals are traced for maintainers on Media internals and the FFmpeg video boundary; the task guides are Tutorial 15, Tutorial 122 and Video Playback.
Why audio and media form a cycle
cna_audio and cna_media depend on each other on purpose. FrameworkDispatcher::Update lives in the audio module, because the dynamic-stream list it walks is audio state, yet it advances MediaPlayer and raises the deferred media events; in the other direction MediaPlayer and VideoPlayer play through the audio mixer. The CMake files declare this static-archive cycle and let the linker repeat the archives. Removing either edge would mean moving the dispatcher and the playback responsibilities, so a change on one side needs a review of the other.
The Song contract
A Song is created three ways: by ContentManager (a loose music file or an XNB/CNB reference), by a MediaLibrary scan, or directly (Song.cpp).
- Construction. The CNAEXT constructor
Song(fileName, name)throwsSystem::IO::FileNotFoundExceptionnaming the path when the file does not exist, including a path the platform cannot even represent.Song::FromUri(name, uri)returns a heapSong*the caller owns. It accepts a plain path or afile:URI and refuses every other scheme withInvalidOperationException("Only local file URIs are supported for now.", FNA's message); it strips the query and fragment before percent-decoding, so an encoded%3For%23stays part of the file name, and it turnsfile://host/pathinto a UNC path rather than silently dropping the host (only an empty host orlocalhostmeans this machine). TheSystem::Urioverload uses the URI's original string, which keeps relative URIs usable. - Library links.
Album,ArtistandGenreare non-owning pointers, null for a directly constructed song and filled only by a library scan from the library's own hierarchy. - Ratings.
IsRatedand the 0–10Ratingcome from an ID3v2POPMframe or a VorbisRATINGcomment. A source rating of zero means "unrated" in both, so it leavesIsRatedfalse rather than reporting a rating of 0; a non-zeroPOPMbyte maps toround(byte × 10 / 255)clamped to 1–10, and a Vorbis value is read on a 0–100 scale (round(value / 10)clamped to 1–10; a non-numeric value is ignored). VorbisRATINGhas no standard scale, so a tagger that writes 0–5 or 0–10 values is misread:RATING=5becomes aRatingof 1. The mapping is CNA's policy, not an XNA rule: XNA defines only the 0–10 property. - Protection.
IsProtectedis always false: the library scan indexes only plain, unencrypted containers, so no protected song can exist. - Mutable counters.
DurationandPlayCounthave CNAEXT setters and change during playback: playing replaces the duration with the one the decoder reports and increments the play count, andMediaPlayer::Stop()resets play counts across the current queue. Which object receives those writes depends on the entry point, as traced on Media internals: the queue. - Identity. The CNAEXT
getHandle()returns the path the song plays from.Equalscompares handles andGetHashCodehashes the handle, so two equal songs always hash alike; that deliberately avoids FNA's inconsistency of an identity hash paired with a value comparison.ToString()returns the song's name. CNA's source comment records this as an inference fromAlbum,Artist,GenreandPlaylist, which all return their names; the decompiled XNA 4.0Song.ToString()in thexna4-decompreference confirms it (return Name;after a disposed check), so the inference holds.
std::unique_ptr<Song> intro(Song::FromUri("Intro Theme", "Content/Audio/intro.ogg"));
MediaPlayer::Play(intro.get()); // the player queues its own copy; the caller still owns intro
Syntax-checked against the TARGET headers with <memory> and the Media header included (not executed).
FNA is not the complete API oracle
FNA's Song omits Album, Artist, Genre and ToString(), all of which are part of the XNA 4.0 surface. Reviews that compared CNA only with FNA missed the difference; a comparison with the recorded XNA 4.0 API data found it, and CNA follows XNA. API parity with FNA and API parity with XNA are separate claims, and where they differ the XNA surface is the target.
A name that resolves is not a file that plays
Three lists disagree at this snapshot, and the difference matters when a song is "found" but silent:
| List | Extensions | Where |
|---|---|---|
Loose files ContentManager tries for a Song, in order | .mp3, .ogg, .wav, .flac, .opus, .aac, .wma | SongTypeReader in ContentManager.cpp |
Files a MediaLibrary scan indexes | .ogg, .oga, .mp3, .wav, .flac, .opus | MediaLibraryIndex.cpp |
| What the mixers decode | WAV, Ogg Vorbis, MP3 and FLAC on both mixers; no AAC, WMA or Opus on either | SDL3_mixer as configured by CNA; CNA's own mixer under ALSA |
So a loose .aac, .wma or .opus file resolves to a Song and then fails to play, and the library scan, whose comment says it indexes only what the bundled mixer can play, still lists .opus files although CNA's configure switches SDL3_mixer's Opus decoder off and CNA's own mixer refuses Ogg Opus by name. A MediaPlayer::Play of an existing file that cannot be decoded does not throw: the player cleans up and leaves its state as it was, so check getStateProperty() rather than assuming the request reached the speakers. Treat the extension lists as naming rules, not as decoder support; re-encode AAC, WMA and Opus music to Ogg Vorbis, MP3 or FLAC.
MediaLibrary in one paragraph
A MediaLibrary is a synchronous, construction-time snapshot: its constructor scans the platform's music and pictures folders (SDL's user folders on SDL3, the Windows known folders on Win32, XDG user directories elsewhere), builds the SongCollection, Album, Artist, Genre, Playlist and picture graphs it owns, and never rescans; there is no Refresh(), so files added later need a new library. Tags come from CNA's own parser (Vorbis comments, Opus tags, ID3v2.3/2.4 with Latin-1, UTF-16 and UTF-8 text, native FLAC comments and pictures), untagged files fall back to file name, parent folder and grandparent folder for title, album and artist, .m3u/.m3u8 playlists are read as UTF-8 with missing or escaping entries skipped, and album art prefers a folder image over embedded art. SavePicture() is the only write and creates Saved Pictures on first use. Tests redirect the roots with an internal override (MediaLibraryPaths::SetMusicRootOverride) so they do not depend on the CI account's files. One lesson from this code is recorded on Media internals: the tag parser once read the track number correctly while the library dropped it when building the Song, which only a fixture with several distinct track numbers checked after library construction could expose; a parser-only test could not.
MediaPlayer: queue, modes and events
- Ownership.
MediaQueue::Add(Song*)adopts the pointer, whileSongCollectionandPlaylist(read-only, as in XNA) only point into the library. To keep the queue from deleting library-owned songs,MediaPlayer::Playcopies every song it is given and queues the copies. - Navigation. Without repeat, next and previous clamp at the ends; with
IsRepeatingthe end wraps to the start. Shuffle chooses uniformly from the whole queue, so the same song can play twice in a row. - End of a song. In a build with a mixer, the mixer's stopped callback sets a flag that the next dispatcher update turns into advancing the queue. Without a mixer (SDL2 or NULL) the player instead compares elapsed time with the duration of the queue's active song and ignores a zero duration. Every queued song is a copy made with the two-argument constructor (
MediaPlayer::LoadSong), which carries no duration, and the mixer-less path never sets one, so in those builds a song never auto-advances or ends on its own: the state staysPlayinguntil the game callsStopor a navigation method. - Volume and mute.
Volumeis clamped to 0–1 andIsMutedis independent state: setting the volume while muted stores the new value for the next unmute, and only the mixer track receives zero gain while muted. - Events.
MediaStateChangedandActiveSongChangedare never raised from the audio thread; they are deferred toFrameworkDispatcher::Update()and coalesced to one raise per pump.Gamepumps the dispatcher during its normal update, but a caller using the static media API outsideGame(a tool, a test) must callFrameworkDispatcher::Update()itself, or songs never advance and no event fires.
Visualization data
MediaPlayer::GetVisualizationData() reads samples that the mixer's post-mix callback writes into a lock-free single-producer, single-consumer ring of std::atomic<float> with relaxed ordering, so the audio-thread writes and game-thread reads are defined C++ behaviour without locks, allocation or exceptions on the audio thread. A reader may see samples spanning two adjacent callback batches; the resulting one-frame imprecision is accepted. A 512-sample Hann-windowed radix-2 FFT produces XNA's 256 frequency bins (VisualizationFFT.cpp). The magnitudes are scaled by 2/N and the window's coherent gain of one half is deliberately left uncompensated, so by that arithmetic a full-scale sine at a bin centre peaks near 0.5 (the source comment describes the target as about 1.0, and the test asserts only that the peak lies in the tone's own bin and exceeds 0.1). XNA documented no normalization, so treat the values as relative. Disabled or not-yet-fed visualization returns zero-filled arrays. The enabled flag is assigned only from what actually happened when the mixer tap was installed or removed, so it never claims a callback that is absent; the media tests run with a working (dummy-driver) mixer, however, so that failed-installation branch is correct by construction rather than by test, as the test's own comment states.
Video and VideoPlayer
Where the FFmpeg backend was built, VideoPlayer decodes with FFmpeg, uploads each frame into a renderer texture and streams the audio through the mixer. The behaviours a caller sees:
Play()compares theVideo's declared width, height and frame rate with the decoded stream and throwsInvalidOperationExceptionon a mismatch (with a one-frame-per-second tolerance on the rate).VideoSoundtrackTypeis metadata only, in CNA as in FNA: no code path ducks or mutes a track because of it.Play,Stop,Pause,Resume,GetTextureand the two track switches throwObjectDisposedException("VideoPlayer")after disposal, FNA's message; the property accessors do not check.Dispose()itself is idempotent, unlike FNA's, because the C++ destructor also calls it and a second explicit disposal must not throw from inside a destructor.SetAudioTrackEXTandSetVideoTrackEXTselect streams independently: a live audio switch reopens only the audio output at the new track's rate, and a genuine live video switch recreates the frame texture at the new track's size on every switch (ReconfigureVideoOutputForCurrentTrack), whether or not the dimensions differ, and does not seek, so decoding continues from the current position in the new stream. Re-selecting the current track or an out-of-range one does nothing. The queued audio is cleared on every close, even when no audio device or audio track is active, so samples cannot accumulate or leak into a later play.
VideoPlayer player;
player.Play(&introVideo); // throws NotSupportedException in a build without the backend
player.SetAudioTrackEXT(1); // switches audio without recreating the video output
Syntax-checked against the TARGET headers with introVideo as a Video (not executed).
Platform availability, then and now
The FFmpeg backend is optional (CNA_ENABLE_VIDEO=AUTO|ON|OFF) and is never built for Windows (MinGW and MSVC), Emscripten, Android or iOS. The alpha.1-era design excluded the video translation units on those targets, so using Video there failed at link time, and a native-Windows preprocessor guard in ContentManager.cpp did not match the CMake condition. Both are history at this snapshot: Video and VideoPlayer are always compiled, a small unavailable decoder keeps the API link-complete, ContentManager.cpp includes the video header unconditionally, and a file-backed video throws NotSupportedException at run time instead. The configure-time switch, the fixtures and what they prove are on Video Playback: platform availability and the FFmpeg boundary's validation section.
Evidence
Source and focused tests establish tag parsing, hierarchy construction, queue ownership, event timing, visualization state, video disposal and multi-track reconfiguration; FFmpeg fixture tests exercise decoding on a configured host. None of that establishes video on the targets where the backend is never built, nor does an extension list establish decoder support. Nothing on this page was executed.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.