hexachords / hexachords.py
pachet's picture
Update app.py and hexachords.py
45d6604
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")