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")