Spaces:
Sleeping
Sleeping
added smallmuse and movements
Browse files- format_conversions.py +48 -14
- legacy/essai_voices.py +52 -0
- working_midi_to_svg_app.py → legacy/working_midi_to_svg_app.py +0 -0
- movements.py +44 -0
- mscore/Dockerfile_mscore +23 -0
- mscore/__init__.py +0 -0
- mscore/app_mscore.py +41 -0
- smallmuse/__init__.py +0 -0
- smallmuse/temporals.py +132 -0
- verovio_app/Dockerfile_Verovio +39 -0
- verovio_app/__init__.py +0 -0
- verovio_app/app_verovio.py +234 -0
- verovio_app/hexachords.py +195 -0
format_conversions.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
import copy
|
| 2 |
import os
|
|
|
|
|
|
|
| 3 |
from music21 import converter, musicxml, stream, note, chord, clef
|
| 4 |
import verovio
|
| 5 |
import shutil
|
|
@@ -61,6 +63,24 @@ class Format_Converter:
|
|
| 61 |
exporter = musicxml.m21ToXml.GeneralObjectExporter(score)
|
| 62 |
return exporter.parse().decode('utf-8') # parse() returns bytes
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
@classmethod
|
| 65 |
def xml_to_mei_string(cls, tk, xmlstring):
|
| 66 |
tk.loadData(xmlstring)
|
|
@@ -79,20 +99,7 @@ class Format_Converter:
|
|
| 79 |
except Exception as e:
|
| 80 |
return f"<p style='color:red'>Error: {e}</p>"
|
| 81 |
|
| 82 |
-
|
| 83 |
-
def midi_to_svg_file(cls, tk, midi_path, output_file):
|
| 84 |
-
try:
|
| 85 |
-
# Convert MIDI to MEI using music21
|
| 86 |
-
musicxml = cls.midi_to_musicxml_string_two_staves(midi_path)
|
| 87 |
-
mei_str = cls.xml_to_mei_string(tk, musicxml)
|
| 88 |
-
# Load MEI and render SVG
|
| 89 |
-
tk.loadData(mei_str)
|
| 90 |
-
svg = tk.renderToSVGFile(output_file)
|
| 91 |
-
return output_file
|
| 92 |
-
except Exception as e:
|
| 93 |
-
print("error in midi_to_svg_file")
|
| 94 |
-
# return f"<p style='color:red'>Error: {e}</p>"
|
| 95 |
-
return output_file
|
| 96 |
|
| 97 |
@classmethod
|
| 98 |
def m21_to_xml(cls, m21):
|
|
@@ -138,6 +145,33 @@ class Format_Converter:
|
|
| 138 |
except Exception as e:
|
| 139 |
return f"Error converting MIDI to audio: {str(e)}"
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
if __name__ == '__main__':
|
| 143 |
svg_string = Format_Converter().midi_to_svg(verovio.toolkit(), 'output.mid')
|
|
|
|
| 1 |
import copy
|
| 2 |
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
from music21 import converter, musicxml, stream, note, chord, clef
|
| 6 |
import verovio
|
| 7 |
import shutil
|
|
|
|
| 63 |
exporter = musicxml.m21ToXml.GeneralObjectExporter(score)
|
| 64 |
return exporter.parse().decode('utf-8') # parse() returns bytes
|
| 65 |
|
| 66 |
+
@classmethod
|
| 67 |
+
def xml_to_svg_file(cls, tk, musicxml, output_file):
|
| 68 |
+
try:
|
| 69 |
+
mei_str = cls.xml_to_mei_string(tk, musicxml)
|
| 70 |
+
# Load MEI and render SVG
|
| 71 |
+
tk.loadData(mei_str)
|
| 72 |
+
svg = tk.renderToSVGFile(output_file)
|
| 73 |
+
return output_file
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print("error in midi_to_svg_file")
|
| 76 |
+
# return f"<p style='color:red'>Error: {e}</p>"
|
| 77 |
+
return output_file
|
| 78 |
+
|
| 79 |
+
@classmethod
|
| 80 |
+
def midi_to_svg_file(cls, tk, midi_path, output_file):
|
| 81 |
+
musicxml = cls.midi_to_musicxml_string_two_staves(midi_path)
|
| 82 |
+
return cls.xml_to_svg_file(tk, musicxml, output_file)
|
| 83 |
+
|
| 84 |
@classmethod
|
| 85 |
def xml_to_mei_string(cls, tk, xmlstring):
|
| 86 |
tk.loadData(xmlstring)
|
|
|
|
| 99 |
except Exception as e:
|
| 100 |
return f"<p style='color:red'>Error: {e}</p>"
|
| 101 |
|
| 102 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
@classmethod
|
| 105 |
def m21_to_xml(cls, m21):
|
|
|
|
| 145 |
except Exception as e:
|
| 146 |
return f"Error converting MIDI to audio: {str(e)}"
|
| 147 |
|
| 148 |
+
def musescore_midi_to_svg(midi_path, svg_path, mscore_path="/Applications/MuseScore 4.app/Contents/MacOS/mscore"):
|
| 149 |
+
subprocess.run([mscore_path, midi_path, "-o", svg_path, "-T"], check=True)
|
| 150 |
+
|
| 151 |
+
def musescore_midi_to_musicxml(
|
| 152 |
+
midi_path: str,
|
| 153 |
+
output_path: str = None,
|
| 154 |
+
musescore_path: str = "/Applications/MuseScore 4.app/Contents/MacOS/mscore"
|
| 155 |
+
):
|
| 156 |
+
midi_file = Path(midi_path)
|
| 157 |
+
if not midi_file.exists():
|
| 158 |
+
raise FileNotFoundError(f"MIDI file not found: {midi_path}")
|
| 159 |
+
|
| 160 |
+
if output_path is None:
|
| 161 |
+
output_path = midi_file.with_suffix(".musicxml")
|
| 162 |
+
else:
|
| 163 |
+
output_path = Path(output_path)
|
| 164 |
+
|
| 165 |
+
command = [musescore_path, str(midi_file), "-o", str(output_path)]
|
| 166 |
+
try:
|
| 167 |
+
result = subprocess.run(command, capture_output=True, text=True, check=True)
|
| 168 |
+
print("✅ MuseScore conversion successful.")
|
| 169 |
+
print(f"➡️ Output: {output_path}")
|
| 170 |
+
except subprocess.CalledProcessError as e:
|
| 171 |
+
print("❌ MuseScore conversion failed.")
|
| 172 |
+
print("stderr:", e.stderr)
|
| 173 |
+
print("stdout:", e.stdout)
|
| 174 |
+
raise
|
| 175 |
|
| 176 |
if __name__ == '__main__':
|
| 177 |
svg_string = Format_Converter().midi_to_svg(verovio.toolkit(), 'output.mid')
|
legacy/essai_voices.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import subprocess
|
| 4 |
+
from mido import MidiFile, MidiTrack, Message, MetaMessage
|
| 5 |
+
|
| 6 |
+
def convert_midi_to_svg(midi_path, svg_path, mscore_path="/Applications/MuseScore 4.app/Contents/MacOS/mscore"):
|
| 7 |
+
subprocess.run([mscore_path, midi_path, "-o", svg_path, "-T", "-S", "../musescore_style.mss"], check=True)
|
| 8 |
+
|
| 9 |
+
def convert_midi_to_musicxml(
|
| 10 |
+
midi_path: str,
|
| 11 |
+
output_path: str = None,
|
| 12 |
+
musescore_path: str = "/Applications/MuseScore 4.app/Contents/MacOS/mscore"
|
| 13 |
+
):
|
| 14 |
+
midi_file = Path(midi_path)
|
| 15 |
+
if not midi_file.exists():
|
| 16 |
+
raise FileNotFoundError(f"MIDI file not found: {midi_path}")
|
| 17 |
+
|
| 18 |
+
if output_path is None:
|
| 19 |
+
output_path = midi_file.with_suffix(".musicxml")
|
| 20 |
+
else:
|
| 21 |
+
output_path = Path(output_path)
|
| 22 |
+
|
| 23 |
+
command = [musescore_path, str(midi_file), "-o", str(output_path)]
|
| 24 |
+
try:
|
| 25 |
+
result = subprocess.run(command, capture_output=True, text=True, check=True)
|
| 26 |
+
print("✅ MuseScore conversion successful.")
|
| 27 |
+
print(f"➡️ Output: {output_path}")
|
| 28 |
+
except subprocess.CalledProcessError as e:
|
| 29 |
+
print("❌ MuseScore conversion failed.")
|
| 30 |
+
print("stderr:", e.stderr)
|
| 31 |
+
print("stdout:", e.stdout)
|
| 32 |
+
raise
|
| 33 |
+
|
| 34 |
+
def remove_tempo_from_svg(svg_path, output_path=None):
|
| 35 |
+
with open(svg_path, "r", encoding="utf-8") as f:
|
| 36 |
+
lines = f.readlines()
|
| 37 |
+
cleaned_lines = [
|
| 38 |
+
line for line in lines
|
| 39 |
+
if "tempo" not in line.lower() and "♩" not in line
|
| 40 |
+
]
|
| 41 |
+
output_path = output_path or svg_path.replace(".svg", "_notempo.svg")
|
| 42 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 43 |
+
f.writelines(cleaned_lines)
|
| 44 |
+
print(f"✅ Tempo removed. Saved to: {output_path}")
|
| 45 |
+
return output_path
|
| 46 |
+
|
| 47 |
+
# Example usage:
|
| 48 |
+
# convert_midi_to_musicxml("movement0.mid",output_path= "movement0.musicxml")
|
| 49 |
+
# with open("movement0.musicxml", "r", encoding="utf-8") as f:
|
| 50 |
+
# xml_string = f.read()
|
| 51 |
+
convert_midi_to_musicxml("../movement5.mid",output_path= "../MSmovement5.svg")
|
| 52 |
+
remove_tempo_from_svg("../MSmovement5-1.svg", "../NTMSmovement5-1.svg")
|
working_midi_to_svg_app.py → legacy/working_midi_to_svg_app.py
RENAMED
|
File without changes
|
movements.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import random
|
| 3 |
+
|
| 4 |
+
from mido import MidiFile, Message, MidiTrack
|
| 5 |
+
from music21 import note, stream, interval, meter, chord
|
| 6 |
+
from ortools.sat.python import cp_model
|
| 7 |
+
|
| 8 |
+
from hexachords import Hexachord
|
| 9 |
+
from smallmuse.temporals import NoteList, TemporalNote
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Movement:
|
| 13 |
+
def __init__(self, h):
|
| 14 |
+
self.hexachord = h
|
| 15 |
+
|
| 16 |
+
def generate_movements(self, param):
|
| 17 |
+
reals = self.hexachord.generate_3_chords_realizations(self.hexachord._base_sequence)
|
| 18 |
+
mvts_templates = []
|
| 19 |
+
for index_of_chord in range(6):
|
| 20 |
+
chords = [real[index_of_chord] for real in reals]
|
| 21 |
+
mvts_templates.append(chords)
|
| 22 |
+
return mvts_templates
|
| 23 |
+
|
| 24 |
+
if __name__ == '__main__':
|
| 25 |
+
hexa = Hexachord()
|
| 26 |
+
note_names = ["C3", "Eb3", "E3", "F#3", "G3", "Bb3"]
|
| 27 |
+
notes = [note.Note(n) for n in note_names]
|
| 28 |
+
cs1 = hexa.generate_base_sequence(notes, intrvl="P4")
|
| 29 |
+
print (cs1)
|
| 30 |
+
move = Movement(hexa)
|
| 31 |
+
mvmts= move.generate_movements(1)
|
| 32 |
+
movement = random.choice(mvmts)
|
| 33 |
+
# make a temporallist
|
| 34 |
+
nl = NoteList()
|
| 35 |
+
for i_chord, chord in enumerate(movement):
|
| 36 |
+
for note in chord:
|
| 37 |
+
nl.add_note(TemporalNote(note.pitch.midi, i_chord * 4, 4))
|
| 38 |
+
nl.join_notes()
|
| 39 |
+
# nl.show()
|
| 40 |
+
nl.apply_rules()
|
| 41 |
+
print('after rules')
|
| 42 |
+
nl.save_midi('output.mid')
|
| 43 |
+
nl.show()
|
| 44 |
+
|
mscore/Dockerfile_mscore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM --platform=linux/amd64 python:3.10-slim
|
| 2 |
+
|
| 3 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 4 |
+
FROM --platform=linux/amd64 python:3.10-slim
|
| 5 |
+
|
| 6 |
+
# Install dependencies needed for MuseScore AppImage
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
wget \
|
| 9 |
+
libqt5gui5 \
|
| 10 |
+
libqt5svg5 \
|
| 11 |
+
libpulse0 \
|
| 12 |
+
fontconfig \
|
| 13 |
+
fuse \
|
| 14 |
+
&& apt-get clean
|
| 15 |
+
|
| 16 |
+
# Download and extract MuseScore 3.6.2 AppImage
|
| 17 |
+
WORKDIR /tmp
|
| 18 |
+
RUN wget https://github.com/musescore/MuseScore/releases/download/v3.6.2/MuseScore-3.6.2.548021370-x86_64.AppImage -O mscore.AppImage \
|
| 19 |
+
&& chmod +x mscore.AppImage \
|
| 20 |
+
&& ./mscore.AppImage --appimage-extract \
|
| 21 |
+
&& mv squashfs-root /opt/mscore \
|
| 22 |
+
&& ln -s /opt/mscore/AppRun /usr/bin/mscore \
|
| 23 |
+
&& rm mscore.AppImage
|
mscore/__init__.py
ADDED
|
File without changes
|
mscore/app_mscore.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import gradio as gr
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
MSCORE_BIN = "/opt/squashfs-root/bin/mscore"
|
| 6 |
+
APPIMAGE = "/opt/mscore.AppImage"
|
| 7 |
+
EXTRACT_DIR = "/opt/squashfs-root"
|
| 8 |
+
|
| 9 |
+
def extract_musescore():
|
| 10 |
+
if not Path(MSCORE_BIN).exists():
|
| 11 |
+
print("🔧 Extracting MuseScore AppImage...")
|
| 12 |
+
subprocess.run([APPIMAGE, "--appimage-extract"], cwd="/opt", check=True)
|
| 13 |
+
|
| 14 |
+
def convert_and_render(midi_path_str):
|
| 15 |
+
midi_path = Path(midi_path_str)
|
| 16 |
+
svg_path = midi_path.with_suffix(".svg")
|
| 17 |
+
result = subprocess.run([
|
| 18 |
+
MSCORE_BIN,
|
| 19 |
+
str(midi_path),
|
| 20 |
+
"-o", str(svg_path)
|
| 21 |
+
], capture_output=True, text=True)
|
| 22 |
+
if result.returncode != 0:
|
| 23 |
+
return f"❌ MuseScore failed: {result.stderr}", None
|
| 24 |
+
return "✅ Conversion successful", svg_path.read_text()
|
| 25 |
+
|
| 26 |
+
iface = gr.Interface(
|
| 27 |
+
fn=convert_and_render,
|
| 28 |
+
inputs=gr.File(type="filepath", label="Upload MIDI file"),
|
| 29 |
+
outputs=["text", gr.HTML(label="Rendered SVG")],
|
| 30 |
+
title="MIDI to SVG Viewer via MuseScore"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
def launch_app():
|
| 34 |
+
if "HUGGINGFACE_SPACE" in os.environ:
|
| 35 |
+
iface.launch(server_name="0.0.0.0", server_port=7860, share=True)
|
| 36 |
+
else:
|
| 37 |
+
iface.launch(server_name="0.0.0.0", server_port=7860)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
launch_app()
|
| 41 |
+
|
smallmuse/__init__.py
ADDED
|
File without changes
|
smallmuse/temporals.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
|
| 3 |
+
from mido import MidiFile, MidiTrack, Message
|
| 4 |
+
|
| 5 |
+
class TemporalNote:
|
| 6 |
+
start_time = 0
|
| 7 |
+
end_time = 0
|
| 8 |
+
pitch = 60
|
| 9 |
+
|
| 10 |
+
def __init__(self, pitch, start_time, duration):
|
| 11 |
+
self.pitch = pitch
|
| 12 |
+
self.start_time = start_time
|
| 13 |
+
self.end_time = start_time + duration
|
| 14 |
+
|
| 15 |
+
def duration(self):
|
| 16 |
+
return self.end_time - self.start_time
|
| 17 |
+
|
| 18 |
+
def set_duration(self, dur):
|
| 19 |
+
self.end_time = self.start_time + dur
|
| 20 |
+
|
| 21 |
+
def __str__(self):
|
| 22 |
+
return f"{self.pitch} @ [{self.start_time}, {self.end_time}]"
|
| 23 |
+
|
| 24 |
+
def __repr__(self):
|
| 25 |
+
return f"{self.pitch} @ [{self.start_time}, {self.end_time}]"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class NoteList:
|
| 29 |
+
notes = []
|
| 30 |
+
|
| 31 |
+
def add_note(self, tmp):
|
| 32 |
+
self.notes.append(tmp)
|
| 33 |
+
self.resort()
|
| 34 |
+
|
| 35 |
+
def remove_note(self, note):
|
| 36 |
+
self.notes.remove(note)
|
| 37 |
+
|
| 38 |
+
def resort(self):
|
| 39 |
+
self.notes.sort(key=lambda note: (note.start_time, note.duration()))
|
| 40 |
+
|
| 41 |
+
def show(self):
|
| 42 |
+
for note in self.notes:
|
| 43 |
+
print(note)
|
| 44 |
+
|
| 45 |
+
# joins adjacent notes with same pitch
|
| 46 |
+
def join_notes(self):
|
| 47 |
+
keep_going = True
|
| 48 |
+
while (keep_going):
|
| 49 |
+
keep_going = False
|
| 50 |
+
for i, note in enumerate(self.notes):
|
| 51 |
+
# is there another note starting where i end ?
|
| 52 |
+
candidates = self.get_notes_starting_at(note.end_time)
|
| 53 |
+
candidates = [cnote for cnote in candidates if cnote.pitch == note.pitch and cnote != note]
|
| 54 |
+
if len(candidates) == 0:
|
| 55 |
+
continue
|
| 56 |
+
max_end = max(candidates, key=lambda x: x.end_time).end_time
|
| 57 |
+
# remove all candidates and sets the max dur to the current note
|
| 58 |
+
for cand in candidates:
|
| 59 |
+
self.remove_note(cand)
|
| 60 |
+
note.set_duration(max_end - note.start_time)
|
| 61 |
+
keep_going = True
|
| 62 |
+
break
|
| 63 |
+
|
| 64 |
+
def get_notes_starting_at(self, start):
|
| 65 |
+
return [n for n in self.notes if n.start_time == start]
|
| 66 |
+
|
| 67 |
+
def get_notes_within(self, start, end):
|
| 68 |
+
return [n for n in self.notes if n.start_time <= end and n.end_time >= start]
|
| 69 |
+
|
| 70 |
+
def get_polyphony_at(self, time_point):
|
| 71 |
+
# how many notes are played at that moment ?
|
| 72 |
+
return len([note for note in self.notes if note.start_time < time_point and note.end_time > time_point])
|
| 73 |
+
|
| 74 |
+
def apply_rules(self):
|
| 75 |
+
for i in range(20):
|
| 76 |
+
if random.random(1,100) > 50:
|
| 77 |
+
self.apply_rule_remove()
|
| 78 |
+
else:
|
| 79 |
+
self.apply_rule_split()
|
| 80 |
+
|
| 81 |
+
def apply_rule_remove(self):
|
| 82 |
+
a_note = random.choice(self.notes)
|
| 83 |
+
notes_within = self.get_notes_within(a_note.start_time, a_note.end_time)
|
| 84 |
+
maxp = 0
|
| 85 |
+
for note in notes_within:
|
| 86 |
+
maxp = max(maxp, self.get_polyphony_at(note.start_time))
|
| 87 |
+
maxp = max(maxp, self.get_polyphony_at(note.end_time))
|
| 88 |
+
# print(f"note {a_note} has polyphony {maxp}")
|
| 89 |
+
if maxp > 2:
|
| 90 |
+
self.remove_note(a_note)
|
| 91 |
+
|
| 92 |
+
def apply_rule_split(self):
|
| 93 |
+
a_note = random.choice(self.notes)
|
| 94 |
+
notes_within = self.get_notes_within(a_note.start_time, a_note.end_time)
|
| 95 |
+
maxp = 0
|
| 96 |
+
for note in notes_within:
|
| 97 |
+
maxp = max(maxp, self.get_polyphony_at(note.start_time))
|
| 98 |
+
maxp = max(maxp, self.get_polyphony_at(note.end_time))
|
| 99 |
+
# print(f"note {a_note} has polyphony {maxp}")
|
| 100 |
+
if maxp > 2:
|
| 101 |
+
self.remove_note(a_note)
|
| 102 |
+
|
| 103 |
+
def save_midi(self, file_name):
|
| 104 |
+
mid = MidiFile()
|
| 105 |
+
track = MidiTrack()
|
| 106 |
+
mid.tracks.append(track)
|
| 107 |
+
click_per_beats = 480
|
| 108 |
+
message_list = []
|
| 109 |
+
for note in self.notes:
|
| 110 |
+
message_list.append(Message('note_on', note=note.pitch, velocity=64, time=(int)(click_per_beats * note.start_time)))
|
| 111 |
+
message_list.append(Message('note_off', note=note.pitch, velocity=0, time=(int)(click_per_beats * note.end_time)))
|
| 112 |
+
message_list.sort(key= lambda msg: msg.time)
|
| 113 |
+
last_event_time = 0
|
| 114 |
+
for msg in message_list:
|
| 115 |
+
delta_time = msg.time - last_event_time
|
| 116 |
+
last_event_time = msg.time
|
| 117 |
+
msg.time = delta_time
|
| 118 |
+
track.append(msg)
|
| 119 |
+
mid.save(file_name)
|
| 120 |
+
return file_name
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == '__main__':
|
| 124 |
+
nl = NoteList()
|
| 125 |
+
nl.add_note(TemporalNote(60, 0, 4))
|
| 126 |
+
nl.add_note(TemporalNote(62, 2, 4))
|
| 127 |
+
nl.add_note(TemporalNote(64, 1, 2))
|
| 128 |
+
nl.add_note(TemporalNote(65, 0, 4))
|
| 129 |
+
nl.add_note(TemporalNote(67, 1, 3))
|
| 130 |
+
nl.add_note(TemporalNote(68, 2, 3))
|
| 131 |
+
# nl.show()
|
| 132 |
+
nl.save_midi('nl_output.mid')
|
verovio_app/Dockerfile_Verovio
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12
|
| 2 |
+
|
| 3 |
+
# Install pip + Gradio dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
swig \
|
| 6 |
+
g++ \
|
| 7 |
+
cmake \
|
| 8 |
+
make \
|
| 9 |
+
libglib2.0-dev \
|
| 10 |
+
libxml2-dev \
|
| 11 |
+
libzip-dev \
|
| 12 |
+
libcurl4-openssl-dev \
|
| 13 |
+
fluidsynth \
|
| 14 |
+
libfluidsynth-dev \
|
| 15 |
+
ffmpeg \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
# Install Python packages
|
| 18 |
+
RUN pip install --upgrade pip
|
| 19 |
+
#RUN pip install verovio
|
| 20 |
+
#RUN python3 -c "import verovio, sys; sys.stderr.write('✅ Verovio version: ' + verovio.toolkit().getVersion() + '\n')"
|
| 21 |
+
#RUN pip install gradio
|
| 22 |
+
#RUN pip install music21
|
| 23 |
+
|
| 24 |
+
COPY requirements.txt .
|
| 25 |
+
RUN pip install -r requirements.txt
|
| 26 |
+
|
| 27 |
+
# Copy app and MEI file
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
COPY . .
|
| 30 |
+
|
| 31 |
+
# Change ownership to user id 1000 (default HF user)
|
| 32 |
+
RUN chown -R 1000:1000 /app
|
| 33 |
+
|
| 34 |
+
# Switch to user id 1000 (non-root, for HF)
|
| 35 |
+
USER 1000
|
| 36 |
+
|
| 37 |
+
RUN which fluidsynth && fluidsynth --version || echo "❌ Fluidsynth not found"
|
| 38 |
+
|
| 39 |
+
CMD ["python3", "app.py"]
|
verovio_app/__init__.py
ADDED
|
File without changes
|
verovio_app/app_verovio.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ast
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import numpy as np
|
| 4 |
+
from mido import Message, MidiFile, MidiTrack
|
| 5 |
+
import os
|
| 6 |
+
from music21 import converter, note, stream, chord, tempo, meter
|
| 7 |
+
import hexachords
|
| 8 |
+
from format_conversions import Format_Converter
|
| 9 |
+
import verovio
|
| 10 |
+
import subprocess
|
| 11 |
+
|
| 12 |
+
from legacy.essai_mido_to_xml import midi_path
|
| 13 |
+
|
| 14 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
|
| 16 |
+
def get_verovio_resource_path():
|
| 17 |
+
for path in verovio.__path__:
|
| 18 |
+
candidate = os.path.join(path, "data")
|
| 19 |
+
if os.path.exists(candidate):
|
| 20 |
+
return candidate
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
# Initialize the Verovio toolkit with rendering options
|
| 24 |
+
tk = verovio.toolkit()
|
| 25 |
+
resource_path = get_verovio_resource_path()
|
| 26 |
+
print(f"resource path: {os.listdir(resource_path)}")
|
| 27 |
+
print("[Debug] Using resource path:", resource_path)
|
| 28 |
+
if resource_path:
|
| 29 |
+
try:
|
| 30 |
+
tk.setResourcePath(resource_path)
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print("[Error] Failed to set resourcePath:", e)
|
| 33 |
+
tk.setOptions({
|
| 34 |
+
"adjustPageWidth": True,
|
| 35 |
+
"header": 'none', # This disables the rendering of the title
|
| 36 |
+
"scale": 70,
|
| 37 |
+
"adjustPageHeight": True,
|
| 38 |
+
"landscape": False,
|
| 39 |
+
})
|
| 40 |
+
print(tk.getOptions())
|
| 41 |
+
|
| 42 |
+
class HexachordApp:
|
| 43 |
+
|
| 44 |
+
def __init__(self):
|
| 45 |
+
self._hexachord = hexachords.Hexachord()
|
| 46 |
+
self.ui = None
|
| 47 |
+
self.on_huggingface = "HUGGINGFACE_SPACE" in os.environ
|
| 48 |
+
|
| 49 |
+
def is_fsynth_installed(self):
|
| 50 |
+
try:
|
| 51 |
+
subprocess.run(["fluidsynth", "--version"], check=True)
|
| 52 |
+
print('fluidsynth is installed')
|
| 53 |
+
return True
|
| 54 |
+
except Exception:
|
| 55 |
+
print('fluidsynth is NOT installed')
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
def generate_chords(self, note_names, itvl):
|
| 59 |
+
interval_21 = 'P5'
|
| 60 |
+
if itvl == 'fourth':
|
| 61 |
+
interval_21 = 'P4'
|
| 62 |
+
return self._hexachord.generate_base_sequence(note_names, intrvl=interval_21)
|
| 63 |
+
|
| 64 |
+
def generate_realizations(self):
|
| 65 |
+
# returns triples of midipath, score image, audio player
|
| 66 |
+
reals = self._hexachord.generate_3_chords_realizations(self._hexachord._base_sequence)
|
| 67 |
+
all_paths = []
|
| 68 |
+
fm = Format_Converter()
|
| 69 |
+
for i, real in enumerate(reals):
|
| 70 |
+
midi_path = f"real{i}.mid"
|
| 71 |
+
all_paths.append(self.create_midi_file(real, midi_path))
|
| 72 |
+
all_paths.append(fm.midi_to_svg_file(tk, midi_path, f"real{i}.svg"))
|
| 73 |
+
all_paths.append(fm.convert_midi_to_audio(midi_path, f"real{i}"))
|
| 74 |
+
return tuple(all_paths)
|
| 75 |
+
|
| 76 |
+
def create_midi_file(self, chords, file_name, duration_in_quarter_lengths=4.0, tempo_bpm=120, time_signature="4/4"):
|
| 77 |
+
midi_path = os.path.join(BASE_DIR, file_name)
|
| 78 |
+
self._hexachord.save_chords_to_midi_file(chords, midi_path)
|
| 79 |
+
return file_name
|
| 80 |
+
|
| 81 |
+
def generate_svg(self, midi_file, output_file_name):
|
| 82 |
+
return Format_Converter().midi_to_svg_file(tk, midi_file, output_file_name)
|
| 83 |
+
|
| 84 |
+
def launch_score_editor(self, midi_path):
|
| 85 |
+
try:
|
| 86 |
+
score = converter.parse(midi_path)
|
| 87 |
+
score.show('musicxml')
|
| 88 |
+
return "Opened MIDI file in the default score editor!"
|
| 89 |
+
except Exception as e:
|
| 90 |
+
return f"Error opening score editor: {str(e)}"
|
| 91 |
+
|
| 92 |
+
def build_octave_dependent_notes_from_string(self, note_string):
|
| 93 |
+
start_octave = 3
|
| 94 |
+
notes = []
|
| 95 |
+
previous_note = None
|
| 96 |
+
for nn in note_string.split():
|
| 97 |
+
n = note.Note(nn)
|
| 98 |
+
n.octave = start_octave
|
| 99 |
+
if previous_note is not None and n.pitch.midi < previous_note.pitch.midi:
|
| 100 |
+
n.octave = n.octave + 1
|
| 101 |
+
start_octave += 1
|
| 102 |
+
notes.append(n)
|
| 103 |
+
previous_note = n
|
| 104 |
+
return notes
|
| 105 |
+
|
| 106 |
+
def process_hexachord(self, hexachord_str, itvl):
|
| 107 |
+
try:
|
| 108 |
+
notes = self.build_octave_dependent_notes_from_string(hexachord_str)
|
| 109 |
+
if len(notes) != 6 or len(set(notes)) != 6:
|
| 110 |
+
return "Please enter exactly 6 unique notes."
|
| 111 |
+
except ValueError:
|
| 112 |
+
return "Invalid input. Enter 6 notes separated by spaces."
|
| 113 |
+
fm = Format_Converter()
|
| 114 |
+
chords = self.generate_chords(notes, itvl)
|
| 115 |
+
midi_path = self.create_midi_file(chords, "../base_chords.mid")
|
| 116 |
+
score_path = fm.midi_to_svg_file(tk, midi_path, "score_base_chords.svg")
|
| 117 |
+
audio_path = fm.convert_midi_to_audio(midi_path, "base_chords")
|
| 118 |
+
return (midi_path, score_path, audio_path) + self.generate_realizations()
|
| 119 |
+
|
| 120 |
+
def generate_movements(self):
|
| 121 |
+
# take 2 realizations of the same root
|
| 122 |
+
everyone = ()
|
| 123 |
+
for index_of_chord in range(6):
|
| 124 |
+
chords = [real[index_of_chord] for real in self._hexachord._realizations]
|
| 125 |
+
fm = Format_Converter()
|
| 126 |
+
midi_path = self.create_midi_file(chords, f"movement{index_of_chord}.mid")
|
| 127 |
+
score_path = fm.midi_to_svg_file(tk, midi_path, f"movement{index_of_chord}.svg")
|
| 128 |
+
audio_path = fm.convert_midi_to_audio(midi_path, f"movement{index_of_chord}")
|
| 129 |
+
everyone = everyone + (midi_path, score_path, audio_path)
|
| 130 |
+
return everyone
|
| 131 |
+
|
| 132 |
+
def render(self):
|
| 133 |
+
with gr.Blocks() as ui:
|
| 134 |
+
gr.Markdown("# Hexachord-based Chord Generator")
|
| 135 |
+
with gr.Tabs():
|
| 136 |
+
with gr.TabItem("Hexachord Generator"):
|
| 137 |
+
with gr.Row():
|
| 138 |
+
hexachord_selector = gr.Dropdown(label="Select Known Hexachord",
|
| 139 |
+
choices=self.get_known_hexachords_choice(), value=None, interactive=True)
|
| 140 |
+
hexachord_input = gr.Textbox(
|
| 141 |
+
label="Enter 6 pitchclasses, separated by spaces",
|
| 142 |
+
value="C D E G A B",
|
| 143 |
+
interactive = True
|
| 144 |
+
)
|
| 145 |
+
interval_switch = gr.Radio(
|
| 146 |
+
choices=["fourth", "fifth"],
|
| 147 |
+
label="Select Interval",
|
| 148 |
+
value="fifth"
|
| 149 |
+
)
|
| 150 |
+
generate_button = gr.Button("Generate Chords")
|
| 151 |
+
with gr.Row():
|
| 152 |
+
gr.Markdown(f"#### base chords")
|
| 153 |
+
midi_output = gr.File(label="Download MIDI File", scale=1)
|
| 154 |
+
score_output = gr.Image(label="Score Visualization", scale=3)
|
| 155 |
+
audio_output = gr.Audio(label="Play Generated Chords", value=None, interactive=False, scale=3)
|
| 156 |
+
realization_outputs = [midi_output, score_output, audio_output]
|
| 157 |
+
for i in range(3): # Three alternative realizations
|
| 158 |
+
with gr.Row():
|
| 159 |
+
gr.Markdown(f"#### Realization {i + 1}")
|
| 160 |
+
midi_output = gr.File(label="Download MIDI File", scale=1)
|
| 161 |
+
piano_roll = gr.Image(label=f"Piano Roll {i + 1}", scale=3)
|
| 162 |
+
audio_player = gr.Audio(label=f"Play Chords {i + 1}", interactive=False, scale=3)
|
| 163 |
+
realization_outputs += (midi_output, piano_roll, audio_player)
|
| 164 |
+
|
| 165 |
+
hexachord_selector.change(
|
| 166 |
+
fn=self.get_selected_hexachord,
|
| 167 |
+
inputs=[hexachord_selector],
|
| 168 |
+
outputs=[hexachord_input]
|
| 169 |
+
)
|
| 170 |
+
generate_button.click(
|
| 171 |
+
fn=self.process_hexachord,
|
| 172 |
+
inputs=[hexachord_input, interval_switch],
|
| 173 |
+
# outputs=[midi_output, piano_roll_output, audio_output]
|
| 174 |
+
outputs = realization_outputs
|
| 175 |
+
)
|
| 176 |
+
# Pressing Enter in the textbox also triggers processing
|
| 177 |
+
hexachord_input.submit(
|
| 178 |
+
fn=self.process_hexachord,
|
| 179 |
+
inputs=[hexachord_input, interval_switch],
|
| 180 |
+
outputs=realization_outputs
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
with gr.TabItem("Movements"):
|
| 184 |
+
gr.Markdown("Movements")
|
| 185 |
+
with gr.Row():
|
| 186 |
+
generate_mvmt_button = gr.Button("Generate Movements")
|
| 187 |
+
realization_outputs = []
|
| 188 |
+
for i in range(6): # Three alternative realizations
|
| 189 |
+
with gr.Row():
|
| 190 |
+
gr.Markdown(f"#### Movement {i + 1}")
|
| 191 |
+
midi_output = gr.File(label="Download MIDI File", scale=1)
|
| 192 |
+
piano_roll = gr.Image(label=f"Piano Roll {i + 1}", scale=3)
|
| 193 |
+
audio_player = gr.Audio(label=f"Play Chords {i + 1}", interactive=False, scale=3)
|
| 194 |
+
realization_outputs += (midi_output, piano_roll, audio_player)
|
| 195 |
+
generate_mvmt_button.click(
|
| 196 |
+
fn=self.generate_movements,
|
| 197 |
+
inputs=[],
|
| 198 |
+
outputs = realization_outputs
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
with gr.TabItem("Settings"):
|
| 202 |
+
gr.Markdown("### Configuration Options")
|
| 203 |
+
setting_1 = gr.Checkbox(label="Enable Advanced Mode")
|
| 204 |
+
setting_2 = gr.Slider(0, 100, label="Complexity Level")
|
| 205 |
+
self.ui = ui
|
| 206 |
+
|
| 207 |
+
def get_known_hexachords_choice(self):
|
| 208 |
+
return self._hexachord.known_hexachords
|
| 209 |
+
|
| 210 |
+
def get_selected_hexachord(self, x):
|
| 211 |
+
# lambda x: {"Hexachord 1": "C3 D3 E3 G3 A3 B3", "Hexachord 2": "D3 E3 F3 A3 B3 C4",
|
| 212 |
+
# "Hexachord 3": "E3 G3 A3 C4 D4 F4"}.get(x, "")
|
| 213 |
+
item = x[x.index('['):x.index(']')+1]
|
| 214 |
+
int_array = np.array(ast.literal_eval(item))
|
| 215 |
+
hexa_string = ''
|
| 216 |
+
start_note = note.Note('C3')
|
| 217 |
+
for i in int_array:
|
| 218 |
+
add_note = start_note.transpose(int(i))
|
| 219 |
+
hexa_string = hexa_string + ' ' + add_note.nameWithOctave
|
| 220 |
+
return hexa_string
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def launch_app():
|
| 224 |
+
hex = HexachordApp()
|
| 225 |
+
hex.is_fsynth_installed()
|
| 226 |
+
hex.render()
|
| 227 |
+
if hex.on_huggingface:
|
| 228 |
+
hex.ui.launch(server_name="0.0.0.0", server_port=7860, share=True)
|
| 229 |
+
else:
|
| 230 |
+
hex.ui.launch(server_name="0.0.0.0", server_port=7860)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
launch_app()
|
| 234 |
+
|
verovio_app/hexachords.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
from mido import MidiFile, Message, MidiTrack
|
| 4 |
+
from music21 import note, stream, interval, meter, chord
|
| 5 |
+
from ortools.sat.python import cp_model
|
| 6 |
+
|
| 7 |
+
class Hexachord:
|
| 8 |
+
known_hexachords = [
|
| 9 |
+
"6-1 [0,1,2,3,4,5] Chromatic hexachord",
|
| 10 |
+
"6-7 [0,1,2,6,7,8] Two-semitone tritone scale",
|
| 11 |
+
"6-Z17A [0,1,2,4,7,8] All-trichord hexachord",
|
| 12 |
+
"6-20 [0,1,4,5,8,9] Augmented scale, Ode-to-Napoleon hexachord",
|
| 13 |
+
"6-Z24A [0,1,3,4,6,8] Minor major eleventh chord",
|
| 14 |
+
"6-Z24B [0,2,4,5,7,8] Half-diminished eleventh chord",
|
| 15 |
+
"6-Z25A [0,1,3,5,6,8] Major eleventh chord",
|
| 16 |
+
"6-Z26 [0,1,3,5,7,8] Major ninth sharp eleventh chord",
|
| 17 |
+
"6-27B [0,2,3,5,6,9] Diminished eleventh chord",
|
| 18 |
+
"6-Z28 [0,1,3,5,6,9] Augmented major eleventh chord",
|
| 19 |
+
"6-Z29 [0,2,3,6,7,9] Bridge chord",
|
| 20 |
+
"6-30B [0,2,3,6,8,9] Petrushka chord, tritone scale",
|
| 21 |
+
"6-32 [0,2,4,5,7,9] Diatonic hexachord, minor eleventh chord",
|
| 22 |
+
"6-33B [0,2,4,6,7,9] Dominant eleventh chord",
|
| 23 |
+
"6-34A [0,1,3,5,7,9] Mystic chord",
|
| 24 |
+
"6-34B [0,2,4,6,8,9] Augmented eleventh chord, dominant sharp eleventh chord, Prélude chord",
|
| 25 |
+
"6-35 [0,2,4,6,8,T] Whole tone scale",
|
| 26 |
+
"6-Z44A [0,1,2,5,6,9] Schoenberg hexachord",
|
| 27 |
+
"6-Z46A [0,1,2,4,6,9] Scale of harmonics",
|
| 28 |
+
"6-Z47B [0,2,3,4,7,9] Blues scale"]
|
| 29 |
+
_base_sequence = None
|
| 30 |
+
_realizations = []
|
| 31 |
+
|
| 32 |
+
def generate_chord_sequence_from_midi_pitches(self, list_of_mp, intrvl="P5"):
|
| 33 |
+
return self.generate_base_sequence([note.Note(mp).nameWithOctave for mp in list_of_mp], intrvl=intrvl)
|
| 34 |
+
|
| 35 |
+
def generate_base_sequence(self, six_notes, intrvl="P5"):
|
| 36 |
+
fifth = interval.Interval(intrvl) # Perfect fifth
|
| 37 |
+
all_pc = [n.pitch.pitchClass for n in six_notes]
|
| 38 |
+
all_chords = []
|
| 39 |
+
for n in six_notes:
|
| 40 |
+
ch = chord.Chord([n])
|
| 41 |
+
current_note = n
|
| 42 |
+
while len(ch) < 6:
|
| 43 |
+
current_note = fifth.transposeNote(current_note)
|
| 44 |
+
if current_note.pitch.pitchClass in all_pc and current_note not in ch:
|
| 45 |
+
while interval.Interval(noteStart=ch[-1], noteEnd=current_note).semitones > (12 + 7):
|
| 46 |
+
current_note = current_note.transpose(-12)
|
| 47 |
+
ch.add(current_note)
|
| 48 |
+
all_chords.append(ch)
|
| 49 |
+
self._base_sequence = all_chords
|
| 50 |
+
return all_chords
|
| 51 |
+
|
| 52 |
+
def generate_3_chords_realizations(self, chord_seq):
|
| 53 |
+
# lower each of the top to notes by 2 ocatves
|
| 54 |
+
res1 = []
|
| 55 |
+
res2 = []
|
| 56 |
+
res3 = []
|
| 57 |
+
for ch in chord_seq:
|
| 58 |
+
new_ch = chord.Chord()
|
| 59 |
+
for i, n in enumerate(ch.notes):
|
| 60 |
+
if i == 4 or i == 5:
|
| 61 |
+
new_ch.add(n.transpose(-24))
|
| 62 |
+
else:
|
| 63 |
+
new_ch.add(n)
|
| 64 |
+
res1.append(new_ch)
|
| 65 |
+
for ch in chord_seq:
|
| 66 |
+
new_ch = chord.Chord()
|
| 67 |
+
for i, n in enumerate(ch.notes):
|
| 68 |
+
if i == 4 or i == 5:
|
| 69 |
+
new_ch.add(n.transpose(-24))
|
| 70 |
+
elif i == 3:
|
| 71 |
+
new_ch.add(n.transpose(-12))
|
| 72 |
+
else:
|
| 73 |
+
new_ch.add(n)
|
| 74 |
+
res2.append(new_ch)
|
| 75 |
+
for ch in chord_seq:
|
| 76 |
+
new_ch = chord.Chord()
|
| 77 |
+
for i, n in enumerate(ch.notes):
|
| 78 |
+
if i == 4 or i == 5:
|
| 79 |
+
new_ch.add(n.transpose(-24))
|
| 80 |
+
elif i == 3 or i == 2:
|
| 81 |
+
new_ch.add(n.transpose(-12))
|
| 82 |
+
else:
|
| 83 |
+
new_ch.add(n)
|
| 84 |
+
res3.append(new_ch)
|
| 85 |
+
self._realizations = res1, res2, res3
|
| 86 |
+
return self._realizations
|
| 87 |
+
|
| 88 |
+
def chords_to_m21(self, chords):
|
| 89 |
+
s = stream.Stream()
|
| 90 |
+
s.append(meter.TimeSignature("4/4"))
|
| 91 |
+
for c in chords:
|
| 92 |
+
ch = chord.Chord(c)
|
| 93 |
+
ch.duration.quarterLength = 4
|
| 94 |
+
s.append(ch)
|
| 95 |
+
return s
|
| 96 |
+
|
| 97 |
+
def chords_to_m21_voices(self, chords):
|
| 98 |
+
s = stream.Stream()
|
| 99 |
+
s.append(meter.TimeSignature("4/4"))
|
| 100 |
+
for i_chord, c in enumerate(chords):
|
| 101 |
+
for chord_note in c:
|
| 102 |
+
n = note.Note(chord_note.pitch)
|
| 103 |
+
n.duration.quarterLength = 4
|
| 104 |
+
s.insert(i_chord * 4, n)
|
| 105 |
+
return s
|
| 106 |
+
|
| 107 |
+
def save_chords_to_midi_file(self, chords, file_name):
|
| 108 |
+
m21 = self.chords_to_m21_voices(chords)
|
| 109 |
+
m21.write('midi', file_name)
|
| 110 |
+
return file_name
|
| 111 |
+
|
| 112 |
+
def alternate_chords(self, s1, s2):
|
| 113 |
+
"""Create a new stream alternating between chords from s1 and s2"""
|
| 114 |
+
new_stream = stream.Stream()
|
| 115 |
+
|
| 116 |
+
# Get chords from both streams
|
| 117 |
+
chords1 = list(s1.getElementsByClass(chord.Chord))
|
| 118 |
+
chords2 = list(s2.getElementsByClass(chord.Chord))
|
| 119 |
+
|
| 120 |
+
# Interleave chords from s1 and s2
|
| 121 |
+
for c1, c2 in zip(chords1, chords2):
|
| 122 |
+
new_stream.append(c1)
|
| 123 |
+
new_stream.append(c2)
|
| 124 |
+
return new_stream
|
| 125 |
+
|
| 126 |
+
def optimize_voice_leading(self, chord_sequence):
|
| 127 |
+
model = cp_model.CpModel()
|
| 128 |
+
octave_variables = {}
|
| 129 |
+
movement_vars = []
|
| 130 |
+
|
| 131 |
+
# Define variables and domains (allowing octave shifts)
|
| 132 |
+
for i, ch in enumerate(chord_sequence):
|
| 133 |
+
for n in ch.notes:
|
| 134 |
+
var_name = f"chord_{i}_note_{n.nameWithOctave}"
|
| 135 |
+
octave_variables[var_name] = model.NewIntVar(n.octave - 1, n.octave + 1,
|
| 136 |
+
var_name) # Allow octave shifts
|
| 137 |
+
spread_vars = []
|
| 138 |
+
# Add constraints to minimize movement between chords
|
| 139 |
+
for i in range(len(chord_sequence) - 1):
|
| 140 |
+
max_octave = model.NewIntVar(0, 10, "max_pitch" + str(i))
|
| 141 |
+
min_octave = model.NewIntVar(0, 10, "min_pitch" + str(i))
|
| 142 |
+
for n in chord_sequence[i]:
|
| 143 |
+
v = octave_variables[f"chord_{i}_note_{n.nameWithOctave}"]
|
| 144 |
+
# model.Add(max_pitch >= v) # max_pitch must be at least as high as any note
|
| 145 |
+
# model.Add(min_pitch <= v) # min_pitch must
|
| 146 |
+
spread_var = max_octave - min_octave
|
| 147 |
+
spread_vars.append(spread_var)
|
| 148 |
+
for i in range(len(chord_sequence) - 1):
|
| 149 |
+
for n1, n2 in zip(chord_sequence[i].notes, chord_sequence[i + 1].notes):
|
| 150 |
+
var1 = octave_variables[f"chord_{i}_note_{n1.nameWithOctave}"]
|
| 151 |
+
var2 = octave_variables[f"chord_{i + 1}_note_{n2.nameWithOctave}"]
|
| 152 |
+
# Define movement variable
|
| 153 |
+
movement_var = model.NewIntVar(0, 36, f"movement_{i}_{n1.name}")
|
| 154 |
+
model.AddAbsEquality(movement_var, var2 - var1)
|
| 155 |
+
# Track movement variable in objective function
|
| 156 |
+
movement_vars.append(movement_var)
|
| 157 |
+
# Define objective: minimize sum of all movement values
|
| 158 |
+
model.Minimize(sum(spread_vars))
|
| 159 |
+
# obj_var = sum(movement_vars)
|
| 160 |
+
# model.Minimize(obj_var)
|
| 161 |
+
# Solve
|
| 162 |
+
solver = cp_model.CpSolver()
|
| 163 |
+
solver.Solve(model)
|
| 164 |
+
# print(solver.Value(obj_var))
|
| 165 |
+
# for v in variables:
|
| 166 |
+
# print(v)
|
| 167 |
+
# print(variables[v].Proto().domain)
|
| 168 |
+
# print(solver.Value(variables[v]))
|
| 169 |
+
# Apply changes to music21 chord sequence
|
| 170 |
+
optimized_chords = []
|
| 171 |
+
for i, ch in enumerate(chord_sequence):
|
| 172 |
+
new_chord = chord.Chord(
|
| 173 |
+
[note.Note(f"{n.name}{solver.Value(octave_variables[f'chord_{i}_note_{n.nameWithOctave}'])}")
|
| 174 |
+
for n in ch.notes])
|
| 175 |
+
optimized_chords.append(new_chord)
|
| 176 |
+
|
| 177 |
+
return optimized_chords
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
if __name__ == '__main__':
|
| 181 |
+
hexa = Hexachord()
|
| 182 |
+
note_names = ["C3", "Eb3", "E3", "F#3", "G3", "Bb3"]
|
| 183 |
+
notes = [note.Note(n) for n in note_names]
|
| 184 |
+
cs1 = hexa.generate_base_sequence(notes, intrvl="P4")
|
| 185 |
+
# cs1 = generate_chord_sequence(["E3", "G3", "Ab3", "B3", "C4", "Eb4"])
|
| 186 |
+
# cs2 = generate_chord_sequence(["C3", "F3", "F#3", "A3", "B4", "E4"])
|
| 187 |
+
# alternation = alternate_chords(cs1, cs2)
|
| 188 |
+
# alternation.write('midi',"alternation.mid")
|
| 189 |
+
|
| 190 |
+
hexa.save_chords_to_midi_file(cs1, 'temp.mid')
|
| 191 |
+
# optimized = optimize_voice_leading([c1, c2, c3])
|
| 192 |
+
optimized = hexa.optimize_voice_leading(cs1)
|
| 193 |
+
stream1 = stream.Stream(optimized)
|
| 194 |
+
stream1.show('text')
|
| 195 |
+
# stream1.write('midi', "optimized.mid")
|