feat(audio): move beat routing server-side and extend presets
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>
This commit is contained in:
75
tests/make_bpm_test_audio.py
Normal file
75
tests/make_bpm_test_audio.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user