pachet commited on
Commit
a4f531b
·
1 Parent(s): 52d7d5e

Updated hexachords.py

Browse files
Files changed (1) hide show
  1. hexachords.py +219 -0
hexachords.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ from music21 import note, stream, interval, meter, chord, converter, metadata
4
+ from ortools.sat.python import cp_model
5
+ from verovio import verovio
6
+
7
+ from format_conversions import Format_Converter
8
+
9
+
10
+ class Hexachord:
11
+ known_hexachords = [
12
+ "6-1 [0,1,2,3,4,5] Chromatic hexachord",
13
+ "6-7 [0,1,2,6,7,8] Two-semitone tritone scale",
14
+ "6-Z17A [0,1,2,4,7,8] All-trichord hexachord",
15
+ "6-20 [0,1,4,5,8,9] Augmented scale, Ode-to-Napoleon hexachord",
16
+ "6-Z24A [0,1,3,4,6,8] Minor major eleventh chord",
17
+ "6-Z24B [0,2,4,5,7,8] Half-diminished eleventh chord",
18
+ "6-Z25A [0,1,3,5,6,8] Major eleventh chord",
19
+ "6-Z26 [0,1,3,5,7,8] Major ninth sharp eleventh chord",
20
+ "6-27B [0,2,3,5,6,9] Diminished eleventh chord",
21
+ "6-Z28 [0,1,3,5,6,9] Augmented major eleventh chord",
22
+ "6-Z29 [0,2,3,6,7,9] Bridge chord",
23
+ "6-30B [0,2,3,6,8,9] Petrushka chord, tritone scale",
24
+ "6-32 [0,2,4,5,7,9] Diatonic hexachord, minor eleventh chord",
25
+ "6-33B [0,2,4,6,7,9] Dominant eleventh chord",
26
+ "6-34A [0,1,3,5,7,9] Mystic chord",
27
+ "6-34B [0,2,4,6,8,9] Augmented eleventh chord, dominant sharp eleventh chord, Prélude chord",
28
+ "6-35 [0,2,4,6,8,T] Whole tone scale",
29
+ "6-Z44A [0,1,2,5,6,9] Schoenberg hexachord",
30
+ "6-Z46A [0,1,2,4,6,9] Scale of harmonics",
31
+ "6-Z47B [0,2,3,4,7,9] Blues scale"]
32
+
33
+ def generate_chord_sequence_from_midi_pitches(self, list_of_mp, intrvl="P5"):
34
+ return self.generate_chord_sequence([note.Note(mp).nameWithOctave for mp in list_of_mp], intrvl=intrvl)
35
+
36
+ def generate_chord_sequence(self, six_notes, intrvl="P5"):
37
+
38
+ fifth = interval.Interval(intrvl) # Perfect fifth
39
+ all_pc = [n.pitch.pitchClass for n in six_notes]
40
+ all_chords = []
41
+ for n in six_notes:
42
+ ch = chord.Chord([n])
43
+ current_note = n
44
+ while len(ch) < 6:
45
+ current_note = fifth.transposeNote(current_note)
46
+ if current_note.pitch.pitchClass in all_pc and current_note not in ch:
47
+ while interval.Interval(noteStart=ch[-1], noteEnd=current_note).semitones > (12 + 7):
48
+ current_note = current_note.transpose(-12)
49
+ ch.add(current_note)
50
+ all_chords.append(ch)
51
+ return all_chords
52
+
53
+ def generate_3_chords_realizations(self, chord_seq):
54
+ # lower each of the top to notes by 2 ocatves
55
+ res1 = []
56
+ res2 = []
57
+ res3 = []
58
+ for ch in chord_seq:
59
+ new_ch = chord.Chord()
60
+ for i, n in enumerate(ch.notes):
61
+ if i == 4 or i == 5:
62
+ new_ch.add(n.transpose(-24))
63
+ else:
64
+ new_ch.add(n)
65
+ res1.append(new_ch)
66
+ for ch in chord_seq:
67
+ new_ch = chord.Chord()
68
+ for i, n in enumerate(ch.notes):
69
+ if i == 4 or i == 5:
70
+ new_ch.add(n.transpose(-24))
71
+ elif i == 3:
72
+ new_ch.add(n.transpose(-12))
73
+ else:
74
+ new_ch.add(n)
75
+ res2.append(new_ch)
76
+ for ch in chord_seq:
77
+ new_ch = chord.Chord()
78
+ for i, n in enumerate(ch.notes):
79
+ if i == 4 or i == 5:
80
+ new_ch.add(n.transpose(-24))
81
+ elif i == 3 or i == 2:
82
+ new_ch.add(n.transpose(-12))
83
+ else:
84
+ new_ch.add(n)
85
+ res3.append(new_ch)
86
+ return res1, res2, res3
87
+
88
+ def chords_to_stream(self, chords, file_name):
89
+ s = stream.Stream()
90
+ s.append(meter.TimeSignature("4/4"))
91
+ for c in chords:
92
+ ch = chord.Chord(c)
93
+ ch.duration.quarterLength = 4
94
+ s.append(ch)
95
+ # s.show('midi')
96
+ s.write('midi', file_name)
97
+ return s
98
+
99
+ def alternate_chords(self, s1, s2):
100
+ """Create a new stream alternating between chords from s1 and s2"""
101
+ new_stream = stream.Stream()
102
+
103
+ # Get chords from both streams
104
+ chords1 = list(s1.getElementsByClass(chord.Chord))
105
+ chords2 = list(s2.getElementsByClass(chord.Chord))
106
+
107
+ # Interleave chords from s1 and s2
108
+ for c1, c2 in zip(chords1, chords2):
109
+ new_stream.append(c1)
110
+ new_stream.append(c2)
111
+ return new_stream
112
+
113
+ def optimize_voice_leading(self, chord_sequence):
114
+ model = cp_model.CpModel()
115
+ octave_variables = {}
116
+ movement_vars = []
117
+
118
+ # Define variables and domains (allowing octave shifts)
119
+ for i, ch in enumerate(chord_sequence):
120
+ for n in ch.notes:
121
+ var_name = f"chord_{i}_note_{n.nameWithOctave}"
122
+ octave_variables[var_name] = model.NewIntVar(n.octave - 1, n.octave + 1,
123
+ var_name) # Allow octave shifts
124
+ spread_vars = []
125
+ # Add constraints to minimize movement between chords
126
+ for i in range(len(chord_sequence) - 1):
127
+ max_octave = model.NewIntVar(0, 10, "max_pitch" + str(i))
128
+ min_octave = model.NewIntVar(0, 10, "min_pitch" + str(i))
129
+ for n in chord_sequence[i]:
130
+ v = octave_variables[f"chord_{i}_note_{n.nameWithOctave}"]
131
+ # model.Add(max_pitch >= v) # max_pitch must be at least as high as any note
132
+ # model.Add(min_pitch <= v) # min_pitch must
133
+ spread_var = max_octave - min_octave
134
+ spread_vars.append(spread_var)
135
+ for i in range(len(chord_sequence) - 1):
136
+ for n1, n2 in zip(chord_sequence[i].notes, chord_sequence[i + 1].notes):
137
+ var1 = octave_variables[f"chord_{i}_note_{n1.nameWithOctave}"]
138
+ var2 = octave_variables[f"chord_{i + 1}_note_{n2.nameWithOctave}"]
139
+ # Define movement variable
140
+ movement_var = model.NewIntVar(0, 36, f"movement_{i}_{n1.name}")
141
+ model.AddAbsEquality(movement_var, var2 - var1)
142
+ # Track movement variable in objective function
143
+ movement_vars.append(movement_var)
144
+ # Define objective: minimize sum of all movement values
145
+ model.Minimize(sum(spread_vars))
146
+ # obj_var = sum(movement_vars)
147
+ # model.Minimize(obj_var)
148
+ # Solve
149
+ solver = cp_model.CpSolver()
150
+ solver.Solve(model)
151
+ # print(solver.Value(obj_var))
152
+ # for v in variables:
153
+ # print(v)
154
+ # print(variables[v].Proto().domain)
155
+ # print(solver.Value(variables[v]))
156
+ # Apply changes to music21 chord sequence
157
+ optimized_chords = []
158
+ for i, ch in enumerate(chord_sequence):
159
+ new_chord = chord.Chord(
160
+ [note.Note(f"{n.name}{solver.Value(octave_variables[f'chord_{i}_note_{n.nameWithOctave}'])}")
161
+ for n in ch.notes])
162
+ optimized_chords.append(new_chord)
163
+
164
+ return optimized_chords
165
+
166
+ def midi_to_svg_file(self, midi_file, output_file):
167
+ # score = converter.parse(midi_file)
168
+ # musicxml_data = score.write('musicxml') # Get MusicXML as a string
169
+ # # Step 2: Load MusicXML into Verovio
170
+ # tk = verovio.toolkit()
171
+ # tk.loadData(musicxml_data.encode()) # Convert to bytes and load into Verovio
172
+ # tk.renderToSVGFile(output_file, 1)
173
+
174
+ tk = verovio.toolkit()
175
+ fm = Format_Converter()
176
+ musicxml = fm.midi_to_musicxml_string(midi_file)
177
+ mei_str = fm.xml_to_mei_string(tk, musicxml)
178
+ # Load MEI and render SVG
179
+ tk.loadData(mei_str)
180
+ svg = tk.renderToSVGFile(output_file)
181
+ return output_file
182
+
183
+ def midi_to_svg(self, midi_file, svg_output):
184
+ """Convert MIDI to SVG using Verovio's command-line tool."""
185
+ score = converter.parse(midi_file)
186
+ score.metadata = metadata.Metadata()
187
+ score.metadata.title = ''
188
+ musicxml_path = "temp.musicxml"
189
+ score.write('musicxml', fp=musicxml_path)
190
+ # Run Verovio via command line (since Python API fails)
191
+ verovio_executable = "verovio/build/verovio" # Ensure correct path
192
+ if not os.path.exists(verovio_executable):
193
+ return "Error: Verovio binary not found!"
194
+ # Run Verovio with the full path
195
+ command = f"{verovio_executable} {musicxml_path} -o {svg_output} --smufl-text-font embedded --scale 50 --page-width 1000 --page-height 500 --footer none"
196
+ result = subprocess.run(command, shell=True, capture_output=True, text=True)
197
+
198
+ if result.returncode != 0:
199
+ print("Verovio Error:", result.stderr)
200
+ return f"Error running Verovio: {result.stderr}"
201
+ return svg_output
202
+
203
+
204
+ if __name__ == '__main__':
205
+ hexa = Hexachord()
206
+ note_names = ["C3", "Eb3", "E3", "F#3", "G3", "Bb3"]
207
+ notes = [note.Note(n) for n in note_names]
208
+ cs1 = hexa.generate_chord_sequence(notes, intrvl="P4")
209
+ # cs1 = generate_chord_sequence(["E3", "G3", "Ab3", "B3", "C4", "Eb4"])
210
+ # cs2 = generate_chord_sequence(["C3", "F3", "F#3", "A3", "B4", "E4"])
211
+ # alternation = alternate_chords(cs1, cs2)
212
+ # alternation.write('midi',"alternation.mid")
213
+
214
+ hexa.chords_to_stream(cs1, 'temp.mid').show('text')
215
+ # optimized = optimize_voice_leading([c1, c2, c3])
216
+ optimized = hexa.optimize_voice_leading(cs1)
217
+ stream1 = stream.Stream(optimized)
218
+ stream1.show('text')
219
+ # stream1.write('midi', "optimized.mid")