Files
BT411/tools/sf2extract.py
T
arcattackandClaude Opus 4.8 5b46655b82 Audio: ENABLE sound -- real OpenAL + in-tree WAV loader; both backend DLLs were no-op STUBS (task #50)
Root cause of "no sound, ever": the two audio backend DLLs shipped in the repo
are fakes. libsndfile-1.dll exports 15 funcs BY ORDINAL ONLY (no names) and
sf_open() always returns NULL; OpenAL32.dll (72KB) imports only KERNEL32 -- no
dsound/wasapi/winmm -- so it is a pure no-op that returns fake handles
(ctx=0x00000001, alGenSources->0) and never touches the hardware. The whole
render->device->buffer->source->play chain ran clean and silent.

Fixes:
- OpenAL32.dll: replace the stub with the real OpenAL Soft 1.25.2 Win32 build
  (imports AVRT/ole32/WINMM, real WASAPI backend). The exe imports the 25 AL
  funcs by NAME so it is a drop-in; alGenSources now yields a live source and
  alSourcePlay reaches AL_PLAYING.
- libsndfile: DROPPED entirely. It is replaced by LoadWavPCM() in L4AUDRES --
  a tiny RIFF/WAVE fmt+data reader that loads our soundbank WAVs (16-bit PCM)
  straight into the AL buffer. Removed the .lib/.dll from the link + copy and
  git-rm'd the stub. (This also kills the "ordinal 50 could not be located in
  libsndfile-1.dll" load-failure popup: adding an sf_strerror import bound to
  an ordinal the 2..16-only stub could not satisfy.)
