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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
52440e13b0
commit
5b46655b82
@@ -0,0 +1,101 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user