Route beat-triggered manual selects from the controller server, add preset background and beat-counter UI support, and bump led-driver to include the matching pattern/runtime fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
76 lines
2.4 KiB
Python
76 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a click-track WAV file at a known BPM.
|
|
|
|
Example:
|
|
python tests/make_bpm_test_audio.py --bpm 128 --seconds 60 --output tests/audio_128bpm.wav
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import math
|
|
import struct
|
|
import wave
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Generate known-BPM click track")
|
|
parser.add_argument("--bpm", type=float, required=True, help="Target BPM (e.g. 120)")
|
|
parser.add_argument("--seconds", type=float, default=30.0, help="Audio duration in seconds")
|
|
parser.add_argument("--sample-rate", type=int, default=44100, help="Sample rate")
|
|
parser.add_argument("--click-ms", type=float, default=25.0, help="Click length in milliseconds")
|
|
parser.add_argument(
|
|
"--output",
|
|
default="tests/bpm_test.wav",
|
|
help="Output WAV path",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
if args.bpm <= 0:
|
|
raise SystemExit("--bpm must be > 0")
|
|
if args.seconds <= 0:
|
|
raise SystemExit("--seconds must be > 0")
|
|
|
|
sample_rate = int(args.sample_rate)
|
|
total_samples = int(args.seconds * sample_rate)
|
|
beat_interval = 60.0 / float(args.bpm)
|
|
click_samples = max(1, int((args.click_ms / 1000.0) * sample_rate))
|
|
|
|
data = [0.0] * total_samples
|
|
beat_index = 0
|
|
t = 0.0
|
|
while t < args.seconds:
|
|
start = int(t * sample_rate)
|
|
# Slight accent every 4 beats to help human counting.
|
|
freq = 1760.0 if beat_index % 4 == 0 else 1320.0
|
|
amp = 0.9 if beat_index % 4 == 0 else 0.6
|
|
for i in range(click_samples):
|
|
idx = start + i
|
|
if idx >= total_samples:
|
|
break
|
|
env = math.exp(-8.0 * (i / click_samples))
|
|
s = amp * env * math.sin((2.0 * math.pi * freq * i) / sample_rate)
|
|
data[idx] += s
|
|
t += beat_interval
|
|
beat_index += 1
|
|
|
|
with wave.open(args.output, "wb") as wf:
|
|
wf.setnchannels(1)
|
|
wf.setsampwidth(2)
|
|
wf.setframerate(sample_rate)
|
|
frames = bytearray()
|
|
for s in data:
|
|
clipped = max(-1.0, min(1.0, s))
|
|
frames.extend(struct.pack("<h", int(clipped * 32767)))
|
|
wf.writeframes(bytes(frames))
|
|
|
|
print(f"Wrote {args.output} at {args.bpm:.2f} BPM for {args.seconds:.1f}s")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|