"""An original 20-bar guitar chorus: a rising fourth and its descending replies. Run arrange.py, then render.py OUTPUT_DIRECTORY. The accepted theme, bass, clock and models are retained. The embedded base score makes this standalone. No reference audio is loaded. No random timing or velocity is applied. """ from pathlib import Path import json HERE = Path(__file__).resolve().parent score = json.loads((HERE / 'base-score.json').read_text()) base_events = score['events'] score['events'] = [e for e in base_events if e['part'] != 'guitar' or e['bar'] < 20] starts = score['soloBarStarts'] # Entries: beat, MIDI pitch, sounding hold in beats, deliberate pluck velocity. # Rhythms are written in the existing four-beat clock, including cross-bar holds. # The first C-F gesture is a question; the final C-F recalls it before the # resolution to Ab above the bass's F. Chromatic notes have destination notes. lines = [ # 1-3: a clear downbeat, the upward fourth, then a long descending answer. [(0,60,.95,.71), (1.5,65,2.10,.74)], [(.5,61,.42,.67), (1,60,.42,.65), (1.5,58,.42,.66), (2,55,.42,.66), (2.5,52,.42,.64), (3,55,.20,.65), (3.25,54,.20,.60), (3.5,53,1.20,.69)], [(1,56,1.65,.70)], # 4-6: the minor third arrives before its bass; A natural gently answers. [(1.5,63,.55,.66), (2.5,57,.42,.64), (3,60,.42,.67), (3.5,61,1.40,.72)], [(1.5,65,.70,.71), (2.5,63,.42,.65), (3,61,1.30,.69)], [(.5,60,.42,.66), (1,58,.42,.64), (1.5,57,1.50,.69)], # 7-9: extend the arch; let the bass finish its E-F pickup before answering. [(.5,61,.80,.69), (1.5,65,1.25,.74), (3,63,.42,.66), (3.5,61,.42,.65)], [(0,58,.80,.68), (1,61,.42,.66), (1.5,60,.42,.64), (2,64,.60,.69)], [(0,65,2.25,.74), (3,60,.42,.65), (3.5,61,.42,.67)], # 10-12: one high point, then a connected descent into the dominant third. # The long A crosses the bar line; its release gives the bass its own space. [(0,64,.80,.70), (1.5,67,1.10,.75), (3,68,.42,.73), (3.5,67,.42,.69)], [(0,66,.42,.68), (.5,65,.42,.66), (1,63,.42,.67), (1.5,60,.42,.65), (2,57,2.50,.71)], [], # 13-16: the answer grows from Db-C-Bb, turning upward through B into C. # The ensuing major-seventh colors retain the same line rather than # announcing a separate bebop section. Ab-A prepares the return to Bb. [(.5,61,.42,.66), (1,60,.42,.64), (1.5,58,.42,.66), (2,55,.42,.65), (2.5,56,.42,.64), (3,58,.42,.67), (3.5,59,.42,.63)], [(0,60,2.65,.71), (3.5,56,.42,.65)], [(0,58,.85,.68), (1.5,61,.42,.66), (2,65,.42,.70), (2.5,64,.42,.66), (3,61,.42,.65), (3.5,60,.42,.64)], [(.5,55,.42,.65), (1,58,.42,.66), (1.5,63,.42,.70), (2,62,.42,.65), (2.5,60,.42,.66), (3,56,.20,.60), (3.25,57,.55,.66)], # 17-20: the opening fourth returns; the final minor third is left singing. [(0,58,.90,.70), (1.5,61,.42,.66), (2,65,.90,.71), (3.5,63,.42,.65)], [(0,60,.90,.68), (1.5,65,1.20,.71), (3,63,.42,.65), (3.5,61,.42,.64)], [(0,60,.90,.66), (1.5,58,.42,.64), (2,55,.42,.65), (2.5,52,.42,.62), (3,55,.65,.65)], [(0,56,3.70,.69)], ] def at_beat(position): """Translate absolute chorus beats using the unchanged local phrase clock.""" bar = min(int(position // 4), 19) return starts[bar] + (position - 4 * bar) * (starts[bar+1] - starts[bar]) / 4 for bar, line in enumerate(lines): for beat, pitch, hold, velocity in line: onset = at_beat(4 * bar + beat) duration = at_beat(4 * bar + beat + hold) - onset e = dict(part='guitar', pitch=pitch, bar=20+bar, beat=beat, time=onset, duration=duration, velocity=velocity, string=5 if pitch >= 64 else 4 if pitch >= 59 else 3 if pitch >= 55 else 2, role='original solo: rising fourth and descending replies') if duration > .55: e['vibratoCents'] = 4.5 score['events'].append(e) score['events'].sort(key=lambda e: e['time']) solo = [e for e in score['events'] if e['part'] == 'guitar' and e['bar'] >= 20] assert solo[0]['time'] == score['soloStart'] assert all(a['time']+a['duration'] <= b['time']+1e-9 for a,b in zip(solo,solo[1:])) assert [e for e in score['events'] if e['bar'] < 20] == [e for e in base_events if e['bar'] < 20] assert [e for e in score['events'] if e['part'] == 'bass'] == [e for e in base_events if e['part'] == 'bass'] score['description'] = 'Our Spanish Love Song: original solo, Astra take 1.' score['arrangement'] = ( 'The accepted theme and complete bass part are unchanged. An original 20-bar ' 'guitar chorus develops an upward fourth, descending replies, cross-bar holds ' 'and a single high point. Chromatic approaches connect harmonic destinations. ' 'The guitar leaves the existing bass turnarounds audible. Original phrase ' 'clock, instrument models, gains and delayed 4.5-cent vibrato are retained. No samples.' ) score['revision'] = 'Original solo composed from scratch; approved theme and bass preserved.' score['newMaterialAt'] = score['soloStart'] (HERE / 'score.json').write_text(json.dumps(score, indent=2) + '\n') print(f'{len(solo)} solo notes; 20 bars; first attack at {solo[0]["time"]:.6f}s')