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