mirror of
https://codeberg.org/virtualxt/virtualxt.git
synced 2026-10-21 14:24:47 +00:00
191 lines
7.6 KiB
Python
Executable File
191 lines
7.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Convert audio samples into embedded C data.
|
|
|
|
Reads:
|
|
- samples/servo_seeks_eq.wav (48kHz float32 mono, 40 seeks)
|
|
- samples/fan_idle_loop.wav (any format, mono)
|
|
|
|
Output: src/frontend/generated/audio_samples.h
|
|
|
|
Process:
|
|
1. Resample to 44100 Hz, convert to int16 mono via ffmpeg.
|
|
2. For servo seeks: detect silence gaps to split into individual seek sounds.
|
|
3. For fan: convert entire file to int16 PCM.
|
|
4. Generate a C header with sample data arrays and lookup tables.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import struct
|
|
import sys
|
|
|
|
# ── Paths ───────────────────────────────────────────────────────────────
|
|
|
|
SEEK_WAV = "samples/servo_seeks_eq.wav"
|
|
FAN_WAV = "samples/fan_idle_loop.wav"
|
|
OUT_DIR = "src/frontend/generated"
|
|
OUT_FILE = os.path.join(OUT_DIR, "audio_samples.h")
|
|
TARGET_RATE = 44100
|
|
|
|
# ── Silence detection (for seek splitting) ──────────────────────────────
|
|
|
|
SILENCE_THRESHOLD = 500 # int16 absolute value threshold
|
|
MIN_SILENCE_SAMPLES = 2205 # 50ms at 44100 Hz
|
|
|
|
# ── Helpers ─────────────────────────────────────────────────────────────
|
|
|
|
def ffmpeg_to_int16(path):
|
|
"""Resample and convert to int16 mono, return raw PCM bytes."""
|
|
cmd = [
|
|
"ffmpeg", "-y", "-i", path,
|
|
"-ar", str(TARGET_RATE),
|
|
"-ac", "1",
|
|
"-sample_fmt", "s16",
|
|
"-f", "s16le",
|
|
"-"
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True)
|
|
if result.returncode != 0:
|
|
print(f"ffmpeg failed for {path}:", result.stderr.decode(), file=sys.stderr)
|
|
sys.exit(1)
|
|
return result.stdout
|
|
|
|
def parse_int16(raw):
|
|
"""Convert raw s16le bytes to a list of int16 values."""
|
|
count = len(raw) // 2
|
|
return list(struct.unpack(f"<{count}h", raw))
|
|
|
|
def find_silence_regions(data):
|
|
"""Find continuous silent regions in int16 PCM data."""
|
|
regions = []
|
|
start = None
|
|
for i, sample in enumerate(data):
|
|
is_silent = abs(sample) < SILENCE_THRESHOLD
|
|
if is_silent and start is None:
|
|
start = i
|
|
elif not is_silent and start is not None:
|
|
length = i - start
|
|
if length >= MIN_SILENCE_SAMPLES:
|
|
regions.append((start, i))
|
|
start = None
|
|
if start is not None and len(data) - start >= MIN_SILENCE_SAMPLES:
|
|
regions.append((start, len(data)))
|
|
return regions
|
|
|
|
def split_into_samples(data, silence_regions):
|
|
"""Split PCM data into individual seek samples using silence delimiters."""
|
|
samples = []
|
|
prev_end = 0
|
|
for s_start, s_end in silence_regions:
|
|
if s_start > prev_end:
|
|
chunk = data[prev_end:s_start]
|
|
if len(chunk) > 1000: # sanity: > 22ms
|
|
samples.append(chunk)
|
|
prev_end = s_end
|
|
if prev_end < len(data):
|
|
chunk = data[prev_end:]
|
|
if len(chunk) > 1000:
|
|
samples.append(chunk)
|
|
return samples
|
|
|
|
def write_int16_array(f, name, data, values_per_line=12):
|
|
"""Write a static const int16_t array."""
|
|
f.write(f"static const int16_t {name}_data[] = {{\n ")
|
|
for j in range(0, len(data), values_per_line):
|
|
vals = data[j:j+values_per_line]
|
|
line = ", ".join(str(v) for v in vals)
|
|
if j + values_per_line < len(data):
|
|
f.write(line + ",\n ")
|
|
else:
|
|
f.write(line + "\n")
|
|
f.write("};\n\n")
|
|
|
|
# ── Main ────────────────────────────────────────────────────────────────
|
|
|
|
def main():
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
|
|
# ── Process seek samples ────────────────────────────────────────────
|
|
if not os.path.exists(SEEK_WAV):
|
|
print(f"Error: {SEEK_WAV} not found.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(f"Converting {SEEK_WAV}...")
|
|
raw_seek = ffmpeg_to_int16(SEEK_WAV)
|
|
seek_data = parse_int16(raw_seek)
|
|
print(f" PCM samples: {len(seek_data)} ({len(seek_data) / TARGET_RATE:.2f}s)")
|
|
|
|
silence = find_silence_regions(seek_data)
|
|
print(f" Found {len(silence)} silence regions")
|
|
|
|
seek_samples = split_into_samples(seek_data, silence)
|
|
print(f" Extracted {len(seek_samples)} seek samples")
|
|
|
|
if len(seek_samples) == 0:
|
|
print("Error: No seek samples extracted.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
for i, s in enumerate(seek_samples[:5]):
|
|
print(f" Sample {i}: {len(s)} samples ({len(s)/TARGET_RATE*1000:.0f}ms)")
|
|
if len(seek_samples) > 5:
|
|
print(f" ... and {len(seek_samples)-5} more")
|
|
|
|
# ── Process fan sample ──────────────────────────────────────────────
|
|
if not os.path.exists(FAN_WAV):
|
|
print(f"Error: {FAN_WAV} not found.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(f"\nConverting {FAN_WAV}...")
|
|
raw_fan = ffmpeg_to_int16(FAN_WAV)
|
|
fan_data = parse_int16(raw_fan)
|
|
print(f" PCM samples: {len(fan_data)} ({len(fan_data) / TARGET_RATE:.2f}s)")
|
|
|
|
# ── Generate header ─────────────────────────────────────────────────
|
|
print(f"\nWriting {OUT_FILE}...")
|
|
|
|
with open(OUT_FILE, "w") as f:
|
|
f.write("// Auto-generated by tools/samples/gen_samples.py\n")
|
|
f.write("// Do not edit manually.\n\n")
|
|
f.write("#ifndef GENERATED_AUDIO_SAMPLES_H\n")
|
|
f.write("#define GENERATED_AUDIO_SAMPLES_H\n\n")
|
|
f.write("#include <stdint.h>\n\n")
|
|
|
|
f.write("// ── Seek sample entry ──────────────────────────────────────────────\n\n")
|
|
f.write("typedef struct {\n")
|
|
f.write(" const int16_t *data;\n")
|
|
f.write(" uint32_t length;\n")
|
|
f.write(" uint8_t distance;\n")
|
|
f.write("} sample_entry_t;\n\n")
|
|
|
|
# Write seek sample arrays
|
|
for i, sample in enumerate(seek_samples):
|
|
write_int16_array(f, f"seek_sample_{i}", sample)
|
|
|
|
# Write seek lookup table
|
|
f.write("static const sample_entry_t seek_samples[] = {\n")
|
|
for i, sample in enumerate(seek_samples):
|
|
start_track = i
|
|
distance = (40 - start_track) * 100 // 40
|
|
if distance < 1: distance = 1
|
|
if distance > 100: distance = 100
|
|
f.write(f" {{ seek_sample_{i}_data, {len(sample)}, {distance} }}")
|
|
if i < len(seek_samples) - 1:
|
|
f.write(",")
|
|
f.write(f" // {i}->40 seek, distance={distance}\n")
|
|
f.write("};\n\n")
|
|
f.write(f"static const int num_seek_samples = {len(seek_samples)};\n\n")
|
|
|
|
# Write fan idle sample
|
|
f.write("// ── Fan idle loop sample ───────────────────────────────────────────\n\n")
|
|
write_int16_array(f, "fan_idle", fan_data)
|
|
f.write(f"static const uint32_t fan_idle_size = {len(fan_data)};\n\n")
|
|
# Note: write_int16_array adds _data suffix, so array is fan_idle_data
|
|
|
|
f.write("#endif // GENERATED_AUDIO_SAMPLES_H\n")
|
|
|
|
print("Done.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|