85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Generate the audio fixtures `webapi_smoke.py` expects.
|
||
|
|
|
||
|
|
Synthesised rather than downloaded so the smoke test has no network dependency
|
||
|
|
and no external tools — `soundfile` and `numpy` come in with `librosa`, so this
|
||
|
|
runs in the container image as-is (no ffmpeg needed).
|
||
|
|
|
||
|
|
sine440.wav 12 s, 440 Hz, stereo, -12 dBFS — a known-tone reference
|
||
|
|
pinknoise.wav 8 s, pink noise, stereo — broadband, exercises the STFT
|
||
|
|
tagged.mp3 a tagged file — only if ffmpeg is available
|
||
|
|
|
||
|
|
Usage: python3 scripts/make_fixtures.py [output_dir] (default /tmp/ujm-test)
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import soundfile as sf
|
||
|
|
|
||
|
|
SR = 44100
|
||
|
|
|
||
|
|
|
||
|
|
def sine(path: Path, seconds: float = 12.0, hz: float = 440.0,
|
||
|
|
dbfs: float = -12.0) -> None:
|
||
|
|
"""A tone at a known level. Peak is exactly 10**(dbfs/20) before encoding."""
|
||
|
|
t = np.arange(int(seconds * SR)) / SR
|
||
|
|
mono = (10 ** (dbfs / 20)) * np.sin(2 * np.pi * hz * t)
|
||
|
|
sf.write(path, np.stack([mono, mono], axis=1), SR, subtype="PCM_16")
|
||
|
|
|
||
|
|
|
||
|
|
def pink(path: Path, seconds: float = 8.0, peak: float = 0.25) -> None:
|
||
|
|
"""Pink noise via 1/sqrt(f) shaping, normalised to a known peak."""
|
||
|
|
rng = np.random.default_rng(7)
|
||
|
|
n = int(seconds * SR)
|
||
|
|
spectrum = np.fft.rfft(rng.standard_normal(n))
|
||
|
|
freqs = np.fft.rfftfreq(n, 1 / SR)
|
||
|
|
freqs[0] = freqs[1] # avoid dividing by zero at DC
|
||
|
|
shaped = np.fft.irfft(spectrum / np.sqrt(freqs), n)
|
||
|
|
mono = peak * shaped / np.max(np.abs(shaped))
|
||
|
|
sf.write(path, np.stack([mono, mono], axis=1), SR, subtype="PCM_16")
|
||
|
|
|
||
|
|
|
||
|
|
def tagged_mp3(path: Path) -> bool:
|
||
|
|
"""An MP3 with ID3 tags, so the tag-reading path is exercised.
|
||
|
|
|
||
|
|
Genuinely optional: MP3 encoding is the one thing numpy cannot do, so this
|
||
|
|
needs ffmpeg and is skipped when it is absent. The suite's tag assertions
|
||
|
|
adapt — see `webapi_smoke.py`.
|
||
|
|
"""
|
||
|
|
if shutil.which("ffmpeg") is None:
|
||
|
|
return False
|
||
|
|
subprocess.run(
|
||
|
|
["ffmpeg", "-y", "-loglevel", "error",
|
||
|
|
"-f", "lavfi", "-i", "sine=frequency=330:duration=6:sample_rate=44100",
|
||
|
|
"-metadata", "artist=Test Artist", "-metadata", "title=Test Title",
|
||
|
|
"-codec:a", "libmp3lame", "-q:a", "2", str(path)],
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
out = Path(sys.argv[1] if len(sys.argv) > 1 else "/tmp/ujm-test")
|
||
|
|
out.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
sine(out / "sine440.wav")
|
||
|
|
pink(out / "pinknoise.wav")
|
||
|
|
mp3 = tagged_mp3(out / "テスト.mp3")
|
||
|
|
|
||
|
|
for f in sorted(out.iterdir()):
|
||
|
|
print(f" {f.name} {f.stat().st_size / 1024:.0f} KiB")
|
||
|
|
if not mp3:
|
||
|
|
print("\nnote: no ffmpeg, so no tagged MP3 fixture was written "
|
||
|
|
"(tag assertions will be skipped)")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|