hexachords_docker / hexachords.py
pachet's picture
Update app.py, format_conversions.py, and hexachords.py
6e4f2b4
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")