Files
NX2navi/tests/audio_mixer_check.py
chrg6 42d16e7f0b Ramp a changed volume across the audio frame instead of applying it at once.
The mixer applied each voice's mix volume and each splitter destination's
volume to a whole 5 ms frame as one constant, so the level jumped at the
frame's first sample whenever the game moved a volume, which is heard as a
click. It now keeps what each voice and each splitter destination was mixed
with in the frame before and goes from there to the new value in equal steps
across the frame's samples, as the console's renderer does. An unchanged
volume stays on the constant path.

A voice that has just started, one that was silent in the frame before or was
moved to another mix or splitter, a destination that is new or was moved, and
the first frame after the renderer starts take the new volume at once.

NX2NAVI_AUDIO_RAMP=0 turns the ramps off. The audio statistics line counts the
render passes that ramped a volume. tests/audio_mixer_check.py builds the
mixer with the rest of the runtime stubbed and checks the ramps and the cases
that must not ramp.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 15:17:48 -05:00

43 lines
1.9 KiB
Python

"""Build and run tests/audio_mixer_check.c: runtime/audio_mixer.c with the rest of the runtime stubbed. No game files
and no audio device (the mixer runs as with NX2NAVI_AUDIO=none); SDL3 is only linked.
Windows: python tests\\audio_mixer_check.py [--toolchain C:/msys64/ucrt64]
Linux: python tests/audio_mixer_check.py (clang and SDL3 development files via pkg-config)
"""
import argparse
import os
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS = Path(r'C:\msys64\ucrt64')
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--toolchain', type=Path, default=MSYS if os.name == 'nt' and MSYS.exists() else None)
a = ap.parse_args()
work = ROOT / 'build' / 'test' / 'audio_mixer'
work.mkdir(parents=True, exist_ok=True)
env = dict(os.environ)
if a.toolchain:
env['PATH'] = str(a.toolchain / 'bin') + os.pathsep + env.get('PATH', '')
cc = os.environ.get('CC') or str(a.toolchain / 'bin' / 'clang.exe')
sdl = ['-L', str(a.toolchain / 'lib'), '-lSDL3']
else:
cc = os.environ.get('CC') or shutil.which('clang') or 'cc'
sdl = shlex.split(subprocess.check_output(['pkg-config', '--cflags', '--libs', 'sdl3'], text=True))
exe = work / ('audio_mixer_check.exe' if os.name == 'nt' else 'audio_mixer_check')
# the default build's target (x86-64-v3: AVX2 and FMA), so the mixing loops are compiled as they ship
subprocess.check_call([cc, '-std=gnu11', '-O2', '-fno-strict-aliasing', '-march=x86-64-v3', '-Wall',
'-Wno-unused-function', '-I', str(ROOT / 'runtime'),
str(ROOT / 'tests' / 'audio_mixer_check.c'), '-o', str(exe), *sdl, '-lm'], env=env)
return subprocess.run([str(exe)], env=env, timeout=60).returncode
if __name__ == '__main__':
sys.exit(main())