import hashlib import json from pathlib import Path import shutil import subprocess import numpy as np import soundfile as sf from scipy.signal import butter, resample_poly, sosfilt from performance import INSTRUMENT, SAMPLE_RATE, render_performance from analysis import _yin_frames HERE = Path(__file__).resolve().parent PROJECT = HERE.parents[1] SCORE = PROJECT / "studies/giant-lead-01/score.json" PHRASING = PROJECT / "studies/giant-lead-01/phrasing.json" RHYTHM = PROJECT / "studies/ride-trio-03/rhythm.wav" REFERENCE = PROJECT / "versions/v19/guitar.wav" def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() def pitch_checks(signal, notes): results = [] for note in notes: start = note["time"] + min(0.055, note["duration"] * 0.4) end = note["time"] + note["duration"] - 0.005 section = resample_poly(signal[round(start * SAMPLE_RATE):round(end * SAMPLE_RATE)], 1, 4) pitches, confidences = _yin_frames(section, SAMPLE_RATE / 4, 384, 64) voiced = confidences > 0.8 measured = float(np.median(pitches[voiced])) if voiced.any() else None expected = 440 * 2 ** ((note["pitch"] - 69) / 12) results.append({ "time": note["time"], "bar": note["bar"], "concertMidi": note["pitch"], "originalConcertMidi": note["originalPitch"], "duration": note["duration"], "estimatedFrequencyHz": measured, "pitchErrorCents": float(1200 * np.log2(measured / expected)) if measured else None, "voicedFraction": float(np.mean(voiced)) if len(voiced) else 0.0, "rms": float(np.sqrt(np.mean(section**2))) if len(section) else 0.0, }) return results def room(signal): stereo = np.column_stack((signal, signal)) for side, seconds, amount in ((0, 0.017, 0.13), (1, 0.023, 0.12), (0, 0.043, 0.055), (1, 0.051, 0.05)): delay = round(seconds * SAMPLE_RATE) stereo[delay:, side] += signal[:-delay] * amount return stereo * 0.84 def write_audio(name, signal): if not np.isfinite(signal).all() or np.max(np.abs(signal)) >= 1: raise ValueError(f"Invalid playback signal: {name}") wav = HERE / f"{name}.wav" sf.write(wav, signal, SAMPLE_RATE, subtype="PCM_24") subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", str(wav), "-c:a", "libmp3lame", "-b:a", "192k", str(HERE / f"{name}.mp3")], check=True) def transient_checks(signal, notes): block = round(0.01 * SAMPLE_RATE) levels = [] for note in notes: start = round((note["time"] + min(0.06, note["duration"] * 0.35)) * SAMPLE_RATE) end = round((note["time"] + note["duration"] - min(0.025, note["duration"] * 0.18)) * SAMPLE_RATE) blocks = [float(np.sqrt(np.mean(signal[offset:offset + block]**2))) for offset in range(start, end - block + 1, block)] if blocks: levels.append(float(np.median(blocks))) steady = max(float(np.median(levels)), 1e-20) highpass = sosfilt(butter(4, 3000, fs=SAMPLE_RATE, btype="highpass", output="sos"), signal) maximum = 0.0 for note in notes: start = max(0, round((note["time"] - 0.03) * SAMPLE_RATE)) end = min(len(signal), round((note["time"] + 0.05) * SAMPLE_RATE)) maximum = max(maximum, max(float(np.sqrt(np.mean(highpass[offset:offset + block]**2))) for offset in range(start, end - block + 1, block))) return {"steadyMedianBlockRms": steady, "peakOverSteadyDb": float(20 * np.log10(np.max(np.abs(signal)) / steady)), "maximumOnset10msHighpassRmsAtMinus20DbfsSteady": float(20 * np.log10(max(maximum, 1e-20) * 0.1 / steady)), "limits": "Waveform proxies, not perceptual judgments. Original band diagnostics used slightly different event-window exclusions."} def main(): original = json.loads(SCORE.read_text()) phrasing = json.loads(PHRASING.read_text())["slurs"] notes = [dict(note, part="alto", originalPitch=note["pitch"], pitch=note["pitch"] - (12 if 16 <= note["bar"] < 48 else 0)) for note in original["events"] if note["part"] == "guitar"] notes.sort(key=lambda note: note["time"]) instrument_files = (INSTRUMENT / "horn.py", INSTRUMENT / "steady-c4.wav", INSTRUMENT / "report.json") instrument_before = {path.name: digest(path) for path in instrument_files} sax, physical = render_performance(notes, original["duration"], phrasing) measurements = pitch_checks(sax, notes) exceptions = [note for note in measurements if note["pitchErrorCents"] is None or abs(note["pitchErrorCents"]) > 45] rhythm, rhythm_rate = sf.read(RHYTHM, dtype="float64") reference, reference_rate = sf.read(REFERENCE, dtype="float64") if rhythm_rate != SAMPLE_RATE or reference_rate != SAMPLE_RATE or rhythm.shape != reference.shape or len(rhythm) != len(sax): raise ValueError("The original backing and new lead must retain identical lengths at 44.1 kHz") active = slice(round(notes[0]["time"] * SAMPLE_RATE), round((notes[-1]["time"] + notes[-1]["duration"]) * SAMPLE_RATE)) gain = 0.8 * float(np.sqrt(np.mean(reference[active]**2))) / max(float(np.sqrt(np.mean(sax[active]**2))), 1e-20) lead = room(sax * gain) mix = rhythm + lead while np.max(np.abs(mix)) >= 0.94: gain *= 0.97 lead = room(sax * gain) mix = rhythm + lead for name, signal in (("mix", mix), ("sax-solo", lead), ("sax-dry", sax * gain)): write_audio(name, signal) candidates = [note["time"] for note in notes if 16 <= note["bar"] < 48] onset = max(candidates, key=lambda start: sum(start <= note["time"] < start + 6 for note in notes)) fast_seconds = [onset - 0.15, min(original["duration"], onset + 6.25)] fast = slice(round(fast_seconds[0] * SAMPLE_RATE), round(fast_seconds[1] * SAMPLE_RATE)) for name, signal in (("fast-pass", mix), ("fast-sax", lead), ("fast-sax-dry", sax * gain)): write_audio(name, signal[fast]) shutil.copyfile(RHYTHM, HERE / "rhythm.wav") preserved, preserved_rate = sf.read(HERE / "rhythm.wav", dtype="float64") backing_identical = preserved_rate == rhythm_rate and np.array_equal(preserved, rhythm) lead_score = {"title": "Giant Steps ยท model-04 alto", "bpm": original["bpm"], "duration": original["duration"], "chords": original["chords"], "sections": original["sections"], "events": notes, "registerAdaptation": "Two original solo choruses lowered one octave, exactly as in giant-alto-01; heads and ending unchanged."} (HERE / "lead-score.json").write_text(json.dumps(lead_score, indent=2) + "\n") checks = {"backingPcmIdentical": backing_identical, "backingFileIdentical": digest(HERE / "rhythm.wav") == digest(RHYTHM), "instrumentSourcesAndToneUnchanged": instrument_before == {path.name: digest(path) for path in instrument_files}, "noAcousticStateResets": physical["keyStateResets"] == 0, "allNotesWithin45Cents": not exceptions, "couplingResidualBelow0Point01Pa": physical["maximumCouplingResidualPa"] < 0.01, "tailEnergyDecays": physical["finalStoredEnergyJ"] / max(physical["peakStoredEnergyJ"], 1e-20) < 0.02, "mixHasHeadroom": float(np.max(np.abs(mix))) < 0.94} report = { "status": "Standalone model04 reused in the preserved arrangement; new band take awaiting listening judgment.", "synthesis": "Model-04 engine imported unchanged, with a wider register-vent preset and lighter tongue contact for this performance.", "sampleFree": True, "paidApiCalls": 0, "bpm": original["bpm"], "durationSeconds": original["duration"], "leadNotes": len(notes), "fastPassageSeconds": fast_seconds, "originalScoreSHA256": digest(SCORE), "originalPhrasingSHA256": digest(PHRASING), "originalRhythmSHA256": digest(RHYTHM), "model04ArtifactsSHA256": instrument_before, "performanceSourceSHA256": digest(HERE / "performance.py"), "rendererSourceSHA256": digest(HERE / "render_score.py"), "unusedContactStudy": {"source": "horn_fast.py", "sha256": digest(HERE / "horn_fast.py"), "usedInAudio": False}, "registerAdaptation": lead_score["registerAdaptation"], "unchanged": "Model-04 source and standalone audio; remaining horn coefficients and sample rate; song tempo, head pitches, note onsets, harmony, form, original bass/drums and earlier versions.", "leadGain": gain, "mixPeak": float(np.max(np.abs(mix))), "additiveBackingError": float(np.max(np.abs(mix - lead - rhythm))), "checks": checks, "pitchChecks": measurements, "pitchCheckExceptions": exceptions, "warnings": [f"{len(exceptions)} note windows fail the unrestricted pitch check; fast register transitions remain experimental."] if exceptions else [], "transients": transient_checks(sax, notes), "physical": physical, "limits": ["Existing arrangement, not a new transcription of the original recording or a tenor model", "Generic horn geometry and prescribed player controls; no measured full moving-key power balance", "The original guitar bend becomes a straight horn arrival", "Solo writing is preserved, not a new improvisation; short-window pitch estimates can be ambiguous", "Pitch and waveform checks do not establish realism or closeness to an original recording"], } (HERE / "report.json").write_text(json.dumps(report, indent=2, allow_nan=False) + "\n") print(json.dumps({"checks": checks, "exceptionCount": len(exceptions), "firstExceptions": exceptions[:10], "leadGain": gain, "transients": report["transients"]}, indent=2), flush=True) failures = [name for name, passed in checks.items() if not passed and name != "allNotesWithin45Cents"] if failures: raise AssertionError(f"Band checks failed: {failures}") if __name__ == "__main__": main()