Sepearate Generation
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Optional, downstream audition tools. Never imported by production composers."""
|
||||
@@ -0,0 +1,719 @@
|
||||
{
|
||||
"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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"""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']:
|
||||
for n in phrase['lead_stages'].get(stage,{}).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'];notes_report.append(f" {n['start']}–{n['end']} {tone.label()} {n['role']} string {pos[0]} fret {pos[1]}\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()
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Read optional reference MusicXML literally, without repairing it or composing."""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from fractions import Fraction as F
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def read(path, role, form_start=None):
|
||||
raw=Path(path).read_bytes();tree=ET.fromstring(raw)
|
||||
result={'path':str(Path(path).resolve()),'sha256':hashlib.sha256(raw).hexdigest(),'validation_role':role,
|
||||
'pitch_convention':'encoded MusicXML; no inferred guitar octave','part_names':[n.text for n in tree.findall('./part-list/score-part/part-name')],
|
||||
'measures':[],'notes':[],'harmonies':[],'annotations':[],'discrepancies':[]}
|
||||
divisions=F(1);absolute=F();nominal=F(4);pending={};active=None
|
||||
for index,m in enumerate(tree.findall('./part/measure')):
|
||||
cursor=F();extent=F();last_start=F();last_duration=F()
|
||||
if (value:=m.findtext('attributes/divisions')) is not None:divisions=F(value)
|
||||
meter=m.find('attributes/time')
|
||||
if meter is not None and meter.findtext('beats'):
|
||||
nominal=sum((F(v) for v in meter.findtext('beats').split('+')),F())*4/F(meter.findtext('beat-type'))
|
||||
for item in m:
|
||||
offset=F(item.findtext('offset','0'))/divisions
|
||||
if item.tag=='backup':cursor-=F(item.findtext('duration'))/divisions
|
||||
elif item.tag=='forward':cursor+=F(item.findtext('duration'))/divisions;extent=max(extent,cursor)
|
||||
elif item.tag=='harmony':
|
||||
active={'time':str(absolute+cursor+offset),'measure':index+1,'offset':str(cursor+offset),
|
||||
'root':item.findtext('root/root-step'),'alter':item.findtext('root/root-alter','0'),
|
||||
'kind':item.findtext('kind'),'display':item.find('kind').get('text') if item.find('kind') is not None else None,
|
||||
'bass':ET.tostring(item.find('bass'),encoding='unicode') if item.find('bass') is not None else None,
|
||||
'degrees':[{'value':d.findtext('degree-value'),'alter':d.findtext('degree-alter'),'type':d.findtext('degree-type')} for d in item.findall('degree')]}
|
||||
result['harmonies'].append(active)
|
||||
elif item.tag=='direction':
|
||||
for words in item.findall('direction-type/words'):
|
||||
result['annotations'].append({'time':str(absolute+cursor+offset),'measure':index+1,'text':''.join(words.itertext()),'interpretation':'descriptive; no production permission'})
|
||||
elif item.tag=='note':
|
||||
duration=F(item.findtext('duration','0'))/divisions
|
||||
start=last_start if item.find('chord') is not None else cursor
|
||||
end=start+duration;voice=item.findtext('voice','1');p=item.find('pitch')
|
||||
encoded={'step':p.findtext('step'),'alter':p.findtext('alter','0'),'octave':int(p.findtext('octave'))} if p is not None else None
|
||||
kinds={t.get('type') for t in item.findall('tie')} | {t.get('type') for t in item.findall('notations/tied')}
|
||||
event={'start':str(absolute+start),'end':str(absolute+end),'measure':index+1,'offset':str(start),'pitch':encoded,'rest':item.find('rest') is not None,
|
||||
'written_type':item.findtext('type'),'dots':len(item.findall('dot')),'voice':voice,'ties':sorted(kinds),
|
||||
'time_modification':ET.tostring(item.find('time-modification'),encoding='unicode') if item.find('time-modification') is not None else None,
|
||||
'tuplets':[dict(t.attrib) for t in item.findall('notations/tuplet')],'grace':item.find('grace') is not None,'new_attack':'stop' not in kinds and p is not None}
|
||||
key=(voice,json.dumps(encoded,sort_keys=True))
|
||||
if 'stop' in kinds:
|
||||
old=pending.get(key)
|
||||
if old is None or result['notes'][old]['end']!=event['start']:
|
||||
result['discrepancies'].append({'measure':index+1,'issue':'unmatched/noncontiguous tie; retained literally'})
|
||||
else:event['continuation_of']=old
|
||||
if 'start' not in kinds:pending.pop(key,None)
|
||||
if 'start' in kinds:pending[key]=len(result['notes'])
|
||||
result['notes'].append(event)
|
||||
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'})
|
||||
result['measures'].append({'number':m.get('number'),'start':str(absolute),'duration':str(extent),'implicit':m.get('implicit'),'initial_harmony_known':any(F(h['time'])<=absolute for h in result['harmonies'])})
|
||||
absolute+=extent
|
||||
if pending:result['discrepancies'].append({'issue':'unterminated ties retained'})
|
||||
# Do not run the strict score importer over these references: annotations,
|
||||
# unusual timing and high encoded pitches are evidence, not repair requests.
|
||||
from generator import harmony, model, fretboard
|
||||
def semantic(h):
|
||||
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']]}
|
||||
for event in result['notes']:
|
||||
if not event['pitch']:continue
|
||||
p=event['pitch'];number=12*(p['octave']+1)+model.NATURAL[p['step'].lower()]+F(p['alter'])
|
||||
event['encoded_midi']=str(number)
|
||||
event['legal_positions_at_encoded_pitch']=len(fretboard.single_positions_for_pitch(number))
|
||||
event['harmonic_spans']=[]
|
||||
start,end=F(event['start']),F(event['end'])
|
||||
changes=[F(h['time']) for h in result['harmonies'] if start<F(h['time'])<end]
|
||||
for a,b in zip([start,*changes],[*changes,end]):
|
||||
current=next((h for h in reversed(result['harmonies']) if F(h['time'])<=a),None)
|
||||
info={'start':str(a),'end':str(b),'chord':current,'chord_degrees':[]}
|
||||
if current and current['root']:
|
||||
try:
|
||||
chord=semantic(current);root=model.NATURAL[chord['root']['letter']]+F(current['alter'])
|
||||
info['chord_degrees']=[d for d,alter in harmony.degrees(chord).items() if (root+harmony.degree_semitones(d,alter)-number)%12==0]
|
||||
info['root_relative_semitones']=str((number-root)%12)
|
||||
info['spelled_simple_degree']=(model.LETTERS.index(p['step'].lower())-model.LETTERS.index(chord['root']['letter']))%7+1
|
||||
except (ValueError,KeyError,AttributeError) as error:
|
||||
info['uncertainty']=str(error)
|
||||
else:info['uncertainty']='No written/carried harmony; none invented.'
|
||||
event['harmonic_spans'].append(info)
|
||||
sounded=[];tie_roots={}
|
||||
for i,n in enumerate(result['notes']):
|
||||
if not n['pitch']:continue
|
||||
previous=n.get('continuation_of')
|
||||
if previous is not None and previous in tie_roots:
|
||||
root=tie_roots[previous];sounded[root]['end']=n['end'];sounded[root]['notation_events'].append(i);tie_roots[i]=root
|
||||
else:
|
||||
tie_roots[i]=len(sounded);sounded.append({'start':n['start'],'end':n['end'],'pitch':n['pitch'],'notation_events':[i]})
|
||||
result['sounding_events']=sounded
|
||||
first=form_start or next((h['measure'] for h in result['harmonies']),None)
|
||||
offset=F(result['measures'][first-1]['start']) if first else None
|
||||
result['alignment']={'form_start_measure':first,'encoded_offset':str(offset) if offset is not None else None,'basis':'explicit override' if form_start else 'first written harmony; review required','preceding_material':'retained, without invented harmony'}
|
||||
for label in result['annotations']:
|
||||
t=F(label['time']); current=[h for h in result['harmonies'] if F(h['time'])<=t]
|
||||
future=next((h for h in result['harmonies'] if F(h['time'])>t),None)
|
||||
label['current_harmony']=current[-1] if current else None
|
||||
label['following_harmony']=future
|
||||
label['events']=[n for n in result['notes'] if F(n['start'])<=t<F(n['end']) or t<=F(n['start'])<t+1]
|
||||
label['uncertainty']='Label meaning must be assessed from these events and harmonic time; label preserved verbatim.'
|
||||
expected={'b9':F(1),'#9':F(3),'b13':F(8),'#11':F(6),'6':F(9)}.get(label['text'].strip())
|
||||
if expected is not None:
|
||||
relationships=[s.get('root_relative_semitones') for n in label['events'] for s in n.get('harmonic_spans',[])]
|
||||
label['literal_interval_present']=str(expected) in relationships
|
||||
if not label['literal_interval_present']:label['uncertainty']='No matching interval in the immediate annotated context; label retained, intended scope uncertain.'
|
||||
# Lead in and Bluesy stay contextual descriptions, never enclosure permits.
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--reference',action='append',default=[],metavar='ROLE=PATH')
|
||||
p.add_argument('--form-start',action='append',default=[],metavar='ROLE=MEASURE')
|
||||
p.add_argument('--output',type=Path,required=True)
|
||||
args=p.parse_args(); starts={k:int(v) for k,v in (x.split('=',1) for x in args.form_start)}
|
||||
results=[read(path,role,starts.get(role)) for role,path in (x.split('=',1) for x in args.reference)]
|
||||
args.output.parent.mkdir(parents=True,exist_ok=True)
|
||||
args.output.write_text(json.dumps({'optional_validation_only':True,'references':results},indent=2))
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Reference in New Issue
Block a user