mirror of
https://github.com/Meridian59/Meridian59
synced 2026-08-17 18:23:09 -04:00
## What - replaced full-file OGG decoding with streaming playback so music transitions no longer freeze the game - related to [ #1293](https://github.com/Meridian59/Meridian59/pull/1293) ## Why - when music changes (room transitions, Jala song interrupts), `MusicPlay()` calls `stb_vorbis_decode_filename()` which decompresses the entire OGG file into raw PCM in a single blocking call on the main thread. - A typical 3.7 MB OGG (Main.ogg, 3:01) expands to ~30 MB of PCM, and the largest track (Castle2.ogg, 4:55) expands to ~50 MB - no rendering, input processing, or animation happens during this decode, so the game visibly stutters/locks for a moment - reported by a tester on older hardware, but the blocking call affects all clients - music is the only audio path affected. Sound effects use small one-shot files with buffer caching and are fine ## How - replaced full-file `stb_vorbis_decode_filename()` with the stb_vorbis streaming API: open the file, read headers, decode in 4096-sample chunks on demand - consolidated music globals into a `MusicStream` struct with a 4-buffer ring, vorbis handle, and state flags - `MusicPlay()` now opens the OGG, fills 4 initial buffers (~64 KB total), and starts playback in ~1ms - a Win32 timer calls MusicStreamUpdate() to rotate processed buffers (unqueue, decode next chunk, re-queue) with automatic underrun recovery - looping is handled by seeking the vorbis stream to the start on EOF, since OpenAL's `AL_LOOPING` only works on single buffers, not queued buffers - the timer approach means streaming works in every client state, including modal dialogs (e.g. login screen) that block the main loop - `music.c` unchanged, the `MusicPlay`/`MusicStop` API contract is the same - added stb to the global include path in common.mak so `audio_openal.c` can include the `stb_vorbis` header directly ### Streaming buffer parameters | Parameter | Value | Rationale | |-----------|-------|-----------| | Buffer count | 4 | Standard ring buffer depth, enough runway for frame rate drops | | Buffer size | 4096 samples | ~93ms per buffer at 44100 Hz | | Total runway | ~0.37 seconds | 4 x 93ms, survives typical frame spikes and timer intervals | | Total streaming memory | ~64 KB | vs 30-50 MB for full decode (470-780x reduction) | | Timer interval | 50ms | Below the ~93ms buffer duration, ensures at least 1 refill per buffer lifetime | ## Examples ### Before https://github.com/user-attachments/assets/6757e5c4-09da-4326-be63-162e4e104257 When a Jala song is interrupted by running, the new room music triggers a full OGG decode on the main thread. On a mid-range system this takes ~100-200ms, during which the player character visibly freezes. On older hardware it is much worse. ### After https://github.com/user-attachments/assets/fec2c139-0ad9-4aab-b230-160755928cdf `MusicPlay()` opens the file and reads headers only (~1ms). The first ~370ms of audio is decoded across 4 small buffers (~16KB each) during the initial call. Subsequent decoding happens via a Win32 timer calling MusicStreamUpdate(), with no perceptible impact on frame time. ### Architecture: Full Decode vs Streaming ```mermaid graph TD subgraph NEW["Streaming (New)"] direction TB N1["MusicPlay()"] N2["stb_vorbis_open_filename()<br/>Parse headers only, ~1ms"] N3["Fill 4 buffers<br/>4096 samples each = 16KB<br/>~0.37s of audio runway"] N4["alSourceQueueBuffers()<br/>alSourcePlay()"] N5["MusicStreamUpdate()<br/>Called via Win32 timer"] N6{"Buffers<br/>processed?"} N7["Unqueue, decode next chunk,<br/>re-queue"] N8["Source starved?<br/>alSourcePlay() recovery"] N1 --> N2 --> N3 --> N4 --> N5 --> N6 N6 -- Yes --> N7 --> N5 N6 -- "No, but stopped" --> N8 --> N5 end subgraph OLD["Full Decode (Old)"] direction TB O1["MusicPlay()"] O2["stb_vorbis_decode_filename()<br/>Decode entire OGG to PCM<br/>~31MB, blocks 100-500ms"] O3["alBufferData()<br/>Upload full PCM to 1 buffer"] O4["alSourcePlay()<br/>AL_LOOPING = TRUE"] O5["OpenAL audio thread<br/>plays indefinitely"] O1 --> O2 --> O3 --> O4 --> O5 end style O2 fill:#ff0000,color:#fff style N2 fill:#2f9e44,color:#fff ```
138 lines
3.3 KiB
Makefile
138 lines
3.3 KiB
Makefile
# stuff included in blakston makefiles
|
|
|
|
# defining RELEASE compiles optimized
|
|
# defining NODEBUG omits debugging information
|
|
# defining RETAIL implies release, and also removes debugging strings from client executable
|
|
|
|
!ifdef RETAIL
|
|
RELEASE = 1
|
|
NODPRINTFS = 1
|
|
!endif
|
|
|
|
!ifdef RELEASE
|
|
!undef DEBUG
|
|
OUTDIR=release
|
|
!else
|
|
DEBUG = 1
|
|
OUTDIR=debug
|
|
!endif
|
|
|
|
# set to get debugging info in executable
|
|
!ifdef NODEBUG
|
|
!undef DEBUGINFO
|
|
!else
|
|
DEBUGINFO = 1
|
|
!endif
|
|
|
|
# Set source directories
|
|
|
|
# Subdirectories of components
|
|
|
|
BBGUNDIR = $(TOPDIR)\bbgun
|
|
CLIENTDIR = $(TOPDIR)\clientd3d
|
|
BLAKSERVDIR = $(TOPDIR)\blakserv
|
|
BLAKCOMPDIR = $(TOPDIR)\blakcomp
|
|
ROOMEDITDIR = $(TOPDIR)\roomedit
|
|
DOCDIR = $(TOPDIR)\doc
|
|
DECODIR = $(TOPDIR)\blakdeco
|
|
MAKEBGFDIR = $(TOPDIR)\makebgf
|
|
RESOURCEDIR = $(TOPDIR)\resource
|
|
MODULEDIR = $(TOPDIR)\module
|
|
UTILDIR = $(TOPDIR)\util
|
|
SPROCKETDIR = $(TOPDIR)\sprocket
|
|
CLUBDIR = $(TOPDIR)\club
|
|
|
|
# 3rd party libraries
|
|
EXTERNALDIR = $(TOPDIR)\external
|
|
LIBARCHIVEDIR = $(EXTERNALDIR)\libarchive
|
|
LIBPNGDIR = $(EXTERNALDIR)\libpng
|
|
ZLIBDIR = $(EXTERNALDIR)\zlib
|
|
OPENALDIR = $(EXTERNALDIR)\openal-soft\openal-soft-1.24.3-bin
|
|
FMTLIBDIR = $(EXTERNALDIR)\fmtlib
|
|
STBDIR = $(EXTERNALDIR)\stb
|
|
|
|
BLAKBINDIR = $(TOPDIR)\bin
|
|
BLAKLIBDIR = $(TOPDIR)\lib
|
|
BLAKINCLUDEDIR = $(TOPDIR)\include
|
|
BLAKSERVRUNDIR = $(TOPDIR)\run\server
|
|
CLIENTRUNDIR = $(TOPDIR)\run\localclient
|
|
|
|
KODDIR = $(TOPDIR)\kod
|
|
KODINCLUDEDIR = $(KODDIR)\include
|
|
|
|
PALETTEFILE = $(TOPDIR)\blakston.pal
|
|
|
|
# compiler specs -- uses multi-threaded DLL C runtime library
|
|
# /FC displays full path of source code file in diagnostics
|
|
# /TP builds C files in C++ mode
|
|
# /WX treats warnings as errors
|
|
# /GR- turns off RTTI
|
|
# /EHsc- turns off exceptions
|
|
# /wd4996 disables warning (deprecated function called)
|
|
# /wd4312 disables warning (cast 32-bit value to 64-bit pointer)
|
|
# /MP enables parallel compiling
|
|
# /MT link with static C runtime library
|
|
# /Zi includes debugging information
|
|
# /DFMT_UNICODE=0 disables Unicode support for fmtlib
|
|
|
|
CCOMMONFLAGS = -nologo -DBLAK_PLATFORM_WINDOWS -DWIN32 -DFMT_UNICODE=0 \
|
|
/wd4996 /wd4312 /FC \
|
|
-TP -WX -GR- -EHsc- -MP -MT -Zi -std:c++20
|
|
|
|
CNORMALFLAGS = $(CCOMMONFLAGS) -W2 /Ox
|
|
CDEBUGFLAGS = $(CCOMMONFLAGS) -W3 -DBLAKDEBUG
|
|
CNODEBUGFLAGS = $(CCOMMONFLAGS) -W2 -DBLAKDEBUG
|
|
LINKNORMALFLAGS =/release /debug
|
|
LINKDEBUGFLAGS = /debug
|
|
LINKNODEBUGFLAGS =
|
|
LINKCONSOLEFLAGS = -subsystem:console
|
|
LINKWINDOWSFLAGS = -subsystem:windows
|
|
|
|
!ifdef DEBUG
|
|
|
|
!ifdef DEBUGINFO
|
|
CFLAGS = $(CDEBUGFLAGS)
|
|
LINKFLAGS = $(LINKDEBUGFLAGS)
|
|
!else
|
|
CFLAGS = $(CNODEBUGFLAGS)
|
|
LINKFLAGS = $(LINKNODEBUGFLAGS)
|
|
!endif DEBUGINFO
|
|
|
|
!else
|
|
CFLAGS = $(CNORMALFLAGS)
|
|
LINKFLAGS = $(LINKNORMALFLAGS)
|
|
|
|
!endif DEBUG
|
|
|
|
!ifdef DLL
|
|
LINKFLAGS = $(LINKFLAGS) /DLL
|
|
!endif
|
|
|
|
!ifdef NODPRINTFS
|
|
CFLAGS = $(CFLAGS) -DNODPRINTFS
|
|
!endif NODPRINTFS
|
|
|
|
# programs
|
|
|
|
CC = cl
|
|
MAKE = nmake -nologo
|
|
LIBPRG = lib -nologo
|
|
LINK = link -nologo
|
|
RC = rc -nologo
|
|
|
|
LEX = $(TOPDIR)\bin\flex -I -i
|
|
YACC = $(TOPDIR)\bin\bison -d -t
|
|
CP = copy /Y
|
|
RM = -del /Q
|
|
RMDIR = -rmdir
|
|
MV = move
|
|
LATEX = latex
|
|
MAKENSIS = "c:\program files (x86)\nsis\makensis.exe"
|
|
|
|
BC = $(BLAKBINDIR)\bc
|
|
MAKEBGF = $(BLAKBINDIR)\makebgf
|
|
|
|
# environment variables for compiler
|
|
|
|
LIB = $(LIB);$(BLAKLIBDIR)
|
|
INCLUDE = $(INCLUDE);$(BLAKINCLUDEDIR);$(LIBARCHIVEDIR);$(LIBPNGDIR);$(ZLIBDIR);$(OPENALDIR)\include;$(FMTLIBDIR);$(STBDIR);
|