- Soundbank: 241 samples cracked from AUDIO1/2.RES (SF2 v1.0) by
  tools/sf2extract.py into content/AUDIO/*.wav + the allPresets[2][128] table
  (audiopresets.cpp), replacing the zero-init btstubs stub. All 241 now load
  (alErr=0). BT_AUDIO_TEST plays buffer0 as proof-of-life; BT_AUDIO_LOG traces
  the chain.

Remaining: in-game triggering (AudioEntities on fire/step/engine/explosion)
so PlayNote fires during play -- next audio wave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:55:33 -05:00

102 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""sf2extract.py -- extract the BattleTech AWE32 SoundFonts (AUDIO1/2.RES) into
loose WAV files + generate the allPresets[] C++ table for the WinTesla port.
The banks are SoundFont **v1.0** (ifil 1.0, EMU8000): the `shdr` sample headers are
16 bytes {dwStart,dwEnd,dwStartLoop,dwEndLoop} (NO name/rate field -- the v2.0
46-byte layout does not apply). Samples are 16-bit mono PCM in `smpl`. Preset ->
sample resolves through phdr->pbag->pgen(inst=41)->inst->ibag->igen(sampleID=53)->shdr.
Output:
content/AUDIO/<PresetName>.wav (one per preset; the L4AUDRES loader sf_opens these)
game/reconstructed/audiopresets.cpp (the allPresets[2][100] table, replaces the
btstubs.cpp zero-init stub)
Sample rate: SF2 v1.0 shdr has no per-sample rate; these were authored at 22050 Hz
(SAMPLE_RATE below -- adjust if pitch is wrong). Loop is taken from the shdr loop
points (a real loop region -> LoopAtWill; else ForceStatic one-shot).
"""
import struct, os, wave
HERE = os.path.dirname(__file__)
AUDIO = os.path.join(HERE, "..", "content", "AUDIO")
OUT_CPP = os.path.join(HERE, "..", "game", "reconstructed", "audiopresets.cpp")
SAMPLE_RATE = 22050
def parse_bank(path):
d = open(path, "rb").read()
assert d[:4]==b"RIFF" and d[8:12]==b"sfbk"
def find(fc): p=d.find(fc); return p+8, struct.unpack_from("<I",d,p+4)[0]
smpl_o, smpl_s = find(b"smpl"); smpl = d[smpl_o:smpl_o+smpl_s]
pho,_=find(b"phdr"); pbo,_=find(b"pbag"); pgo,_=find(b"pgen")
ino,_=find(b"inst"); ibo,_=find(b"ibag"); igo,_=find(b"igen")
sho,shs=find(b"shdr") # 16-byte v1.0 records
def u16(o): return struct.unpack_from("<H",d,o)[0]
def genlk(base,g0,g1,oper):
for g in range(g0,g1):
op,amt=struct.unpack_from("<HH",d,base+g*4)
if op==oper: return amt
return None
nphdr = 0
while d[pho+nphdr*38:pho+nphdr*38+3] != b"EOP" and nphdr < 200:
nphdr += 1
out = []
for i in range(nphdr):
name=d[pho+i*38:pho+i*38+20].split(b"\x00")[0].decode("latin1").strip()
preset,bank,pbag=struct.unpack_from("<HHH",d,pho+i*38+20)
pbag2=struct.unpack_from("<H",d,pho+(i+1)*38+24)[0]
instId=None
for b in range(pbag,pbag2):
instId=genlk(pgo,u16(pbo+b*4),u16(pbo+(b+1)*4),41)
if instId is not None: break
sampId=None
if instId is not None:
ib=u16(ino+instId*22+20); ib2=u16(ino+(instId+1)*22+20)
for b in range(ib,ib2):
sampId=genlk(igo,u16(ibo+b*4),u16(ibo+(b+1)*4),53)
if sampId is not None: break
if sampId is None: continue
st,en,ls,le=struct.unpack_from("<IIII", d, sho+sampId*16)
looping = (le > ls + 8) and (le <= en)
out.append((preset, name, smpl[st*2:en*2], looping))
return out
def write_wav(name, pcm):
with wave.open(os.path.join(AUDIO, name+".wav"), "wb") as w:
w.setnchannels(1); w.setsampwidth(2); w.setframerate(SAMPLE_RATE)
w.writeframes(pcm)
def main():
banks = {1:"AUDIO1.RES", 2:"AUDIO2.RES"}
table = {1:{}, 2:{}}
for bn, fn in banks.items():
presets = parse_bank(os.path.join(AUDIO, fn))
for (preset, name, pcm, looping) in presets:
if not name or not pcm: continue
write_wav(name, pcm)
table[bn][preset] = (name, looping)
print(f"bank {bn} ({fn}): {len(table[bn])} presets extracted to WAV")
# emit allPresets[2][100]
with open(OUT_CPP, "w", newline="\n") as f:
f.write('// GENERATED by scratchpad/sf2extract.py -- the audio soundbank table.\n')
f.write('// Replaces the zero-init allPresets stub (btstubs.cpp) with the real\n')
f.write('// bank/preset -> sample map recovered from the AWE32 SoundFonts\n')
f.write('// (AUDIO1/2.RES, SF2 v1.0). Each preset -> a loose <Name>.wav the\n')
f.write('// L4AUDRES loader sf_opens. Regenerate: python scratchpad/sf2extract.py\n')
f.write('#include <L4AUDLVL.hpp>\n\n')
f.write('PRESETINFO allPresets[2][128] = {};\n\n')
f.write('namespace {\nstruct AudioPresetInit {\n\tAudioPresetInit() {\n')
for bn in (1,2):
for preset, (name, looping) in sorted(table[bn].items()):
loop = "SampleLoop::LoopAtWill" if looping else "SampleLoop::ForceStatic"
f.write(f'\t\t{{ SAMPLEINFO &s = allPresets[{bn-1}][{preset}].samples[0];\n')
f.write(f'\t\t allPresets[{bn-1}][{preset}].sampleNum = 1;\n')
f.write(f'\t\t s.implemented = true; s.bufferIndex = -1; s.file = "{name}.wav";\n')
f.write(f'\t\t s.chan = SampleChannel::CHANNEL_CENTER; s.loop = {loop}; }}\n')
f.write('\t}\n} s_audioPresetInit;\n}\n')
total = len(table[1]) + len(table[2])
print(f"wrote {OUT_CPP} ({total} presets)")
if __name__ == "__main__":
main()