from pathlib import Path import sys import numpy as np INSTRUMENT = Path(__file__).resolve().parent.parent / "alto-sax-model-04" sys.path.insert(0, str(INSTRUMENT)) from horn import SAMPLE_RATE, render_sequence VENT_RADIUS = 0.00125 TONGUE_STRENGTH = 0.25 def performance_events(notes, slurs, duration): slur_interiors = {round(onset, 6) for gesture in slurs for onset in gesture["onsets"][1:]} events = [] for index, note in enumerate(notes): pitch = note["pitch"] + 9 pressure = 5000.0 if pitch <= 73 else 5500.0 if pitch <= 75 else 6500.0 gap = 0.00075 if pitch <= 73 else 0.0006 if pitch <= 75 else 0.0005 if pitch == 76 else 0.00045 touch = (note["velocity"] / 0.65)**0.2 end = min(duration, note["time"] + note["duration"] + 0.018) if index + 1 < len(notes): following = notes[index + 1] end = following["time"] if round(following["time"], 6) in slur_interiors else min(end, following["time"]) events.append({"time": note["time"], "duration": round(end - note["time"], 9), "writtenMidi": pitch, "pressurePa": pressure * touch, "gapM": gap * touch, "tongued": round(note["time"], 6) not in slur_interiors}) return events def render_performance(notes, duration, slurs): events = performance_events(notes, slurs, duration) audio, details = render_sequence(events, duration, vent_radius=VENT_RADIUS, tongue_strength=TONGUE_STRENGTH) details.update({ "continuousReedAndBore": True, "slurredOnsets": sum(not event["tongued"] for event in events), "fingeringChanges": int(np.count_nonzero(np.diff([event["writtenMidi"] for event in events]))), "playerControls": events, "touchModel": "Scored velocity weakly adjusts breath pressure and resting gap together; model-04 source unchanged.", "playingTongueStrength": TONGUE_STRENGTH, "instrumentPreset": "1.25 mm register vents for fast transitions, versus 0.65 mm in the untouched standalone lab; other instrument coefficients unchanged.", "noteRelease": "Same 18 ms note-end extension and slur connections as the earlier physical-alto arrangement.", }) return audio, details