File size: 9,134 Bytes
d020355
0a882c0
 
99a4b1a
6fcf15f
 
99a4b1a
 
45d6604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99a4b1a
 
ead7d25
99a4b1a
f9418ac
99a4b1a
 
f9418ac
99a4b1a
f9418ac
99a4b1a
 
 
 
 
ff7a47f
99a4b1a
 
 
 
 
0cc4ffc
f9418ac
0cc4ffc
 
 
f9418ac
 
 
45d6604
f9418ac
 
 
0cc4ffc
 
 
 
45d6604
0cc4ffc
 
 
 
 
 
 
 
 
45d6604
0cc4ffc
 
 
 
 
 
 
f9418ac
99a4b1a
 
 
 
 
 
 
 
45d6604
99a4b1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45d6604
 
99a4b1a
 
 
45d6604
 
99a4b1a
 
 
 
 
 
 
 
 
45d6604
99a4b1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45d6604
 
 
99a4b1a
 
 
 
6fcf15f
 
 
 
 
 
 
 
0a882c0
 
 
 
 
 
 
 
d020355
 
 
 
 
0a882c0
d020355
0a882c0
 
 
 
99a4b1a
45d6604
99a4b1a
 
ff7a47f
 
 
99a4b1a
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import os
import subprocess
from music21 import note, stream, interval, meter, chord, converter, metadata
from ortools.sat.python import cp_model
from verovio import verovio


