Files
product-vamp/generator/audio_assets.py
T
khodowany bc7baea7c4
ci / checks (push) Failing after 23m22s
Cleanup
2026-10-08 02:13:45 -05:00

69 lines
3.0 KiB
Python

"""Validate and copy supplied recordings without synthesis or transformation."""
from __future__ import annotations
import fractions
import hashlib
import json
import re
import shutil
import wave
import generator.model
import generator.performance
TEMPOS = tuple(range(60, 121, 10))
def describe_wav(path, root, *, song_id='', bpm=0):
if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(root.resolve()):
raise ValueError(f'{path}: expected a contained regular WAV')
try:
with wave.open(str(path), 'rb') as audio:
channels, rate, frames = audio.getnchannels(), audio.getframerate(), audio.getnframes()
if audio.getcomptype() != 'NONE' or audio.getsampwidth() != 2 or channels not in (1, 2) or not 8000 <= rate <= 192000 or frames <= 0:
raise ValueError('expected nonempty mono/stereo PCM16 WAV')
remaining = frames
while remaining:
count = min(remaining, 65536)
if len(audio.readframes(count)) != count * channels * 2:
raise ValueError('truncated WAV data')
remaining -= count
except (wave.Error, EOFError) as error:
raise ValueError(f'{path}: invalid WAV: {error}') from error
with path.open('rb') as source:
digest = hashlib.file_digest(source, 'sha256').hexdigest()
return dict(path=path.relative_to(root).as_posix(), sha256=digest, channels=channels, sample_rate=rate, frames=frames, song_id=song_id, bpm=bpm)
def copy_audio(static, destination, song_ids):
entries = []
inputs = [(static / 'sounds' / name, '', 0) for name in ('beat1.wav', 'beat2.wav')]
for path in sorted((static / 'backing').glob('*.wav')):
match = re.fullmatch(r'([a-z0-9]+(?:_[a-z0-9]+)*)_(60|70|80|90|100|110|120)\.wav', path.name)
if not match:
raise ValueError(f'{path}: unsupported backing filename or tempo')
if match[1] not in song_ids:
continue
inputs.append((path, match[1], int(match[2])))
for source, song_id, bpm in inputs:
description = describe_wav(source, static, song_id=song_id, bpm=bpm)
target = destination / description['path']
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, target)
entries.append(description)
return entries
def validate_recording_lengths(root, entries):
durations = {}
for path in sorted((root / 'songs').glob('*.json')):
document = json.loads(path.read_text())
performance = generator.performance.timing(document)
song_id = performance['song_id']
duration = generator.model.fraction(performance['duration']) + performance['count_in_beats']
if song_id in durations and durations[song_id] != duration:
raise ValueError(f'{song_id}: parts disagree about performance duration')
durations[song_id] = duration
for entry in entries:
if entry['song_id']:
seconds = fractions.Fraction(entry['frames'], entry['sample_rate'])
if seconds < durations[entry['song_id']] * 60 / entry['bpm']:
raise ValueError(f"{entry['path']}: backing recording is shorter than count-in and score")