meridian59/common.mak
Adrien Laws e1b4aa8fef
feature: add ogg streaming playback for music (#1388)
## 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

```
2026-03-21 21:12:24 -07:00

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);