File size: 8,266 Bytes
6e4f2b4
 
 
fc99099
a4f531b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc99099
 
a4f531b
 
fc99099
a4f531b
fc99099
a4f531b
 
 
 
 
 
 
 
 
 
 
 
 
fc99099
a4f531b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc99099
 
a4f531b
6e4f2b4
a4f531b
 
 
 
 
 
 
 
6e4f2b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4f531b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e4f2b4
a4f531b
 
 
 
fc99099
a4f531b
 
 
 
 
6e4f2b4
a4f531b
 
 
 
 
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
import os

from mido import MidiFile, Message, MidiTrack
from music21 import note, stream, interval, meter, chord
from ortools.sat.python import cp_model

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"]
    _base_sequence = None
    _realizations = []

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

    def generate_base_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)
        self._base_sequence = all_chords
        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)
        self._realizations =  res1, res2, res3
        return self._realizations

    def chords_to_m21(self, chords):
        s = stream.Stream()
        s.append(meter.TimeSignature("4/4"))
        for c in chords:
            ch = chord.Chord(c)
            ch.duration.quarterLength = 4
            s.append(ch)
        return s

    def chords_to_m21_voices(self, chords):
        s = stream.Stream()
        s.append(meter.TimeSignature("4/4"))
        for i_chord, c in enumerate(chords):
            for chord_note in c:
                n = note.Note(chord_note.pitch)
                n.duration.quarterLength = 4
                s.insert(i_chord * 4, n)
        return s

    def save_chords_to_midi_file(self, chords, file_name):
        m21 = self.chords_to_m21_voices(chords)
        m21.write('midi', file_name)
        return file_name

    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


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_base_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.save_chords_to_midi_file(cs1, 'temp.mid')
    # 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")