Spaces:
Sleeping
Sleeping
| 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") | |