class Hexachord:
    known_hexachords = [
        "6-1	[0,1,2,3,4,5]	Chromatic hexachord",
        "6-7	[0,1,2,6,7,8]	Two-semitone tritone scale",
        "6-Z17A	[0,1,2,4,7,8]	All-trichord hexachord",
        "6-20	[0,1,4,5,8,9]	Augmented scale, Ode-to-Napoleon hexachord",
        "6-Z24A	[0,1,3,4,6,8]	Minor major eleventh chord",
        "6-Z24B	[0,2,4,5,7,8]	Half-diminished eleventh chord",
        "6-Z25A	[0,1,3,5,6,8]	Major eleventh chord",
        "6-Z26	[0,1,3,5,7,8]	Major ninth sharp eleventh chord",
        "6-27B	[0,2,3,5,6,9]	Diminished eleventh chord",
        "6-Z28	[0,1,3,5,6,9]	Augmented major eleventh chord",
        "6-Z29	[0,2,3,6,7,9]	Bridge chord",
        "6-30B	[0,2,3,6,8,9]	Petrushka chord, tritone scale",
        "6-32	[0,2,4,5,7,9]	Diatonic hexachord, minor eleventh chord",
        "6-33B	[0,2,4,6,7,9]	Dominant eleventh chord",
        "6-34A	[0,1,3,5,7,9]	Mystic chord",
        "6-34B	[0,2,4,6,8,9]	Augmented eleventh chord, dominant sharp eleventh chord, Prélude chord",
        "6-35	[0,2,4,6,8,T]	Whole tone scale",
        "6-Z44A	[0,1,2,5,6,9]	Schoenberg hexachord",
        "6-Z46A	[0,1,2,4,6,9]	Scale of harmonics",
        "6-Z47B	[0,2,3,4,7,9]	Blues scale"]

    def generate_chord_sequence_from_midi_pitches(self, list_of_mp, intrvl="P5"):
        return self.generate_chord_sequence([note.Note(mp).nameWithOctave for mp in list_of_mp], intrvl=intrvl)

    def generate_chord_sequence(self, six_notes, intrvl="P5"):

        fifth = interval.Interval(intrvl)  # Perfect fifth
        all_pc = [n.pitch.pitchClass for n in six_notes]
        all_chords = []
        for n in six_notes:
            ch = chord.Chord([n])
            current_note = n
            while len(ch) < 6:
                current_note = fifth.transposeNote(current_note)
                if current_note.pitch.pitchClass in all_pc and current_note not in ch:
                    while interval.Interval(noteStart=ch[-1], noteEnd=current_note).semitones > (12 + 7):
                        current_note = current_note.transpose(-12)
                    ch.add(current_note)
            all_chords.append(ch)
        return all_chords

    def generate_3_chords_realizations(self, chord_seq):
        # lower each of the top to notes by 2 ocatves
        res1 = []
        res2 = []
        res3 = []
        for ch in chord_seq:
            new_ch = chord.Chord()
            for i, n in enumerate(ch.notes):
                if i == 4 or i == 5:
                    new_ch.add(n.transpose(-24))
                else:
                    new_ch.add(n)
            res1.append(new_ch)
        for ch in chord_seq:
            new_ch = chord.Chord()
            for i, n in enumerate(ch.notes):
                if i == 4 or i == 5:
                    new_ch.add(n.transpose(-24))
                elif i == 3:
                    new_ch.add(n.transpose(-12))
                else:
                    new_ch.add(n)
            res2.append(new_ch)
        for ch in chord_seq:
            new_ch = chord.Chord()
            for i, n in enumerate(ch.notes):
                if i == 4 or i == 5:
                    new_ch.add(n.transpose(-24))
                elif i == 3 or i == 2:
                    new_ch.add(n.transpose(-12))
                else:
                    new_ch.add(n)
            res3.append(new_ch)
        return res1, res2, res3

    def chords_to_stream(self, chords, file_name):
        s = stream.Stream()
        s.append(meter.TimeSignature("4/4"))
        for c in chords:
            ch = chord.Chord(c)
            ch.duration.quarterLength = 4
            s.append(ch)
        # s.show('midi')
        s.write('midi', file_name)
        return s

    def alternate_chords(self, s1, s2):
        """Create a new stream alternating between chords from s1 and s2"""
        new_stream = stream.Stream()

        # Get chords from both streams
        chords1 = list(s1.getElementsByClass(chord.Chord))
        chords2 = list(s2.getElementsByClass(chord.Chord))

        # Interleave chords from s1 and s2
        for c1, c2 in zip(chords1, chords2):
            new_stream.append(c1)
            new_stream.append(c2)
        return new_stream

    def optimize_voice_leading(self, chord_sequence):
        model = cp_model.CpModel()
        octave_variables = {}
        movement_vars = []

        # Define variables and domains (allowing octave shifts)
        for i, ch in enumerate(chord_sequence):
            for n in ch.notes:
                var_name = f"chord_{i}_note_{n.nameWithOctave}"
                octave_variables[var_name] = model.NewIntVar(n.octave - 1, n.octave + 1,
                                                             var_name)  # Allow octave shifts
        spread_vars = []
        # Add constraints to minimize movement between chords
        for i in range(len(chord_sequence) - 1):
            max_octave = model.NewIntVar(0, 10, "max_pitch" + str(i))
            min_octave = model.NewIntVar(0, 10, "min_pitch" + str(i))
            for n in chord_sequence[i]:
                v = octave_variables[f"chord_{i}_note_{n.nameWithOctave}"]
                # model.Add(max_pitch >= v)  # max_pitch must be at least as high as any note
                # model.Add(min_pitch <= v)  # min_pitch must
            spread_var = max_octave - min_octave
            spread_vars.append(spread_var)
        for i in range(len(chord_sequence) - 1):
            for n1, n2 in zip(chord_sequence[i].notes, chord_sequence[i + 1].notes):
                var1 = octave_variables[f"chord_{i}_note_{n1.nameWithOctave}"]
                var2 = octave_variables[f"chord_{i + 1}_note_{n2.nameWithOctave}"]
                # Define movement variable
                movement_var = model.NewIntVar(0, 36, f"movement_{i}_{n1.name}")
                model.AddAbsEquality(movement_var, var2 - var1)
                # Track movement variable in objective function
                movement_vars.append(movement_var)
        # Define objective: minimize sum of all movement values
        model.Minimize(sum(spread_vars))
        # obj_var = sum(movement_vars)
        # model.Minimize(obj_var)
        # Solve
        solver = cp_model.CpSolver()
        solver.Solve(model)
        # print(solver.Value(obj_var))
        # for v in variables:
        #     print(v)
        #     print(variables[v].Proto().domain)
        #     print(solver.Value(variables[v]))
        # Apply changes to music21 chord sequence
        optimized_chords = []
        for i, ch in enumerate(chord_sequence):
            new_chord = chord.Chord(
                [note.Note(f"{n.name}{solver.Value(octave_variables[f'chord_{i}_note_{n.nameWithOctave}'])}")
                 for n in ch.notes])
            optimized_chords.append(new_chord)

        return optimized_chords

    def midi_to_svg_file(self, midi_file, output_file):
        score = converter.parse(midi_file)
        musicxml_data = score.write('musicxml')  # Get MusicXML as a string
        # Step 2: Load MusicXML into Verovio
        tk = verovio.toolkit()
        tk.loadData(musicxml_data.encode())  # Convert to bytes and load into Verovio
        tk.renderToSVGFile(output_file, 1)

    def midi_to_svg(self, midi_file, svg_output):
        """Convert MIDI to SVG using Verovio's command-line tool."""
        score = converter.parse(midi_file)
        score.metadata = metadata.Metadata()
        score.metadata.title = ''
        musicxml_path = "temp.musicxml"
        score.write('musicxml', fp=musicxml_path)
        # Run Verovio via command line (since Python API fails)
        verovio_executable = "verovio/build/verovio"  # Ensure correct path
        if not os.path.exists(verovio_executable):
            return "Error: Verovio binary not found!"
        # Run Verovio with the full path
        command = f"{verovio_executable} {musicxml_path} -o {svg_output} --smufl-text-font embedded --scale 50 --page-width 1000 --page-height 500 --footer none"
        result = subprocess.run(command, shell=True, capture_output=True, text=True)

        if result.returncode != 0:
            print("Verovio Error:", result.stderr)
            return f"Error running Verovio: {result.stderr}"
        return svg_output


if __name__ == '__main__':
    hexa = Hexachord()
    note_names = ["C3", "Eb3", "E3", "F#3", "G3", "Bb3"]
    notes = [note.Note(n) for n in note_names]
    cs1 = hexa.generate_chord_sequence(notes, intrvl="P4")
    # cs1 = generate_chord_sequence(["E3", "G3", "Ab3", "B3", "C4", "Eb4"])
    # cs2 = generate_chord_sequence(["C3", "F3", "F#3", "A3", "B4", "E4"])
    # alternation = alternate_chords(cs1, cs2)
    # alternation.write('midi',"alternation.mid")

    hexa.chords_to_stream(cs1, 'temp.mid').show('text')
    # optimized = optimize_voice_leading([c1, c2, c3])
    optimized = hexa.optimize_voice_leading(cs1)
    stream1 = stream.Stream(optimized)
    stream1.show('text')
    # stream1.write('midi', "optimized.mid")