Cleanup
This commit is contained in:
@@ -1,719 +0,0 @@
|
||||
{
|
||||
"description": "Original hand-composed reference, not output from the user engine or a quotation of Autumn Leaves.",
|
||||
"tempo_bpm": 120,
|
||||
"meter": "4/4",
|
||||
"pitch_convention": "sounding",
|
||||
"time_units": "exact rational quarter-note beats",
|
||||
"velocity_policy": "lead 80, backing 48; constant within each track; no accents or dynamics",
|
||||
"scale_regions": [
|
||||
{
|
||||
"start": "0",
|
||||
"end": "16",
|
||||
"scale": "D Aeolian"
|
||||
},
|
||||
{
|
||||
"start": "16",
|
||||
"end": "24",
|
||||
"scale": "D Harmonic Minor"
|
||||
},
|
||||
{
|
||||
"start": "24",
|
||||
"end": "32",
|
||||
"scale": "D Aeolian"
|
||||
}
|
||||
],
|
||||
"harmony": [
|
||||
{
|
||||
"bar": 1,
|
||||
"chord": "Gm7",
|
||||
"required_basic_pitch_classes": [
|
||||
7,
|
||||
10,
|
||||
2,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"chord": "C7",
|
||||
"required_basic_pitch_classes": [
|
||||
0,
|
||||
4,
|
||||
7,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"chord": "Fmaj7",
|
||||
"required_basic_pitch_classes": [
|
||||
5,
|
||||
9,
|
||||
0,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"bar": 4,
|
||||
"chord": "Bbmaj7",
|
||||
"required_basic_pitch_classes": [
|
||||
10,
|
||||
2,
|
||||
5,
|
||||
9
|
||||
]
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"chord": "Em7b5",
|
||||
"required_basic_pitch_classes": [
|
||||
4,
|
||||
7,
|
||||
10,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"chord": "A7",
|
||||
"required_basic_pitch_classes": [
|
||||
9,
|
||||
1,
|
||||
4,
|
||||
7
|
||||
]
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"chord": "Dm7",
|
||||
"required_basic_pitch_classes": [
|
||||
2,
|
||||
5,
|
||||
9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"chord": "Dm7",
|
||||
"required_basic_pitch_classes": [
|
||||
2,
|
||||
5,
|
||||
9,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"variants": {
|
||||
"lead_theme": {
|
||||
"attacks": 31,
|
||||
"events": [
|
||||
{
|
||||
"bar": 1,
|
||||
"onset_beats": "0",
|
||||
"duration_beats": "1",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 1,
|
||||
"onset_beats": "1",
|
||||
"duration_beats": "3/2",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 1,
|
||||
"onset_beats": "5/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 1,
|
||||
"onset_beats": "3",
|
||||
"duration_beats": "1",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "4",
|
||||
"duration_beats": "2",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "6",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "13/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "7",
|
||||
"duration_beats": "1",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"onset_beats": "8",
|
||||
"duration_beats": "1",
|
||||
"pitch": "C4",
|
||||
"midi_pitch": 60
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"onset_beats": "9",
|
||||
"duration_beats": "3/2",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"onset_beats": "21/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"onset_beats": "11",
|
||||
"duration_beats": "1",
|
||||
"pitch": "C4",
|
||||
"midi_pitch": 60
|
||||
},
|
||||
{
|
||||
"bar": 4,
|
||||
"onset_beats": "12",
|
||||
"duration_beats": "2",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 4,
|
||||
"onset_beats": "14",
|
||||
"duration_beats": "1",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 4,
|
||||
"onset_beats": "15",
|
||||
"duration_beats": "1",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"onset_beats": "16",
|
||||
"duration_beats": "3/2",
|
||||
"pitch": "Bb4",
|
||||
"midi_pitch": 70
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"onset_beats": "35/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"onset_beats": "18",
|
||||
"duration_beats": "1",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"onset_beats": "19",
|
||||
"duration_beats": "1",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "20",
|
||||
"duration_beats": "1",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "21",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "43/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "22",
|
||||
"duration_beats": "1",
|
||||
"pitch": "C#4",
|
||||
"midi_pitch": 61
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "23",
|
||||
"duration_beats": "1",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"onset_beats": "24",
|
||||
"duration_beats": "1",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"onset_beats": "25",
|
||||
"duration_beats": "3/2",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"onset_beats": "53/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"onset_beats": "27",
|
||||
"duration_beats": "1",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"onset_beats": "28",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"onset_beats": "57/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "C4",
|
||||
"midi_pitch": 60
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"onset_beats": "29",
|
||||
"duration_beats": "2",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"onset_beats": "31",
|
||||
"duration_beats": "1",
|
||||
"pitch": null,
|
||||
"midi_pitch": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"lead_with_runs": {
|
||||
"attacks": 36,
|
||||
"events": [
|
||||
{
|
||||
"bar": 1,
|
||||
"onset_beats": "0",
|
||||
"duration_beats": "1",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 1,
|
||||
"onset_beats": "1",
|
||||
"duration_beats": "3/2",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 1,
|
||||
"onset_beats": "5/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 1,
|
||||
"onset_beats": "3",
|
||||
"duration_beats": "1",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "4",
|
||||
"duration_beats": "1",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "5",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "11/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "6",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "13/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "Bb4",
|
||||
"midi_pitch": 70
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "7",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 2,
|
||||
"onset_beats": "15/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"onset_beats": "8",
|
||||
"duration_beats": "1",
|
||||
"pitch": "C4",
|
||||
"midi_pitch": 60
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"onset_beats": "9",
|
||||
"duration_beats": "3/2",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"onset_beats": "21/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 3,
|
||||
"onset_beats": "11",
|
||||
"duration_beats": "1",
|
||||
"pitch": "C4",
|
||||
"midi_pitch": 60
|
||||
},
|
||||
{
|
||||
"bar": 4,
|
||||
"onset_beats": "12",
|
||||
"duration_beats": "2",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 4,
|
||||
"onset_beats": "14",
|
||||
"duration_beats": "1",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 4,
|
||||
"onset_beats": "15",
|
||||
"duration_beats": "1",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"onset_beats": "16",
|
||||
"duration_beats": "3/2",
|
||||
"pitch": "Bb4",
|
||||
"midi_pitch": 70
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"onset_beats": "35/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"onset_beats": "18",
|
||||
"duration_beats": "1",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 5,
|
||||
"onset_beats": "19",
|
||||
"duration_beats": "1",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "20",
|
||||
"duration_beats": "1",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "21",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "43/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "22",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "45/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "C#4",
|
||||
"midi_pitch": 61
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "23",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 6,
|
||||
"onset_beats": "47/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "C#4",
|
||||
"midi_pitch": 61
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"onset_beats": "24",
|
||||
"duration_beats": "1",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"onset_beats": "25",
|
||||
"duration_beats": "3/2",
|
||||
"pitch": "A4",
|
||||
"midi_pitch": 69
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"onset_beats": "53/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "G4",
|
||||
"midi_pitch": 67
|
||||
},
|
||||
{
|
||||
"bar": 7,
|
||||
"onset_beats": "27",
|
||||
"duration_beats": "1",
|
||||
"pitch": "F4",
|
||||
"midi_pitch": 65
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"onset_beats": "28",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "E4",
|
||||
"midi_pitch": 64
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"onset_beats": "57/2",
|
||||
"duration_beats": "1/2",
|
||||
"pitch": "C4",
|
||||
"midi_pitch": 60
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"onset_beats": "29",
|
||||
"duration_beats": "2",
|
||||
"pitch": "D4",
|
||||
"midi_pitch": 62
|
||||
},
|
||||
{
|
||||
"bar": 8,
|
||||
"onset_beats": "31",
|
||||
"duration_beats": "1",
|
||||
"pitch": null,
|
||||
"midi_pitch": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "original_lead_theme",
|
||||
"acceptance": "User accepted plain theme and runs as references",
|
||||
"tonic": "D",
|
||||
"mode": "minor",
|
||||
"run_windows": [
|
||||
{
|
||||
"start": "4",
|
||||
"end": "8"
|
||||
},
|
||||
{
|
||||
"start": "20",
|
||||
"end": "24"
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
{
|
||||
"id": "leap_return",
|
||||
"start": "0",
|
||||
"end": "4",
|
||||
"relationship": "timed leap and return"
|
||||
},
|
||||
{
|
||||
"id": "restatement",
|
||||
"start": "8",
|
||||
"end": "12",
|
||||
"relationship": "related rhythm and leap with harmonic adaptation"
|
||||
}
|
||||
],
|
||||
"development": [
|
||||
{
|
||||
"role": "statement",
|
||||
"start": "0",
|
||||
"end": "4",
|
||||
"degrees": [
|
||||
0,
|
||||
3,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "answer",
|
||||
"start": "4",
|
||||
"end": "8",
|
||||
"degrees": [
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "return",
|
||||
"start": "8",
|
||||
"end": "12",
|
||||
"degrees": [
|
||||
0,
|
||||
3,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "extension",
|
||||
"start": "12",
|
||||
"end": "16",
|
||||
"degrees": [
|
||||
0,
|
||||
2,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "departure",
|
||||
"start": "16",
|
||||
"end": "20",
|
||||
"degrees": [
|
||||
0,
|
||||
-1,
|
||||
-2,
|
||||
-4
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "continuation",
|
||||
"start": "20",
|
||||
"end": "24",
|
||||
"degrees": [
|
||||
0,
|
||||
-1,
|
||||
-2,
|
||||
-4,
|
||||
-2
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "recall",
|
||||
"start": "24",
|
||||
"end": "28",
|
||||
"degrees": [
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "ending",
|
||||
"start": "28",
|
||||
"end": "32",
|
||||
"degrees": [
|
||||
0,
|
||||
-2,
|
||||
-1
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
"""Export auditions from completed assets; this command never composes music."""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import array
|
||||
import copy
|
||||
import dataclasses
|
||||
import html
|
||||
import json
|
||||
from pathlib import Path
|
||||
from fractions import Fraction as F
|
||||
from generator import model, musicxml_writer, playback, score
|
||||
from generator.solo import notation, placement, types, similarity
|
||||
from generator.solo.audition import excerpt, mix
|
||||
|
||||
|
||||
def region(document, part):
|
||||
measures=document['staves'][0]['measures']
|
||||
first=next(i for i,m in enumerate(measures) if m.get('section_label')==part.capitalize()+' Solo')
|
||||
finish=next((i for i in range(first+1,len(measures)) if measures[i].get('section_label')),len(measures))
|
||||
return first,finish-first
|
||||
|
||||
|
||||
def staged(document, details, stage, tempo):
|
||||
notes=[]
|
||||
for phrase in details['phrases']:
|
||||
selected=phrase['lead_stages'].get(stage) or phrase['draft']
|
||||
for n in selected.get('notes',[]):
|
||||
p=n['pitch']
|
||||
notes.append(types.Note(F(n['start']),F(n['end']),types.Pitch(p['letter'],F(p['alter']),p['octave']),
|
||||
tuple(n['sources']),n['role'],continuation=n['continuation'],event_id=n['event_id']))
|
||||
notes=types.sounding_notes(tuple(notes))
|
||||
placed=placement.place(notes,tempo)
|
||||
if notes and placed is None:raise ValueError(stage+': no legal audition placement')
|
||||
result=copy.deepcopy(document)
|
||||
notation.replace_region(result,0,notation.measures(result['staves'][0]['measures'],placed[1] if placed else (),stage))
|
||||
model.validate_document(result)
|
||||
return result
|
||||
|
||||
|
||||
def overlap(lead, backing):
|
||||
a=playback.compile_timeline(lead)['events'];b=playback.compile_timeline(backing)['events']
|
||||
total=covered=above=0.
|
||||
for n in a:
|
||||
start=model.fraction(n['onset']);end=start+model.fraction(n['duration']);pitch=model.fraction(n['pitch'])
|
||||
cuts={start,end}
|
||||
for other in b:
|
||||
s=model.fraction(other['onset']);e=s+model.fraction(other['duration'])
|
||||
if s<end and e>start:cuts.update((max(s,start),min(e,end)))
|
||||
points=sorted(cuts)
|
||||
for x,y in zip(points,points[1:]):
|
||||
simultaneous=[o for o in b if model.fraction(o['onset'])<=x<model.fraction(o['onset'])+model.fraction(o['duration'])]
|
||||
total+=float(y-x)
|
||||
if simultaneous:covered+=float(y-x)
|
||||
if any(model.fraction(o['pitch'])>pitch for o in simultaneous):above+=float(y-x)
|
||||
return {'sounding_beats':total,'backing_overlap_fraction':covered/max(.001,total),'backing_above_lead_fraction':above/max(.001,total),
|
||||
'interpretation':'score overlap only; not a perceptual masking measurement; no feedback to composition'}
|
||||
|
||||
|
||||
def export(assets,baseline,output):
|
||||
output.mkdir(parents=True,exist_ok=True)
|
||||
manifest={'composition_inputs':False,'gain':.65,'backing_gain':.55,'playback':'unchanged neutral constant-level synthesis',
|
||||
'listening_status':'not auditioned by the agent; score validation and rendered signal checks only','solos':{}}
|
||||
tone_cache={};clips=[];notes_report=[]
|
||||
for roles_path in sorted((assets/'analysis').glob('*.roles')):
|
||||
song=roles_path.stem;roles=json.loads(roles_path.read_text())
|
||||
for part in ('acoustic','electric'):
|
||||
print('Audition: '+song+'/'+part,flush=True)
|
||||
other='electric' if part=='acoustic' else 'acoustic'
|
||||
assembled=score.load_json(assets/'songs'/f'{song}_{part}.json')
|
||||
backing=score.load_json(assets/'songs'/f'{song}_{other}.json')
|
||||
first,count=region(assembled,part)
|
||||
lead=excerpt(assembled,first,count);backing=excerpt(backing,first,count)
|
||||
detail=roles[part+'_solo']['details']
|
||||
tempo=next((model.fraction(t['bpm'])*model.written_length(t['written']) for m in assembled['staves'][0]['measures'] for t in m.get('tempos',[]) if 'bpm' in t),F(120))
|
||||
versions={'final':lead, 'plain':staged(lead,detail,'refined',tempo), 'strict':staged(lead,detail,'before_enclosure',tempo), 'source':staged(lead,detail,'initial',tempo)}
|
||||
baseline_backing=None
|
||||
old=baseline/'songs'/f'{song}_{part}.json' if baseline else None
|
||||
if old and old.is_file():
|
||||
original=score.load_json(old);a,b=region(original,part)
|
||||
versions['baseline']=excerpt(original,a,b)
|
||||
baseline_backing=excerpt(score.load_json(baseline/'songs'/f'{song}_{other}.json'),a,b)
|
||||
key=song+'_'+part
|
||||
manifest['solos'][key]={'tempo':str(tempo),'source_form_measures':count,'details':detail,
|
||||
'input_trace':roles[part+'_solo']['trace'],'ensemble':overlap(lead,backing)}
|
||||
notes_report.append(key+' — solo-relative rational quarter-note time; sounding pitches\n')
|
||||
for phrase in detail['phrases']:
|
||||
notes_report.append(f"{phrase['id']}: {phrase['start']}–{phrase['end']}; {phrase['ending']}; completed={phrase['ending_completed']}\n")
|
||||
for n in phrase['draft']['notes']:
|
||||
p=n['pitch'];tone=types.Pitch(p['letter'],F(p['alter']),p['octave'])
|
||||
pos=n['position'];tab=f' string {pos[0]} fret {pos[1]}' if pos else ' (draft pitch; final TAB in XML)'
|
||||
notes_report.append(f" {n['start']}–{n['end']} {tone.label()} {n['role']}{tab}\n")
|
||||
back_audio=mix([backing],tempo,output,tone_cache)
|
||||
for name,document in versions.items():
|
||||
label=key+'_'+name
|
||||
score.write_json(output/(label+'.json'),document)
|
||||
musicxml_writer.write_musicxml(output/(label+'.xml'),document)
|
||||
solo_audio=mix([document],tempo,output,tone_cache)
|
||||
combined=array.array('f',(v+.55*(back_audio[i] if i<len(back_audio) else 0) for i,v in enumerate(solo_audio)))
|
||||
variants={'solo':solo_audio,'fixed_backing':combined}
|
||||
if name=='final':variants['final_arrangement']=combined
|
||||
if name=='baseline' and baseline_backing:
|
||||
oldback=mix([baseline_backing],tempo,output,tone_cache)
|
||||
variants['original_backing']=array.array('f',(v+.55*(oldback[i] if i<len(oldback) else 0) for i,v in enumerate(solo_audio)))
|
||||
for context,values in variants.items():
|
||||
peak=max(map(abs,values),default=0)*manifest['gain']
|
||||
if peak>=1:raise ValueError('Audition gain would clip; choose a lower shared gain')
|
||||
path=label+'_'+context+'.wav'
|
||||
playback.write_pcm(output/path,(v*manifest['gain'] for v in values))
|
||||
clips.append({'file':path,'song':song,'part':part,'version':name,'context':context,'peak':peak})
|
||||
if context=='fixed_backing':
|
||||
# Exact slices retain the same backing and gain. Counts come from
|
||||
# the completed source form, not a presumed 32-bar chorus.
|
||||
measures=document['staves'][0]['measures'];durations=[model.fraction(m['duration']) for m in measures]
|
||||
for excerpt_name,start,end in (('opening',F(),sum(durations[:8],F())),('ending',sum(durations[:-4],F()),sum(durations,F()))):
|
||||
factor=F(60*playback.SAMPLE_RATE)/tempo
|
||||
selection=values[round(start*factor):round(end*factor)]
|
||||
clip=label+'_'+excerpt_name+'.wav'
|
||||
playback.write_pcm(output/clip,(v*manifest['gain'] for v in selection))
|
||||
clips.append({'file':clip,'song':song,'part':part,'version':name,'context':excerpt_name,'peak':peak})
|
||||
manifest['pair_comparisons']={}
|
||||
for song in sorted({c['song'] for c in clips}):
|
||||
def events(part):
|
||||
detail=manifest['solos'][song+'_'+part]['details']
|
||||
return tuple((float(F(n['start'])),float(F(n['end'])),types.Pitch(n['pitch']['letter'],F(n['pitch']['alter']),n['pitch']['octave']).number())
|
||||
for p in detail['phrases'] for n in p['draft']['notes'])
|
||||
manifest['pair_comparisons'][song]=similarity.compare_events(events('electric'),events('acoustic'))
|
||||
manifest['clips']=clips
|
||||
(output/'report.json').write_text(json.dumps(manifest,indent=2))
|
||||
(output/'notes.txt').write_text(''.join(notes_report))
|
||||
rows=['<!doctype html><meta charset="utf-8"><title>Independent generation auditions</title><style>body{max-width:1100px;margin:2rem auto;font:16px system-ui}audio{width:420px}td{padding:.5rem}details{margin:1rem}</style>',
|
||||
'<h1>Independent generation auditions</h1><p>Baseline and new music, same neutral rendering and fixed comparison backing. All files share gain 0.65; backing gain 0.55. Final arrangements use the new independent canonical backing. No subjective listening verdict has been made.</p><p><a href="report.json">Full roles, contracts and diagnostics</a> · <a href="notes.txt">Sounding note report</a></p>']
|
||||
for key,detail in manifest['solos'].items():
|
||||
rows.append('<h2>'+html.escape(key)+'</h2><table>')
|
||||
for c in clips:
|
||||
if c['song']+'_'+c['part']!=key:continue
|
||||
label=c['version']+' / '+c['context']
|
||||
rows.append('<tr><td>'+html.escape(label)+'</td><td><audio controls preload="none" src="'+c['file']+'"></audio></td><td><a href="'+c['file']+'">WAV</a></td></tr>')
|
||||
rows.append('</table>')
|
||||
(output/'index.html').write_text('\n'.join(rows))
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--assets',type=Path,required=True);p.add_argument('--baseline',type=Path);p.add_argument('--output',type=Path,required=True)
|
||||
args=p.parse_args();export(args.assets,args.baseline,args.output)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -59,6 +59,11 @@ def read(path, role, form_start=None, pitch_convention='encoded'):
|
||||
if 'start' not in kinds:pending.pop(key,None)
|
||||
if 'start' in kinds:pending[key]=len(result['notes'])
|
||||
result['notes'].append(event)
|
||||
for container in ('notations/technical/fingering','notations/technical/other-technical','notations/other-notation'):
|
||||
for label in item.findall(container):
|
||||
text=''.join(label.itertext()).strip()
|
||||
if text:result['annotations'].append({'time':str(absolute+start),'measure':index+1,'text':text,
|
||||
'container':container,'interpretation':'Original note annotation; container does not establish musical permission or guitar fingering.'})
|
||||
if item.find('chord') is None and item.find('grace') is None:cursor=end
|
||||
last_start,last_duration=start,duration;extent=max(extent,end,cursor)
|
||||
if extent!=nominal and m.get('implicit')!='yes':result['discrepancies'].append({'measure':index+1,'issue':f'encoded span {extent} differs from nominal {nominal}; no padding applied'})
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"""Summarize finished production decisions; no composition or reference reading."""
|
||||
from __future__ import annotations
|
||||
import argparse,collections,json
|
||||
from fractions import Fraction as F
|
||||
from pathlib import Path
|
||||
from generator import vocabulary
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--assets',type=Path,required=True);p.add_argument('--output',type=Path,required=True);args=p.parse_args();args.output.mkdir(parents=True,exist_ok=True)
|
||||
evidence={};text=['# Finished production evidence','', 'Times below are exact quarter-note beats relative to the solo. Counts describe the selected music; they are not targets or listening verdicts.','']
|
||||
for path in sorted((args.assets/'analysis').glob('*.roles')):
|
||||
roles=json.loads(path.read_text())
|
||||
for role,v in roles.items():
|
||||
key=path.stem+'/'+role
|
||||
evidence[key]={'document_hash':vocabulary.fingerprint(v['document']),'trace':v['trace']}
|
||||
if not role.endswith('_solo'):continue
|
||||
d=v['details'];ps=d['phrases'];ns=[n for p in ps for n in p['draft']['notes']]
|
||||
operations=collections.Counter(t['operation'] for p in ps for t in p['draft']['transformations'])
|
||||
span=max(F(p['end']) for p in ps)-min(F(p['start']) for p in ps)
|
||||
sounding=sum((F(n['end'])-F(n['start']) for n in ns),F())
|
||||
summary={'attacks':sum(not n['continuation'] for n in ns),'quarter_beats':str(span),'occupancy':float(sounding/span),'fretboard':d['fretboard'],'operations_selected':dict(operations),'operation_funnel':d['selection_trace'].get('operations',{}),'expressions_selected':dict(collections.Counter(e['kind'] for e in d['expressions'])),'expression_funnel':d['selection_trace'].get('expression_operations',{}),'evaluation_total':d['selection_trace']['selection_total'],'unresolved':d['unresolved_treatments'],'boundaries':[]}
|
||||
evidence[key].update(summary)
|
||||
text += ['## '+key,'',f"{summary['attacks']} attacks over {span} quarter beats; sounding occupancy {summary['occupancy']:.1%}; sounding MIDI range {d['fretboard']['range_midi']}. Harmonic valid: {d['harmonic_valid']}; unresolved obligations: {len(d['unresolved_treatments'])}.",'', '| Span | Behavior | Destination / actual hold | Home identity |','|---|---|---|---|']
|
||||
for phrase in ps:
|
||||
anchors=[{k:a[k] for k in ('role','effect','pitch','actual_start','actual_end','completed')} for a in phrase['anchors']]
|
||||
summary['boundaries'].append({'start':phrase['start'],'end':phrase['end'],'closure':phrase['closure'],'anchors':anchors,'home':phrase['home']})
|
||||
destination='; '.join(a['effect']+' '+a['pitch']+' '+str(a['actual_start'])+'–'+str(a['actual_end']) for a in anchors) or 'no forced arrival'
|
||||
text.append(f"| {phrase['start']}–{phrase['end']} | {phrase['closure']} / {phrase['ending']} | {destination} | `{phrase['home']}` |")
|
||||
text += ['', 'Selected actions: '+', '.join(f'{k}: {v}' for k,v in sorted(operations.items()))+'.','', 'Typed treatments: '+str(summary['expressions_selected'])+'.','', '### Representative evaluated phrase','']
|
||||
phrase=next(p for p in ps if p['draft']['notes']);evaluation=phrase['evaluation'];assert abs(phrase['score']-sum(evaluation['contributions'].values()))<1e-10
|
||||
text += [f"Selected `{phrase['id']}`; score {phrase['score']:.9f}. Identity is a hard feature contract.",'','| Contribution | Raw | Weight | Weighted |','|---|---:|---:|---:|']
|
||||
for k,value in evaluation['raw'].items():text.append(f"| {k} | {value:.9f} | {evaluation['weights'][k]} | {evaluation['contributions'][k]:.9f} |")
|
||||
pool=d['selection_trace'].get('source_candidate_pools',{}).get(phrase['passage'],[])
|
||||
alternatives=sorted((c for c in pool if c['id']!=phrase['draft']['id']),key=lambda c:(c['score'],c['id']))
|
||||
text += ['', 'Other retained alternatives for this source window (possibly different incoming histories):']
|
||||
for c in alternatives[:3]:text.append(f"- `{c['id']}`: {c['score']:.9f}, {c['attacks']} attacks; selected ending {c['ending']}.")
|
||||
text += ['', 'These are local alternatives, not a proof of a global optimum. The chorus frontier compares complete histories using duration-weighted phrase scores. The JSON retains every reported contribution, operation cutoff counts and source input trace.','']
|
||||
evidence[key]['winner_evaluation']=evaluation;evidence[key]['local_alternatives']=alternatives[:3]
|
||||
(args.output/'evidence.json').write_text(json.dumps(evidence,indent=2))
|
||||
(args.output/'phrase_traces.md').write_text('\n'.join(text)+'\n')
|
||||
if __name__=='__main__':main()
|
||||
@@ -95,8 +95,23 @@ def compare(generated,reference):
|
||||
'reference_playability':playability(reference),'reference_original':reference}
|
||||
|
||||
|
||||
def gesture_coverage(raw):
|
||||
"""Group timing-only retained-unit labels with their direct construction.
|
||||
|
||||
The source label can include rhythm edits such as '; diminution'. These
|
||||
describe direct candidates, not additional harmonic proposal families.
|
||||
Raw generation evidence remains untouched in the manifest.
|
||||
"""
|
||||
result={}
|
||||
for label,counts in raw.items():
|
||||
purpose=label if label.startswith(('direct','harmonic_arrival:','sustained_color')) else 'direct'
|
||||
target=result.setdefault(purpose,dict.fromkeys(('proposed','retained_jobs','valid_routes','retained_units'),0))
|
||||
for name,value in counts.items():target[name]+=value
|
||||
return result
|
||||
|
||||
|
||||
def render_events(notes, duration, tempo, root, cache):
|
||||
"""The same neutral note envelopes as production_review; no expression edits."""
|
||||
"""Use the production neutral note envelopes; no expression edits."""
|
||||
from generator import playback
|
||||
factor=F(60*playback.SAMPLE_RATE)/tempo
|
||||
output=array.array('f',[0])* (round(duration*factor)+2400)
|
||||
@@ -187,12 +202,22 @@ def run(generated_dir,reference_paths,starts,pitch_convention,output,assets=None
|
||||
rows=['<!doctype html><meta charset="utf-8"><title>Solo comparison</title>',
|
||||
'<style>body{font:16px system-ui;max-width:1100px;margin:2rem auto}td,th{padding:.5rem;text-align:left}pre{white-space:pre-wrap}</style>',
|
||||
'<h1>Completed solos and reference scores</h1><p>Differences are diagnostic. These files do not supply generation inputs or candidate scores. No listening verdict is claimed.</p>',
|
||||
'<p><a href="comparison.json">Exact timing, notes, harmony, annotations, placement, and phrase reports</a></p>']
|
||||
'<p><a href="comparison.json">Exact timing, notes, harmony, annotations, placement, and phrase reports</a> · <a href="analysis/theory.md">Complete musical relationship review</a></p>']
|
||||
for role,r in result['solos'].items():
|
||||
actual_path=generated_dir/manifest['solos'][role]['file']
|
||||
reference_path=Path(reference_paths[role])
|
||||
rows += ['<h2>'+html.escape(role)+'</h2>',f'<p>{r["exact_matching_attacks"]} exactly matching attacks; {len(r["differences"])} differing onset positions. This is not a musical-quality score.</p>',
|
||||
rows += ['<h2>'+html.escape(role)+'</h2>',f'<p>Exact event match: <strong>{"yes" if r["exact_musical_match"] else "no"}</strong>. {r["exact_matching_attacks"]} exactly matching attacks; {len(r["differences"])} differing onset positions. This is not a musical-quality score.</p>',
|
||||
'<p><a href="'+html.escape(os.path.relpath(actual_path,output))+'">Generated MusicXML</a> · <a href="'+html.escape(os.path.relpath(reference_path,output))+'">Untouched reference MusicXML</a></p>']
|
||||
selected=collections.Counter(c['kind'] for c in r['production_phrases'].get('expressions',[]))
|
||||
rows.append('<p>Selected, validated production treatments: '+html.escape(str(dict(selected)))+'. Ordinary mapped material remains separate.</p>')
|
||||
if r.get('reference_events_after_production_end'):
|
||||
rows.append('<p>Reference material extends beyond the production vocal-protection window: '+str(len(r['reference_events_after_production_end']))+' events. The reference and vocal timing remain unchanged.</p>')
|
||||
funnel=gesture_coverage(r['production_phrases'].get('selection_trace',{}).get('joint_gestures',{}))
|
||||
if funnel:
|
||||
rows.append('<details><summary>Construction coverage (not similarity scoring)</summary><table><tr><th>Purpose</th><th>Proposed structures</th><th>Retained jobs</th><th>Valid routes</th><th>Retained units</th></tr>')
|
||||
for purpose,counts in sorted(funnel.items()):
|
||||
rows.append('<tr><td>'+html.escape(purpose)+'</td>'+''.join('<td>'+str(counts[k])+'</td>' for k in ('proposed','retained_jobs','valid_routes','retained_units'))+'</tr>')
|
||||
rows.append('</table></details>')
|
||||
for clip in r.get('audition',{}).get('clips',[]):
|
||||
rows.append('<p>'+html.escape(clip['version']+' / '+clip['context'])+'<br><audio controls preload="none" src="'+html.escape(clip['file'])+'"></audio></p>')
|
||||
rows.append('<details><summary>Exact note differences</summary><table><tr><th>Beat relative to form</th><th>Generated</th><th>Reference</th></tr>')
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
"""Explain complete optional solos. No composer calls or production-data writes."""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
from fractions import Fraction as F
|
||||
from pathlib import Path
|
||||
from audition import reference_review, solo_compare
|
||||
from generator import harmony, model
|
||||
|
||||
|
||||
def chord(h):
|
||||
if not h or not h['root']:return None
|
||||
root=h['root']+('#'*int(F(h['alter'])) if F(h['alter'])>0 else 'b'*-int(F(h['alter'])))
|
||||
if h['kind']=='other':
|
||||
# The explicit displayed quality is evidence; never substitute a target
|
||||
# chord for a missing source chord. Preserve any parse error in the report.
|
||||
return harmony.parse_symbol(root+(h['display'] or ''))
|
||||
return {'root':{'letter':h['root'].lower(),'alter':model.rational(F(h['alter']))},'kind':h['kind'],
|
||||
'degrees':[{'value':int(d['value']),'alter':model.rational(F(d['alter'])),'type':d['type']} for d in h['degrees']]}
|
||||
|
||||
|
||||
def members(h):
|
||||
c=chord(h)
|
||||
if not c:return {}
|
||||
root=model.NATURAL[c['root']['letter']]+model.alteration(c['root']['alter'])
|
||||
return {int(root+harmony.degree_semitones(d,a))%12:str(d) for d,a in harmony.degrees(c).items()}
|
||||
|
||||
|
||||
def explain(review, source=None):
|
||||
"""Observed relationships and hypotheses, not retrospective permissions."""
|
||||
notes=solo_compare.events(review);offset=F(review['alignment']['encoded_offset'] or 0)
|
||||
hs=[{**h,'time':str(F(h['time'])-offset)} for h in review['harmonies']]
|
||||
output=[]
|
||||
for i,n in enumerate(notes):
|
||||
start,end=F(n['start']),F(n['end']);p=int(F(n['pitch']))
|
||||
active=next((h for h in reversed(hs) if F(h['time'])<=start),None)
|
||||
future=next((h for h in hs if F(h['time'])>start),None)
|
||||
relation=[];uncertainty=[]
|
||||
try:
|
||||
cm=members(active);fm=members(future)
|
||||
except (ValueError,KeyError) as e:cm=fm={};uncertainty.append(str(e))
|
||||
if p%12 in cm:relation.append('chord member '+cm[p%12])
|
||||
else:relation.append('non-basic pitch: needs collection/color/motion analysis')
|
||||
if future and start<F(future['time'])<end and p%12 in fm:
|
||||
relation.append('anticipated future chord member '+fm[p%12])
|
||||
if i and i+1<len(notes):
|
||||
a,b=notes[i-1],notes[i+1];x=p-int(F(a['pitch']));y=int(F(b['pitch']))-p
|
||||
if F(a['end'])==start and end==F(b['start']) and abs(x)<=2 and abs(y)<=2:
|
||||
relation.append('neighbor motion' if x==-y else 'directional passing motion' if x*y>0 else 'turn')
|
||||
for target in notes[i+1:i+4]:
|
||||
t=F(target['start']);q=int(F(target['pitch']))
|
||||
if t-start>F(3,2):break
|
||||
if abs(q-p)==1 and end<=t and t-end<=F(1,2):
|
||||
relation.append('semitone approach to '+target['pitch']+' at '+target['start']);break
|
||||
if end-start>=2:relation.append('sustained event; '+('breath follows' if i+1<len(notes) and F(notes[i+1]['start'])>end else 'continuation follows'))
|
||||
label=[a['text'] for a in review['annotations'] if start<=F(a['time'])-offset<end]
|
||||
output.append({**n,'relationships':relation,'annotation':label,'uncertainty':uncertainty})
|
||||
def signature(ns):
|
||||
origin=F(ns[0]['start']);base=int(F(ns[0]['pitch']))
|
||||
return tuple((str(F(n['start'])-origin),str(F(n['end'])-F(n['start'])),int(F(n['pitch']))-base) for n in ns)
|
||||
repeats=collections.defaultdict(list)
|
||||
for i in range(len(notes)-3):repeats[signature(notes[i:i+4])].append(notes[i]['start'])
|
||||
source_matches=[]
|
||||
if source:
|
||||
original=solo_compare.events(source)
|
||||
fingerprints={signature(original[i:i+4]):original[i]['start'] for i in range(len(original)-3)}
|
||||
source_matches=[{'solo_start':n['start'],'source_start':fingerprints[signature(notes[i:i+4])]} for i,n in enumerate(notes[:-3]) if signature(notes[i:i+4]) in fingerprints]
|
||||
return {'events':output,'relative_four_event_returns':[{'signature':k,'starts':v} for k,v in repeats.items() if len(v)>1],
|
||||
'source_four_event_relationships':source_matches,'source_relationship_limit':'Exact relative four-event evidence only; absence does not disprove thematic influence.',
|
||||
'interpretation':'Descriptive hypotheses; no label waives production validation. No output from this command is loaded by production.'}
|
||||
|
||||
|
||||
def rule_review(review, choices, offset):
|
||||
"""Read-only recognition under production policies; never run composition.
|
||||
|
||||
Recognition deliberately need not cover every reference note. Failed or
|
||||
ambiguous relationships remain failures, rather than changing a policy.
|
||||
"""
|
||||
import dataclasses
|
||||
from generator import vocabulary
|
||||
from generator.solo import types, harmonic, expressions
|
||||
choices=tuple(dataclasses.replace(c,span=dataclasses.replace(c.span,start=c.span.start-offset,end=c.span.end-offset)) for c in choices)
|
||||
h=harmonic.Harmony(choices);ns=[]
|
||||
for i,n in enumerate(solo_compare.events(review)):
|
||||
p=n['spelling'];ns.append(types.Note(F(n['start']),F(n['end']),types.Pitch(p['step'].lower(),F(p['alter']),p['octave']-1),event_id=str(i)))
|
||||
contracts=[];rejections=collections.defaultdict(list)
|
||||
def accept(c):
|
||||
if expressions.validate(tuple(ns),(*contracts,c),h)[1]:return False
|
||||
contracts.append(c);return True
|
||||
for n in ns:
|
||||
for boundary in (s.start for s in h.spans if n.start<s.start<n.end):
|
||||
accept(types.ExpressionContract('recognition:'+n.event_id,'anticipation','early_chord_tone',(n,),n,h.at(boundary).chord,boundary=boundary))
|
||||
for i,n in enumerate(ns):
|
||||
span=h.at(n.start)
|
||||
if not span or not span.scale:continue
|
||||
if any(n in c.events for c in contracts):continue
|
||||
if n.pitch.number()%12 in span.roles.basic:continue
|
||||
for rule in vocabulary.data('harmony')['colors']:
|
||||
target=ns[i+1] if rule.get('resolve') and i+1<len(ns) else n
|
||||
c=types.ExpressionContract('recognition:'+n.event_id,'color',rule['id'],(n,),target,h.at(target.start).chord if h.at(target.start) else '')
|
||||
if accept(c):break
|
||||
written=harmony.parse_symbol(span.chord)
|
||||
proposed=expressions.degree_pitch(written,rule['degree'],rule['alter'],n.pitch.number())
|
||||
if expressions.quality(written) in rule['qualities'] and proposed.number()==n.pitch.number():
|
||||
error=expressions.validate(tuple(ns),(*contracts,c),h)[1]
|
||||
rejections[n.event_id].append({'rule':rule['id'],'expected_spelling':proposed.label(),'rejection':error})
|
||||
for target in ns:
|
||||
anticipated=next((c for c in contracts if c.kind=='anticipation' and c.target==target),None)
|
||||
span=h.at(anticipated.boundary if anticipated else target.start)
|
||||
if not span or not span.scale:continue
|
||||
for rule in vocabulary.data('lead_ins')['patterns']:
|
||||
start=target.start-F(rule['gap'])-sum(map(F,rule['durations']),F())
|
||||
events=tuple(n for n in ns if start<=n.start<target.start)
|
||||
if not events:continue
|
||||
prior=next((n for n in reversed(ns) if n.end<=start),None)
|
||||
c=types.ExpressionContract('recognition:'+target.event_id+':'+rule['id'],rule['kind'],rule['id'],events,target,span.chord,
|
||||
boundary=anticipated.boundary if anticipated else None,preparation=prior if rule['kind']=='connection' else None)
|
||||
if accept(c):break
|
||||
permissions,error=expressions.validate(tuple(ns),tuple(contracts),h)
|
||||
if error:raise ValueError(error)
|
||||
events=[]
|
||||
for n in ns:
|
||||
spans=h.segments(n);mapped=bool(spans) and spans[0][1]==n.start and spans[-1][2]==n.end and all(s.scale and n.pitch.number()%12 in s.scale.pitches for s,a,b in spans)
|
||||
basic=bool(spans) and all(n.pitch.number()%12 in s.roles.basic for s,a,b in spans)
|
||||
permission=permissions.get(n.event_id)
|
||||
status='recognized_treatment' if permission else 'ordinary_valid' if mapped and (basic or n.end-n.start<2) else 'unexplained_or_unsupported'
|
||||
events.append({'start':str(n.start),'end':str(n.end),'pitch':n.pitch.label(),'mapped_throughout':mapped,
|
||||
'chord_member_throughout':basic,'status':status,'rule':permission.rule if permission else None,
|
||||
'candidate_rejections':rejections.get(n.event_id,[]) if status=='unexplained_or_unsupported' else [],
|
||||
'harmonies':[s.chord for s,a,b in spans],'collections':[s.scale.label() if s.scale else None for s,a,b in spans]})
|
||||
return {'events':events,'rules':dict(collections.Counter(c.rule for c in contracts)),
|
||||
'statuses':dict(collections.Counter(n['status'] for n in events)),
|
||||
'limit':'Conservative recognition, not optimal parsing. Unrecognized events may be valid under a relationship not recognized here. Production harmony comes from the original source, never the target.'}
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--reference',action='append',required=True,metavar='ROLE=XML')
|
||||
p.add_argument('--source',action='append',required=True,metavar='SONG=XML')
|
||||
p.add_argument('--generated-dir',type=Path,required=True)
|
||||
p.add_argument('--output',type=Path,required=True)
|
||||
args=p.parse_args();args.output.mkdir(parents=True,exist_ok=True)
|
||||
sources={k:reference_review.read(v,'original') for k,v in (s.split('=',1) for s in args.source)}
|
||||
from generator import musicxml
|
||||
from generator.scales import timeline, selection, library
|
||||
scales=library.load_library(Path('data/scales'));source_maps={}
|
||||
for name,path in (s.split('=',1) for s in args.source):
|
||||
document=musicxml.load_musicxml(path);staff=document['staves'][0]
|
||||
end=sum((model.fraction(m['duration']) for m in staff['measures']),F())
|
||||
source_maps[name]=selection.select(timeline.read(staff),timeline.Solo(F(),end,'analysis only'),scales)
|
||||
manifest=json.loads((args.generated_dir/'manifest.solo').read_text())
|
||||
results={};lines=['# Musical decisions in the four supplied solos','','Sounding pitch is encoded guitar pitch minus one octave. Times are exact quarter beats from the first written harmony; preceding material remains separate. All measures and all sounding events are retained.','',
|
||||
'The relationships below are observations/hypotheses, not harmonic permission or a claim of listening. Source comparisons inspect relative pitch AND rhythm; annotations remain verbatim.','']
|
||||
for role,path in (s.split('=',1) for s in args.reference):
|
||||
ref=reference_review.read(path,role,pitch_convention='guitar-written')
|
||||
item=manifest['solos'][role];actual=reference_review.read(args.generated_dir/item['file'],role,item['form_start_measure'])
|
||||
analysis=explain(ref,sources[role.split('/')[0]]);comparison=solo_compare.compare(actual,ref)
|
||||
source=sources[role.split('/')[0]]
|
||||
recognized=rule_review(ref,source_maps[role.split('/')[0]],F(source['alignment']['encoded_offset'] or 0))
|
||||
results[role]={'reference':ref,'analysis':analysis,'rule_review':recognized,'generated_analysis':explain(actual),
|
||||
'exact_musical_match':comparison['exact_musical_match'],'exact_matching_attacks':comparison['exact_matching_attacks'],
|
||||
'different_onset_positions':len(comparison['differences']),'reference_playability':comparison['reference_playability']}
|
||||
lines.extend(['## '+role,'',f"Exact output match: **{comparison['exact_musical_match']}**; equal pitch/onset/release attacks: {comparison['exact_matching_attacks']}. This is not a perceptual similarity score.",'',
|
||||
'| Original measure | Aligned onset | Sounding pitch / duration | Observed relationship | Original annotation |','|---|---|---|---|---|'])
|
||||
for n in analysis['events']:
|
||||
raw=n['spelling'];pitch=raw['step']+('#'*int(F(raw['alter'])) if F(raw['alter'])>0 else 'b'*-int(F(raw['alter'])))+str(raw['octave']-1)
|
||||
lines.append(f"| {n['measure']} | {n['start']} | {pitch} / {F(n['end'])-F(n['start'])} | {'; '.join(n['relationships'])} | {'; '.join(n['annotation']).replace(chr(10),' / ')} |")
|
||||
lines.extend(['',f"Relative four-event returns: {analysis['relative_four_event_returns']}",'',f"Exact relative source relationships: {analysis['source_four_event_relationships']}",'',
|
||||
'General rule recognition against ORIGINAL source harmony: '+str(recognized['statuses']),
|
||||
'Recognized policies: '+str(recognized['rules']),'',recognized['limit'],'',
|
||||
'| Onset | Pitch | Collection-valid throughout | Recognition | Policy / source harmony |','|---|---|---|---|---|'])
|
||||
for n in recognized['events']:
|
||||
failures='; '.join(r['rule']+': '+r['rejection'].split(': ',1)[-1]+' (expected '+r['expected_spelling']+')' for r in n['candidate_rejections'])
|
||||
lines.append(f"| {n['start']} | {n['pitch']} | {n['mapped_throughout']} | {n['status']} | {n['rule'] or ''} / {', '.join(n['harmonies'])} {failures} |")
|
||||
(args.output/'theory.json').write_text(json.dumps(results,indent=2))
|
||||
(args.output/'theory.md').write_text('\n'.join(lines)+'\n')
|
||||
print('Reviewed every sounding event in',len(results),'complete reference solos; no composer invoked.')
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Reference in New Issue
Block a user