Spaces:
Running on Zero
Running on Zero
File size: 7,361 Bytes
7bb70e8 55082a2 7bb70e8 55082a2 7bb70e8 55082a2 b295017 7bb70e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | """Lock a strict-tempo rendering onto a performance whose bars breathe."""
from pathlib import Path
import sys
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent))
import scores
import sync
SR = 22050
failures = []
def check(label, condition, detail=""):
print((" PASS " if condition else " FAIL ") + label + ("" if condition else " -> " + str(detail)))
if not condition:
failures.append(label)
def play(score, bar_times, sr=SR, lead=0.0, total=None, beat_times=None):
"""Sine rendering: chord tones plus melody, bar i spanning bar_times[i:i+2].
`beat_times`, one per quarter note, overrides the even spread inside bars."""
end = total or bar_times[-1] + lead + 0.5
out = np.zeros(int(end * sr))
def to_time(q):
if beat_times is not None:
return lead + float(np.interp(float(q), np.arange(len(beat_times)), beat_times))
i = next(k for k, bar in enumerate(score.bars) if bar.start <= q < bar.end or k == len(score.bars) - 1)
bar = score.bars[i]
share = float((q - bar.start) / bar.length)
return lead + bar_times[i] + share * (bar_times[i + 1] - bar_times[i])
def tone(pitch, a, b, level):
lo, hi = int(a * sr), min(len(out), int(b * sr))
if hi <= lo:
return
t = np.arange(hi - lo) / sr
env = np.exp(-2.5 * t) * np.minimum(1, t * 200)
out[lo:hi] += level * env * np.sin(2 * np.pi * 440 * 2 ** ((pitch - 69) / 12) * t)
for onset, pitch, duration in score.vocal + score.ins:
tone(pitch, to_time(onset), to_time(min(onset + duration, score.length - scores.Fraction(1, 64))), 0.3)
for i, bar in enumerate(score.bars):
for symbol in scores.bar_chords(score, i)[:1]:
match = scores.ROOT.match(symbol)
root = scores.PITCH_CLASS[match.group(1)]
for step in scores.QUALITY_TONES.get(match.group(2), (0, 4, 7)):
tone(48 + (root + step) % 12, lead + bar_times[i], lead + bar_times[i + 1], 0.2)
return out
score = scores.read((Path(__file__).resolve().parent / "fixtures" / "score.abc").read_text(encoding="utf-8"))
bars = len(score.bars)
per_bar = score.seconds(score.bars[0].length)
rng = np.random.default_rng(7)
# The singer: bars wander up to 12% either way, starting 0.4 s in.
lengths = per_bar * (1 + rng.uniform(-0.12, 0.12, bars))
target = [0.4] + list(0.4 + np.cumsum(lengths))
duration = target[-1] + 0.3
performance = play(score, target, total=duration)
# YuE2: strict tempo, 1.1 s of lead-in, and it rings on for a second.
strict = sync.uniform_starts(bars, per_bar)
lead = 1.1
generated = play(score, strict, lead=lead, total=strict[-1] + lead + 1.0)
heard = [t + lead for t in strict]
print("bar_starts from beat rows")
rows = [(0.5 + k * 0.5, k % 4 + 1, 4) for k in range(10)]
found = sync.bar_starts(rows, 4, 6.0)
check("bars start on each downbeat and extrapolate", found[:3] == [0.5, 2.5, 4.5], found)
pickup = sync.bar_starts([(0.2, 3, 4), (0.7, 4, 4), (1.2, 1, 4), (1.7, 2, 4)], 2, 4.0)
check("a pickup bar starts on the first beat", pickup[:2] == [0.2, 1.2], pickup)
print("\nlock()")
stereo = np.stack([generated, generated])
warped, report = sync.lock(stereo, SR, score, target, heard, duration)
print(" report:", report)
check("most bars matched", report["matched"] >= bars - 2, report)
check("a warp was produced", warped is not None)
if warped is not None:
check("output is exactly the recording's length", warped.shape[1] == int(round(duration * SR)),
warped.shape)
import librosa
env_w = librosa.onset.onset_strength(y=warped[0], sr=SR, hop_length=256)
env_p = librosa.onset.onset_strength(y=performance, sr=SR, hop_length=256)
errors = []
for t in target[1:-1]:
frame = int(t * SR / 256)
window = slice(max(0, frame - 40), frame + 40)
peak_w = int(np.argmax(env_w[window])) + window.start
peak_p = int(np.argmax(env_p[window])) + window.start
errors.append(abs(peak_w - peak_p) * 256 / SR)
median = float(np.median(errors))
print(" bar-line error: median %.3f s, worst %.3f s" % (median, max(errors)))
check("bar lines land within 30 ms of the performance", median < 0.03, median)
# Without the warp, the same bars are far out.
plain = np.zeros_like(warped[0])
shift = int((target[0] - heard[0]) * SR)
src = generated[max(0, -shift):]
plain[max(0, shift):max(0, shift) + len(src)] = src[:len(plain) - max(0, shift)]
env_n = librosa.onset.onset_strength(y=plain, sr=SR, hop_length=256)
drift = []
for t in target[1:-1]:
frame = int(t * SR / 256)
window = slice(max(0, frame - 40), frame + 40)
drift.append(abs(int(np.argmax(env_n[window])) - int(np.argmax(env_p[window]))) * 256 / SR)
print(" unwarped error for comparison: median %.3f s" % float(np.median(drift)))
print("\nlock() on every beat")
# A singer who pushes and pulls inside the bar, not only across bars.
quarters = int(score.length)
spacing = score.seconds(1) * (1 + rng.uniform(-0.18, 0.18, quarters))
beat_times = np.concatenate([[0.4], 0.4 + np.cumsum(spacing)])
bar_lines = [float(beat_times[int(bar.start)]) for bar in score.bars] + [float(beat_times[-1])]
swing_len = bar_lines[-1] + 0.3
swung = play(score, bar_lines, total=swing_len, beat_times=beat_times)
strict_beats = [lead + score.seconds(q) for q in range(quarters + 1)]
def beat_error(y):
import librosa
env_y = librosa.onset.onset_strength(y=y, sr=SR, hop_length=256)
env_p = librosa.onset.onset_strength(y=swung, sr=SR, hop_length=256)
errors = []
for t in beat_times[1:-1]:
frame = int(t * SR / 256)
window = slice(max(0, frame - 12), frame + 12)
errors.append(abs(int(np.argmax(env_y[window])) - int(np.argmax(env_p[window]))) * 256 / SR)
return float(np.median(errors)), float(np.mean(np.array(errors) < 0.03))
by_bar, _ = sync.lock(stereo, SR, score, bar_lines, heard, swing_len)
by_beat, report = sync.lock(stereo, SR, score, bar_lines, heard, swing_len,
target_beats=beat_times.tolist(), heard_beats=strict_beats)
print(" report:", report)
bar_med, bar_hit = beat_error(by_bar[0])
beat_med, beat_hit = beat_error(by_beat[0])
print(" every-beat error: bars only %.3f s (%.0f%% within 30 ms), with beats %.3f s (%.0f%% within 30 ms)"
% (bar_med, 100 * bar_hit, beat_med, 100 * beat_hit))
check("beat anchors were used", report["beats"] > 0, report)
check("beat anchors put more beats within 30 ms", beat_hit > bar_hit, (bar_hit, beat_hit))
check("beats land within 30 ms of the performance", beat_med < 0.03, beat_med)
print("\ntimemap()")
# Bar lines matched past the end of a short render must not reach the map.
src, dst = sync.timemap([(1.0, 0.5), (3.0, 2.6), (5.0, 4.4), (9.0, 8.0)], 6 * SR, SR, 7.0)
check("the map ends inside the audio", src[-1] <= 6 * SR, src)
check("both sides strictly rise", all(b > a for a, b in zip(src, src[1:])) and all(b > a for a, b in zip(dst, dst[1:])))
check("the lead-in keeps its natural speed", src[0] == int(0.5 * SR) and dst[0] == 0, (src[0], dst[0]))
check("too few bar lines give no map", sync.timemap([(1.0, 1.0)], SR, SR, 1.0) is None)
print("\n" + ("ALL CHECKS PASSED" if not failures else "FAILED: " + ", ".join(failures)))
sys.exit(1 if failures else 0)
|