• src/doors/termgfx/audio_m

    From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Mon Jun 29 20:25:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/2f4543c6054bae5c01afabfd
    Added Files:
    src/doors/termgfx/audio_midi.c audio_midi.h
    Modified Files:
    src/doors/syncduke/.gitignore CMakeLists.txt syncduke.h syncduke_input.c syncduke_io.c syncduke_stubs.c src/doors/termgfx/CMakeLists.txt audio.c audio.h audio_mgr.c audio_mgr.h
    Log Message:
    syncduke: OPL3 MIDI music + digital-audio polish

    Builds on the SFX path (5b29253803 diabetes-21-onto) with in-process music
    and a round of audio fixes, all through termgfx's SyncTERM audio-APC manager:

    - Music: render the GRP's MIDIs via libADLMIDI (OPL3), peak-normalized, cached
    as OGG/Vorbis (libsndfile, build-gated TERMGFX_WITH_SNDFILE) under a content-
    addressed m/<track>.ogg so a track uploads once; raw-WAV fallback otherwise. - SFX: transcode the VOCs libsndfile rejects (multi-block) to 8-bit WAV.
    - Setup Sound volume sliders wired (FX + music master) and SOUND/MUSIC toggles. - Looping ambient (FX_PlayLooped*) on dedicated channels; stop on door exit.
    - soundonce flood fix: a door-side per-sound rate limit. Returning a real voice
    handle so the engine's Sound[].num gate works would deadlock newgame()'s
    busy-wait on the skill-announce voice (the engine assumes an async audiolib
    completion source our APC path lacks), so instead we drop a same-sound
    re-dispatch inside an 8-tick window -- collapsing a per-tic soundonce stream
    to one burst while leaving real weapon fire untouched.

    New termgfx audio module (audio_midi.{c,h}) + audio.c/audio_mgr.c additions; syncduke wires them through its FX_/MUSIC_ stubs.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Jun 30 22:32:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/85a351a3d0d739779d4fc9d8
    Modified Files:
    src/doors/termgfx/audio_mgr.c
    Log Message:
    termgfx: atomic audio-cache write + bounded C;L capture

    - The door-side OGG cache write goes through a per-pid temp then rename(), so a
    concurrent render of the same (content-addressed) track by two nodes, or a
    write cut short by a full disk / door exit, always yields a complete file.
    POSIX rename is atomic; Windows removes the target first (its rename won't
    replace an existing one).
    - The C;L capture buffer is bounded (256KB) so an oversized or unterminated
    reply can't grow the buffer without bound.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Wed Jul 1 02:20:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/3ccf90eda0d43776acde7f9f
    Modified Files:
    src/doors/termgfx/audio_mgr.c
    Log Message:
    termgfx: make async music transcode portable via threadwrap/semwrap

    The worker that renders music (MIDI/MUS -> PCM -> OGG -> C;S upload APC)
    off the game thread was POSIX-only. Move it onto xpdev's cross-platform wrappers so it works in Windows builds too:

    - mutex + thread create: threadwrap (pthread_mutex_* -- native pthreads
    on *nix, Win32 critical sections on Windows; _beginthread)
    - worker wake: a semwrap semaphore (native POSIX sem / Win32 HANDLE)
    in place of the pthread condvar
    - shutdown "join": _beginthread makes a detached thread on every
    platform (no joinable handle), so termgfx_audio_destroy() posts the
    wake semaphore and waits on a worker-set exit flag rather than
    pthread_join (which threadwrap lacks)

    TERMGFX_ASYNC_MUSIC is now always on; the sync fallback remains behind
    the macro for debugging. No behavior change on *nix -- validated end to
    end (create -> submit real SMF -> poll SHIPPED -> destroy joins cleanly;
    idle create/destroy clean) and both doors build clean under GCC -Werror.
    The Windows leg uses the same wrappers the rest of Synchronet relies on
    but is unverified by an MSVC compile here.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Mon Jul 6 20:44:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/dcb6be63ca06fa49e3557126
    Modified Files:
    src/doors/termgfx/audio_mgr.c
    Log Message:
    termgfx: queue looping SFX at 25% so loops mix at one-shot levels

    SyncTERM's audio-APC volume is two independent gain stages (ticket #262, confirmed as-designed): the A;Queue volume is baked into the entry at
    queue time (0% floors to -60dB, unrecoverable) and A;Volume only moves
    the live channel gain, whose percentage form caps at unity. One-shots
    carry their level in the queue entry and ride the default -12dB channel
    base (sfx_dispatch never sends A;Volume); loops queue an entry and drive
    the channel percent up to 0dB -- queueing that entry at 100% therefore
    ran looping ambiences up to 12dB (4x) hotter than one-shots of equal
    nominal volume. Queue loop entries at 25% (~ -12dB) instead: loops and one-shots now mix at the same level across the whole range, a uniform
    -12dB shift with the distance-falloff shape unchanged (and it still
    matches the DOS audiolib's linear-amplitude MV_Pan3D curve).

    Rewrite the loop_start comment to this model, replacing the "SyncTERM
    will not un-mute a dead voice" theory from 2a84663b40 (card-4-shades):
    a 0%-queued voice isn't dead, it's -60dB baked, which live percentage
    volume can never recover -- the queue-active + A;Volume architecture
    stands, only the queue level changes.

    No client-cache impact: the queue volume is an A;Queue command
    parameter, not sample content, so cached samples play at the corrected
    level with no re-upload or version bump.

    Validated live over SyncTERM: up-close ambience level (E1L1 projector)
    now sits right alongside one-shot SFX.

    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thu Jul 9 01:55:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/35961c8931362fcc6d940d19
    Modified Files:
    src/doors/termgfx/audio_mgr.c audio_mgr.h
    Log Message:
    termgfx: add name-addressed SFX (store + play_named)

    The SFX pair keys its client-cache name on the caller's id ("<prefix>/sfx/<id>", audio_mgr.c's snprintf(leaf, "%d", id)) and Stores lazily, inside the play call, guarded by sfx_up[id]. Neither an eager
    Store nor a caller-supplied name is expressible, and syncmoo1 needs both:
    1oom registers every sample up front, before the audio tier is even
    known, and content-addressed names are what let a later cross-session
    upload skip be correct.

    Add termgfx_audio_sfx_store() and termgfx_audio_sfx_play_named(), keyed
    on a caller-supplied leaf with their own "Stored this session" table.
    The transcode-and-Store body is extracted to sfx_store_bytes() and shared
    with the id path, whose sfx_up[]/sfx_dur[] behaviour is unchanged.

    Additive: SyncDOOM and SyncDuke keep the id path byte-for-byte and both
    still build.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thu Jul 9 01:55:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/5ada5ab247cf590c48934f6f
    Modified Files:
    src/doors/termgfx/audio_mgr.c audio_mgr.h
    Log Message:
    termgfx_audio: reject over-long SFX cache names instead of
    truncating

    termgfx_audio_sfx_store() and _play_named() key a per-session
    table on a caller-supplied leaf name (a content hash, e.g.
    "s_<fnv1a8>"). The table stores leaves in a fixed 16-byte field
    copied via strncpy(), silently truncating longer leaves. When
    _play_named() later searches for the full leaf with strcmp(), it
    never matches the truncated stored copy -- the sample never
    plays.

    Add length checks to both functions' early-return guards so an
    over-long leaf is rejected (stores nothing, plays nothing) rather
    than half-stored under a name that can never be found again. Also
    replace strncpy() + manual NUL with snprintf() for clarity, and
    document the 16-byte constraint on the header declarations (the
    leaf must be shorter than NAMEDLEN, which is 16 bytes).

    Current callers use "s_<8-hex>" (10 chars), so they're unaffected.
    A future wider hash or longer name prefix would have hit this
    silently without the length check; it now fails cleanly.

    Test: all three doors (syncdoom/syncduke/syncmoo1) still build,
    and syncmoo1's unit tests (test_map, test_audio) both pass.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net