173 lines
6.7 KiB
Python
173 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""CPU benchmark for each FX block — measures per-block processing time.
|
|
|
|
Usage:
|
|
python scripts/benchmark_fx.py # All effects, 100 iterations
|
|
python scripts/benchmark_fx.py --effect delay # Single effect
|
|
python scripts/benchmark_fx.py --iters 500 # More iterations
|
|
python scripts/benchmark_fx.py --csv # CSV output
|
|
|
|
Results report:
|
|
- Mean, min, max time (microseconds) per 256-sample block
|
|
- Whether the effect meets the < 500 us (0.5 ms) target
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
from src.dsp.pipeline import AudioPipeline
|
|
from src.presets.types import FXBlock, FXType, Preset
|
|
|
|
# ── Test tone parameters ───────────────────────────────────────────
|
|
|
|
BLOCK = (
|
|
np.sin(2 * np.pi * 440.0 * np.arange(BLOCK_SIZE) / SAMPLE_RATE)
|
|
.astype(np.float32) * 0.5
|
|
)
|
|
SILENCE = np.zeros(BLOCK_SIZE, dtype=np.float32)
|
|
|
|
# ── Effect parameter profiles ──────────────────────────────────────
|
|
|
|
FX_PROFILES: list[tuple[str, FXType, dict]] = [
|
|
("noise_gate", FXType.NOISE_GATE, {"threshold": 0.01, "release": 100.0}),
|
|
("compressor", FXType.COMPRESSOR, {"threshold": -20.0, "ratio": 4.0,
|
|
"attack": 5.0, "release": 100.0, "gain": 1.0}),
|
|
("boost", FXType.BOOST, {"gain_db": 6.0}),
|
|
("overdrive", FXType.OVERDRIVE, {"drive": 0.5, "tone": 0.5, "gain": 1.0}),
|
|
("distortion", FXType.DISTORTION, {"drive": 0.7, "tone": 0.5, "gain": 1.0}),
|
|
("fuzz", FXType.FUZZ, {"drive": 0.8, "tone": 0.5, "gain": 1.0}),
|
|
("eq", FXType.EQ, {"bass": 6.0, "mid": 3.0, "treble": -3.0,
|
|
"bass_freq": 200.0, "mid_freq": 1000.0,
|
|
"treble_freq": 3500.0, "q": 0.707}),
|
|
("chorus", FXType.CHORUS, {"rate": 0.5, "depth": 0.5, "mix": 0.5,
|
|
"delay": 20.0}),
|
|
("flanger", FXType.FLANGER, {"rate": 0.25, "depth": 0.7, "feedback": 0.3,
|
|
"mix": 0.5, "delay": 5.0}),
|
|
("phaser", FXType.PHASER, {"rate": 0.4, "depth": 0.5, "feedback": 0.3,
|
|
"mix": 0.5, "stages": 4}),
|
|
("tremolo", FXType.TREMOLO, {"rate": 4.0, "depth": 0.7, "shape": "sine"}),
|
|
("vibrato", FXType.VIBRATO, {"rate": 3.0, "depth": 0.5}),
|
|
("delay", FXType.DELAY, {"time": 400.0, "feedback": 0.3, "mix": 0.4}),
|
|
("reverb", FXType.REVERB, {"decay": 0.5, "damping": 0.4, "mix": 0.3,
|
|
"predelay": 30.0}),
|
|
("volume", FXType.VOLUME, {"level": 0.8}),
|
|
]
|
|
|
|
|
|
def benchmark_effect(
|
|
fx_type: FXType,
|
|
params: dict,
|
|
iterations: int = 100,
|
|
) -> dict:
|
|
"""Time one effect over N iterations. Returns timing stats."""
|
|
pipeline = AudioPipeline()
|
|
block = FXBlock(fx_type=fx_type, enabled=True, bypass=False, params=params)
|
|
preset = Preset(name="bench", chain=[block], master_volume=1.0)
|
|
pipeline.load_preset(preset)
|
|
|
|
# Warm-up: process a few blocks to initialise state (delay buffers, etc.)
|
|
for _ in range(5):
|
|
pipeline.process(BLOCK)
|
|
|
|
# Timing loop
|
|
times = np.zeros(iterations, dtype=np.float64)
|
|
|
|
# Alternate between tone and silence to exercise stateful effects
|
|
for i in range(iterations):
|
|
inp = BLOCK if i % 2 == 0 else SILENCE
|
|
t0 = time.perf_counter()
|
|
pipeline.process(inp)
|
|
t1 = time.perf_counter()
|
|
times[i] = (t1 - t0) * 1e6 # microseconds
|
|
|
|
return {
|
|
"mean_us": float(np.mean(times)),
|
|
"min_us": float(np.min(times)),
|
|
"max_us": float(np.max(times)),
|
|
"std_us": float(np.std(times)),
|
|
"passes": float(np.mean(times)) < 500.0,
|
|
}
|
|
|
|
|
|
def run_all(iterations: int, csv_mode: bool) -> None:
|
|
results = []
|
|
|
|
print(f"FX Block Benchmark — {iterations} iterations per effect")
|
|
print(f"Block size: {BLOCK_SIZE} samples @ {SAMPLE_RATE} Hz")
|
|
print(f"Target: < 500 µs per block (< 0.5 ms)")
|
|
print()
|
|
print(f"{'Effect':<16} {'Mean (µs)':>10} {'Min (µs)':>10} {'Max (µs)':>10} "
|
|
f"{'Std (µs)':>10} {'Pass':>6}")
|
|
print("-" * 66)
|
|
|
|
for name, fx_type, params in FX_PROFILES:
|
|
stats = benchmark_effect(fx_type, params, iterations)
|
|
results.append((name, stats))
|
|
|
|
if csv_mode:
|
|
continue
|
|
|
|
pass_mark = "PASS" if stats["passes"] else "FAIL"
|
|
print(f"{name:<16} {stats['mean_us']:>10.1f} {stats['min_us']:>10.1f} "
|
|
f"{stats['max_us']:>10.1f} {stats['std_us']:>10.1f} "
|
|
f"{pass_mark:>6}")
|
|
|
|
if csv_mode:
|
|
print("name,mean_us,min_us,max_us,std_us,passes")
|
|
for name, stats in results:
|
|
print(f"{name},{stats['mean_us']:.1f},{stats['min_us']:.1f},"
|
|
f"{stats['max_us']:.1f},{stats['std_us']:.1f},{stats['passes']}")
|
|
|
|
# Summary
|
|
passed = sum(1 for _, s in results if s["passes"])
|
|
total = len(results)
|
|
print()
|
|
print(f"Results: {passed}/{total} effects pass the < 500 µs target")
|
|
|
|
if passed < total:
|
|
failing = [n for n, s in results if not s["passes"]]
|
|
print(f"Failing: {', '.join(failing)}")
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Benchmark per-FX-block CPU time",
|
|
)
|
|
parser.add_argument(
|
|
"--effect", "-e", type=str, default=None,
|
|
help="Benchmark a single effect by name (e.g. 'delay', 'reverb')",
|
|
)
|
|
parser.add_argument(
|
|
"--iters", "-i", type=int, default=100,
|
|
help="Number of iterations per effect (default: 100)",
|
|
)
|
|
parser.add_argument(
|
|
"--csv", action="store_true",
|
|
help="Output CSV format",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if args.effect:
|
|
matches = [(n, t, p) for n, t, p in FX_PROFILES if n == args.effect]
|
|
if not matches:
|
|
available = ", ".join(n for n, _, _ in FX_PROFILES)
|
|
print(f"Unknown effect '{args.effect}'. Choose from: {available}")
|
|
sys.exit(1)
|
|
name, fx_type, params = matches[0]
|
|
stats = benchmark_effect(fx_type, params, args.iters)
|
|
print(f"{name}: mean={stats['mean_us']:.1f}µs, min={stats['min_us']:.1f}µs, "
|
|
f"max={stats['max_us']:.1f}µs, std={stats['std_us']:.1f}µs, "
|
|
f"{'PASS' if stats['passes'] else 'FAIL'} < 500µs target")
|
|
sys.exit(0)
|
|
|
|
run_all(args.iters, args.csv)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |