voice_audio_multi/tests/audio/stft_torch_parity.py
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

131 lines
4 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import subprocess
import sys
import tempfile
from pathlib import Path
import numpy as np
import soundfile as sf
import torch
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Compare audio.cpp STFT against torch.stft.")
parser.add_argument("wav", type=Path)
parser.add_argument("--dump-bin", type=Path, default=Path("build/debug/bin/stft_parity_dump"))
parser.add_argument("--kind", choices=("stft",), default="stft")
parser.add_argument("--n-fft", type=int, default=512)
parser.add_argument("--hop-length", type=int, default=160)
parser.add_argument("--win-length", type=int, default=400)
parser.add_argument("--pad-mode", choices=("constant", "reflect"), default="constant")
parser.add_argument("--preemphasis", type=float, default=0.0)
parser.add_argument("--periodic-window", action="store_true")
return parser.parse_args()
def run_dump(
dump_bin: Path,
wav: Path,
kind: str,
preemphasis: float,
n_fft: int,
hop_length: int,
win_length: int,
pad_mode: str,
) -> tuple[np.ndarray, tuple[int, ...]]:
with tempfile.TemporaryDirectory(prefix=f"stft-{kind}-") as tmp:
tmp_path = Path(tmp)
f32_path = tmp_path / "spec.f32"
shape_path = tmp_path / "shape.i64"
cmd = [str(dump_bin), str(wav), kind, str(f32_path), str(shape_path)]
if preemphasis != 0.0:
cmd.extend(["--preemphasis", str(preemphasis)])
cmd.extend(
[
"--n-fft",
str(n_fft),
"--hop-length",
str(hop_length),
"--win-length",
str(win_length),
"--pad-mode",
pad_mode,
]
)
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
shape = tuple(np.fromfile(shape_path, dtype=np.int64).tolist())
spec = np.fromfile(f32_path, dtype=np.float32).reshape(shape)
return spec, shape
def apply_preemphasis(audio: np.ndarray, coeff: float) -> np.ndarray:
if coeff == 0.0 or audio.size == 0:
return audio
out = audio.copy()
out[1:] = out[1:] - coeff * out[:-1]
return out
def compare(name: str, cpp_spec: np.ndarray, torch_spec: np.ndarray) -> None:
diff = cpp_spec - torch_spec
abs_diff = np.abs(diff)
idx = np.unravel_index(np.argmax(abs_diff), abs_diff.shape)
print(f"{name}:")
print(f" shape={cpp_spec.shape}")
print(f" max_abs={float(abs_diff.max())}")
print(f" mean_abs={float(abs_diff.mean())}")
print(f" rmse={float(np.sqrt(np.mean(diff * diff)))}")
print(f" worst_idx={idx}")
print(f" cpp_val={float(cpp_spec[idx])}")
print(f" torch_val={float(torch_spec[idx])}")
def main() -> int:
args = parse_args()
audio, sr = sf.read(args.wav, dtype="float32")
if sr != 16000:
raise RuntimeError(f"expected 16000 Hz WAV, got {sr}")
if audio.ndim != 1:
raise RuntimeError("expected mono WAV")
audio = apply_preemphasis(audio, args.preemphasis)
x = torch.from_numpy(audio)
window = torch.hann_window(
args.win_length,
periodic=args.periodic_window,
dtype=torch.float32,
)
spec = torch.stft(
x,
n_fft=args.n_fft,
hop_length=args.hop_length,
win_length=args.win_length,
window=window,
center=True,
pad_mode=args.pad_mode,
normalized=False,
onesided=True,
return_complex=True,
)
torch_spec = torch.view_as_real(spec).unsqueeze(0).numpy().astype(np.float32)
cpp_spec, _ = run_dump(
args.dump_bin,
args.wav,
args.kind,
args.preemphasis,
args.n_fft,
args.hop_length,
args.win_length,
args.pad_mode,
)
compare(args.kind, cpp_spec, torch_spec)
return 0
if __name__ == "__main__":
sys.exit(main())