pachet commited on
Commit
c2f178d
·
1 Parent(s): 6e4f2b4

added smallmuse and movements

Browse files
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
- @classmethod
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")