pachet commited on
Commit
6fcf15f
·
1 Parent(s): d020355

Add new app and working version files

Browse files
Files changed (4) hide show
  1. app.py +4 -46
  2. hexachords.py +10 -0
  3. new_app.py +265 -0
  4. working_version.py +223 -0
app.py CHANGED
@@ -9,49 +9,6 @@ from pydub import AudioSegment
9
  import hexachords
10
 
11
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
12
- os.environ["VEROVIO_FONT_PATH"] = "/usr/local/share/verovio/fonts"
13
-
14
- def install_verovio():
15
- verovio_dir = "verovio"
16
- build_dir = os.path.join(verovio_dir, "build")
17
- cmake_source_dir = os.path.join(verovio_dir, "cmake") # The correct directory containing CMakeLists.txt
18
- verovio_executable = os.path.join(build_dir, "verovio")
19
-
20
- if not os.path.exists(verovio_executable):
21
- print("Installing Verovio...")
22
- try:
23
- # Clone the Verovio repository
24
- subprocess.run(["git", "clone", "--recursive", "https://github.com/rism-digital/verovio.git"], check=True)
25
- # Ensure build directory exists
26
- os.makedirs(build_dir, exist_ok=True)
27
- # Move into the build directory
28
- os.chdir(build_dir)
29
- # Run CMake using the correct source path (`../cmake`)
30
- subprocess.run(["cmake", "../cmake"], check=True)
31
- # Compile Verovio
32
- subprocess.run(["make", "-j4"], check=True)
33
- # Move back to the original directory
34
- os.chdir("../../")
35
- except subprocess.CalledProcessError as e:
36
- print(f"An error occurred during Verovio installation: {e}")
37
- os.chdir("../../") # Ensure returning to the original directory in case of error
38
- else:
39
- print("Verovio is already installed.")
40
- install_verovio() # Ensure Verovio is installed before running Gradio
41
-
42
- # Ensure Verovio has the Bravura font
43
- def install_bravura():
44
- font_path = "/usr/local/share/verovio/fonts"
45
- os.makedirs(font_path, exist_ok=True) # Ensure the folder exists
46
- bravura_font = os.path.join(font_path, "Bravura.otf")
47
-
48
- if not os.path.exists(bravura_font):
49
- print("Downloading Bravura font...")
50
- os.system(f"curl -L -o {bravura_font} https://github.com/steinbergmedia/bravura/releases/download/v1.392/Bravura-1.392.otf")
51
- else:
52
- print("Bravura font already installed.")
53
-
54
- install_bravura()
55
 
56
  class HexachordApp:
57
 
@@ -115,6 +72,7 @@ class HexachordApp:
115
  mid.save(midi_path)
116
  return midi_path
117
 
 
118
  def convert_midi_to_audio(self, midi_path, file_name):
119
  if not shutil.which("fluidsynth"):
120
  try:
@@ -136,8 +94,8 @@ class HexachordApp:
136
  except Exception as e:
137
  return f"Error converting MIDI to audio: {str(e)}"
138
 
139
- def midi_to_svg(self, midi_file, output_file_name):
140
- return self._hexachord.midi_to_svg(midi_file, output_file_name)
141
 
142
  def generate_piano_roll(self, chords, label):
143
  fig, ax = plt.subplots(figsize=(8, 4))
@@ -183,7 +141,7 @@ class HexachordApp:
183
  chords = self.generate_chords(notes, itvl)
184
  midi_path = self.create_midi(chords, "base_chords.mid")
185
  # piano_roll_path = self.generate_piano_roll(chords, "base_chords.png")
186
- score_path = self.midi_to_svg (midi_path, "score_base_chords.svg")
187
  audio_path = self.convert_midi_to_audio(midi_path, "base_chords")
188
 
189
  return midi_path, score_path, audio_path
 
9
  import hexachords
10
 
11
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  class HexachordApp:
14
 
 
72
  mid.save(midi_path)
73
  return midi_path
74
 
75
+
76
  def convert_midi_to_audio(self, midi_path, file_name):
77
  if not shutil.which("fluidsynth"):
78
  try:
 
94
  except Exception as e:
95
  return f"Error converting MIDI to audio: {str(e)}"
96
 
97
+ def generate_png(self, midi_file, output_file_name):
98
+ self._hexachord.midi_to_png(midi_file, output_file_name)
99
 
100
  def generate_piano_roll(self, chords, label):
101
  fig, ax = plt.subplots(figsize=(8, 4))
 
141
  chords = self.generate_chords(notes, itvl)
142
  midi_path = self.create_midi(chords, "base_chords.mid")
143
  # piano_roll_path = self.generate_piano_roll(chords, "base_chords.png")
144
+ score_path = self.generate_png (midi_path, "score_base_chords.png")
145
  audio_path = self.convert_midi_to_audio(midi_path, "base_chords")
146
 
147
  return midi_path, score_path, audio_path
hexachords.py CHANGED
@@ -2,6 +2,8 @@ import os
2
  import subprocess
3
  from music21 import note, stream, interval, meter, chord, converter, metadata
4
  from ortools.sat.python import cp_model
 
 
5
 
6
  class Hexachord:
7
 
@@ -139,6 +141,14 @@ class Hexachord:
139
 
140
  return optimized_chords
141
 
 
 
 
 
 
 
 
 
142
  def midi_to_svg(self, midi_file, svg_output):
143
  """Convert MIDI to SVG using Verovio's command-line tool."""
144
  score = converter.parse(midi_file)
 
2
  import subprocess
3
  from music21 import note, stream, interval, meter, chord, converter, metadata
4
  from ortools.sat.python import cp_model
5
+ from verovio import verovio
6
+
7
 
8
  class Hexachord:
9
 
 
141
 
142
  return optimized_chords
143
 
144
+ def midi_to_svg_file(self, midi_file, output_file):
145
+ score = converter.parse(midi_file)
146
+ musicxml_data = score.write('musicxml') # Get MusicXML as a string
147
+ # Step 2: Load MusicXML into Verovio
148
+ tk = verovio.toolkit()
149
+ tk.loadData(musicxml_data.encode()) # Convert to bytes and load into Verovio
150
+ tk.renderToSVGFile(output_file, 1)
151
+
152
  def midi_to_svg(self, midi_file, svg_output):
153
  """Convert MIDI to SVG using Verovio's command-line tool."""
154
  score = converter.parse(midi_file)
new_app.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ import gradio as gr
3
+ from matplotlib import pyplot as plt
4
+ from mido import Message, MidiFile, MidiTrack
5
+ import os
6
+ import subprocess
7
+ from music21 import converter, note
8
+ from pydub import AudioSegment
9
+ import hexachords
10
+
11
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
12
+ os.environ["VEROVIO_FONT_PATH"] = "/usr/local/share/verovio/fonts"
13
+
14
+ def install_verovio():
15
+ verovio_dir = "verovio"
16
+ build_dir = os.path.join(verovio_dir, "build")
17
+ cmake_source_dir = os.path.join(verovio_dir, "cmake") # The correct directory containing CMakeLists.txt
18
+ verovio_executable = os.path.join(build_dir, "verovio")
19
+
20
+ if not os.path.exists(verovio_executable):
21
+ print("Installing Verovio...")
22
+ try:
23
+ # Clone the Verovio repository
24
+ subprocess.run(["git", "clone", "--recursive", "https://github.com/rism-digital/verovio.git"], check=True)
25
+ # Ensure build directory exists
26
+ os.makedirs(build_dir, exist_ok=True)
27
+ # Move into the build directory
28
+ os.chdir(build_dir)
29
+ # Run CMake using the correct source path (`../cmake`)
30
+ subprocess.run(["cmake", "../cmake"], check=True)
31
+ # Compile Verovio
32
+ subprocess.run(["make", "-j4"], check=True)
33
+ # Move back to the original directory
34
+ os.chdir("../../")
35
+ except subprocess.CalledProcessError as e:
36
+ print(f"An error occurred during Verovio installation: {e}")
37
+ os.chdir("../../") # Ensure returning to the original directory in case of error
38
+ else:
39
+ print("Verovio is already installed.")
40
+ install_verovio() # Ensure Verovio is installed before running Gradio
41
+
42
+ # Ensure Verovio has the Bravura font
43
+ def install_bravura():
44
+ font_path = "/usr/local/share/verovio/fonts"
45
+ os.makedirs(font_path, exist_ok=True) # Ensure the folder exists
46
+ bravura_font = os.path.join(font_path, "Bravura.otf")
47
+
48
+ if not os.path.exists(bravura_font):
49
+ print("Downloading Bravura font...")
50
+ os.system(f"curl -L -o {bravura_font} https://github.com/steinbergmedia/bravura/releases/download/v1.392/Bravura-1.392.otf")
51
+ else:
52
+ print("Bravura font already installed.")
53
+
54
+ install_bravura()
55
+
56
+ class HexachordApp:
57
+
58
+ def __init__(self):
59
+ self._hexachord = None
60
+ self._hexachord_base_sequence = None
61
+ self.ui = None
62
+ self.on_huggingface = "HUGGINGFACE_SPACE" in os.environ
63
+
64
+ def is_fsynth_installed(self):
65
+ """ Check to make sure fluidsynth exists in the PATH """
66
+ for path in os.environ['PATH'].split(os.pathsep):
67
+ f = os.path.join(path, 'fluidsynth')
68
+ if os.path.exists(f) and os.access(f, os.X_OK):
69
+ print('fluidsynth is installed')
70
+ return True
71
+ print('fluidsynth is NOT installed')
72
+ return False
73
+
74
+ def generate_chords(self, note_names, itvl):
75
+ # Placeholder for your actual chord generation function
76
+ # Assuming hexachord is a list of MIDI note numbers
77
+ self._hexachord = hexachords.Hexachord()
78
+ interval_21 = 'P5'
79
+ if itvl == 'fourth':
80
+ interval_21 = 'P4'
81
+ self._hexachord_base_sequence = self._hexachord.generate_chord_sequence(note_names, intrvl=interval_21)
82
+ return self._hexachord_base_sequence
83
+
84
+ def generate_realizations(self):
85
+ # returns 3 triples of midipath, piano roll, audio player
86
+ reals = self._hexachord.generate_3_chords_realizations(self._hexachord_base_sequence)
87
+ midi_path1 = self.create_midi(reals[0], "real1.mid")
88
+ piano_roll_path1 = self.generate_piano_roll(reals[0], "real1.png")
89
+ audio_path1 = self.convert_midi_to_audio(midi_path1, "real1")
90
+ midi_path2 = self.create_midi(reals[1], "real2.mid")
91
+ piano_roll_path2 = self.generate_piano_roll(reals[1], "real2.png")
92
+ audio_path2 = self.convert_midi_to_audio(midi_path2, "real2")
93
+ midi_path3 = self.create_midi(reals[2], "real3.mid")
94
+ piano_roll_path3 = self.generate_piano_roll(reals[2], "real3.png")
95
+ audio_path3 = self.convert_midi_to_audio(midi_path3, "real3")
96
+ return midi_path1, piano_roll_path1, audio_path1, midi_path2, piano_roll_path2, audio_path2, midi_path3, piano_roll_path3, audio_path3
97
+
98
+ def create_midi(self, chords, file_name):
99
+ mid = MidiFile()
100
+ track = MidiTrack()
101
+ mid.tracks.append(track)
102
+ delta_time = 480 * 4
103
+ for i_chord, chord in enumerate(chords):
104
+ for i, note in enumerate(chord):
105
+ if i == 0 and i_chord != 0:
106
+ track.append(Message('note_on', note=note.pitch.midi, velocity=64, time=1))
107
+ else:
108
+ track.append(Message('note_on', note=note.pitch.midi, velocity=64, time=0))
109
+ for i, note in enumerate(chord):
110
+ if i==0:
111
+ track.append(Message('note_off', note=note.pitch.midi, velocity=0, time=delta_time - 1))
112
+ else:
113
+ track.append(Message('note_off', note=note.pitch.midi, velocity=0, time=0))
114
+ midi_path = os.path.join(BASE_DIR, file_name)
115
+ mid.save(midi_path)
116
+ return midi_path
117
+
118
+ def convert_midi_to_audio(self, midi_path, file_name):
119
+ if not shutil.which("fluidsynth"):
120
+ try:
121
+ subprocess.run(["apt-get", "update"], check=True)
122
+ subprocess.run(["apt-get", "install", "-y", "fluidsynth"], check=True)
123
+ except Exception as e:
124
+ return f"Error installing Fluidsynth: {str(e)}"
125
+ wav_path = os.path.join(BASE_DIR, file_name + ".wav")
126
+ mp3_path = os.path.join(BASE_DIR, file_name + ".mp3")
127
+ soundfont_path = os.path.join(BASE_DIR, "soundfont.sf2")
128
+ if not os.path.exists(soundfont_path):
129
+ return "Error: SoundFont file not found. Please provide a valid .sf2 file."
130
+ try:
131
+ subprocess.run(["fluidsynth", "-ni", soundfont_path, midi_path, "-F", wav_path, "-r", "44100"], check=True)
132
+ AudioSegment.converter = "ffmpeg"
133
+ audio = AudioSegment.from_wav(wav_path)
134
+ audio.export(mp3_path, format="mp3")
135
+ return mp3_path
136
+ except Exception as e:
137
+ return f"Error converting MIDI to audio: {str(e)}"
138
+
139
+ def midi_to_svg(self, midi_file, output_file_name):
140
+ return self._hexachord.midi_to_svg(midi_file, output_file_name)
141
+
142
+ def generate_piano_roll(self, chords, label):
143
+ fig, ax = plt.subplots(figsize=(8, 4))
144
+
145
+ for i, chord in enumerate(chords):
146
+ for note in chord:
147
+ ax.broken_barh([(i * 1, 0.8)], (note.pitch.midi - 0.4, 0.8), facecolors='blue')
148
+
149
+ ax.set_xlabel("Chord Progression")
150
+ ax.set_ylabel("MIDI Note Number")
151
+ min_min_chords = 128
152
+ max_max_chords = 0
153
+ for ch in chords:
154
+ for note in ch:
155
+ if note.pitch.midi < min_min_chords:
156
+ min_min_chords = note.pitch.midi
157
+ if note.pitch.midi > max_max_chords:
158
+ max_max_chords = note.pitch.midi
159
+ ax.set_yticks(range(min_min_chords, max_max_chords + 1, 2))
160
+ ax.set_xticks(range(len(chords)))
161
+ ax.set_xticklabels([f"Chord {i + 1}" for i in range(len(chords))])
162
+
163
+ plt.grid(True, linestyle='--', alpha=0.5)
164
+ plt.savefig(label)
165
+ return label
166
+
167
+ def launch_score_editor(self, midi_path):
168
+ try:
169
+ score = converter.parse(midi_path)
170
+ score.show('musicxml')
171
+ return "Opened MIDI file in the default score editor!"
172
+ except Exception as e:
173
+ return f"Error opening score editor: {str(e)}"
174
+
175
+ def process_hexachord(self, hexachord_str, itvl):
176
+ try:
177
+ notes = [note.Note(n) for n in hexachord_str.split()]
178
+ if len(notes) != 6 or len(set(notes)) != 6:
179
+ return "Please enter exactly 6 unique MIDI note numbers."
180
+ except ValueError:
181
+ return "Invalid input. Enter 6 MIDI note numbers separated by spaces."
182
+ self._hexachord = notes
183
+ chords = self.generate_chords(notes, itvl)
184
+ midi_path = self.create_midi(chords, "base_chords.mid")
185
+ # piano_roll_path = self.generate_piano_roll(chords, "base_chords.png")
186
+ score_path = self.midi_to_svg (midi_path, "score_base_chords.svg")
187
+ audio_path = self.convert_midi_to_audio(midi_path, "base_chords")
188
+
189
+ return midi_path, score_path, audio_path
190
+
191
+ def render(self):
192
+ with gr.Blocks() as ui:
193
+ gr.Markdown("# Hexachord-based Chord Generator")
194
+
195
+ with gr.Tabs():
196
+ with gr.TabItem("Hexachord Generator"):
197
+ with gr.Row():
198
+ hexachord_input = gr.Textbox(
199
+ label="Enter 6 notes (pitchclass plus octave, separated by spaces)",
200
+ value="C3 D3 E3 G3 A3 B3",
201
+ interactive = True
202
+ )
203
+ interval_switch = gr.Radio(
204
+ choices=["fourth", "fifth"],
205
+ label="Select Interval",
206
+ value="fifth"
207
+ )
208
+ generate_button = gr.Button("Generate Chords")
209
+ midi_output = gr.File(label="Download MIDI File")
210
+ # piano_roll_output = gr.Image(label="Piano Roll Visualization")
211
+ score_output = gr.Image(label="Score Visualization")
212
+ audio_output = gr.Audio(label="Play Generated Chords", value=None, interactive=False)
213
+
214
+ generate_button.click(
215
+ fn=self.process_hexachord,
216
+ inputs=[hexachord_input, interval_switch],
217
+ # outputs=[midi_output, piano_roll_output, audio_output]
218
+ outputs = [midi_output, score_output, audio_output]
219
+ )
220
+ # Pressing Enter in the textbox also triggers processing
221
+ hexachord_input.submit(
222
+ fn=self.process_hexachord,
223
+ inputs=[hexachord_input, interval_switch],
224
+ outputs=[midi_output, score_output, audio_output]
225
+ )
226
+
227
+ with gr.TabItem("Alternative Chord Realizations"):
228
+ gr.Markdown("Alternative Chord Realizations")
229
+
230
+ realization_button = gr.Button("Generate Alternative Realizations")
231
+ realization_outputs = []
232
+
233
+ for i in range(3): # Three alternative realizations
234
+ with gr.Group():
235
+ gr.Markdown(f"#### Realization {i + 1}")
236
+ midi_output = gr.File(label="Download MIDI File")
237
+ piano_roll = gr.Image(label=f"Piano Roll {i + 1}")
238
+ audio_player = gr.Audio(label=f"Play Chords {i + 1}", interactive=False)
239
+ realization_outputs.append((midi_output, piano_roll, audio_player))
240
+
241
+ # Clicking the button triggers realization generation
242
+ realization_button.click(
243
+ fn=self.generate_realizations,
244
+ inputs=[],
245
+ outputs=[output for trip in realization_outputs for output in trip]
246
+ )
247
+
248
+ with gr.TabItem("Settings"):
249
+ gr.Markdown("### Configuration Options")
250
+ setting_1 = gr.Checkbox(label="Enable Advanced Mode")
251
+ setting_2 = gr.Slider(0, 100, label="Complexity Level")
252
+ self.ui = ui
253
+
254
+
255
+ def launch_app():
256
+ hex = HexachordApp()
257
+ hex.is_fsynth_installed()
258
+ hex.render()
259
+ if hex.on_huggingface:
260
+ hex.ui.launch(server_name="0.0.0.0", server_port=7860)
261
+ else:
262
+ hex.ui.launch()
263
+
264
+
265
+ launch_app()
working_version.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ import gradio as gr
3
+ from matplotlib import pyplot as plt
4
+ from mido import Message, MidiFile, MidiTrack
5
+ import os
6
+ import subprocess
7
+ from music21 import converter, note
8
+ from pydub import AudioSegment
9
+ import hexachords
10
+
11
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
12
+
13
+ class HexachordApp:
14
+
15
+ def __init__(self):
16
+ self._hexachord = None
17
+ self._hexachord_base_sequence = None
18
+ self.ui = None
19
+ self.on_huggingface = "HUGGINGFACE_SPACE" in os.environ
20
+
21
+ def is_fsynth_installed(self):
22
+ """ Check to make sure fluidsynth exists in the PATH """
23
+ for path in os.environ['PATH'].split(os.pathsep):
24
+ f = os.path.join(path, 'fluidsynth')
25
+ if os.path.exists(f) and os.access(f, os.X_OK):
26
+ print('fluidsynth is installed')
27
+ return True
28
+ print('fluidsynth is NOT installed')
29
+ return False
30
+
31
+ def generate_chords(self, note_names, itvl):
32
+ # Placeholder for your actual chord generation function
33
+ # Assuming hexachord is a list of MIDI note numbers
34
+ self._hexachord = hexachords.Hexachord()
35
+ interval_21 = 'P5'
36
+ if itvl == 'fourth':
37
+ interval_21 = 'P4'
38
+ self._hexachord_base_sequence = self._hexachord.generate_chord_sequence(note_names, intrvl=interval_21)
39
+ return self._hexachord_base_sequence
40
+
41
+ def generate_realizations(self):
42
+ # returns 3 triples of midipath, piano roll, audio player
43
+ reals = self._hexachord.generate_3_chords_realizations(self._hexachord_base_sequence)
44
+ midi_path1 = self.create_midi(reals[0], "real1.mid")
45
+ piano_roll_path1 = self.generate_piano_roll(reals[0], "real1.png")
46
+ audio_path1 = self.convert_midi_to_audio(midi_path1, "real1")
47
+ midi_path2 = self.create_midi(reals[1], "real2.mid")
48
+ piano_roll_path2 = self.generate_piano_roll(reals[1], "real2.png")
49
+ audio_path2 = self.convert_midi_to_audio(midi_path2, "real2")
50
+ midi_path3 = self.create_midi(reals[2], "real3.mid")
51
+ piano_roll_path3 = self.generate_piano_roll(reals[2], "real3.png")
52
+ audio_path3 = self.convert_midi_to_audio(midi_path3, "real3")
53
+ return midi_path1, piano_roll_path1, audio_path1, midi_path2, piano_roll_path2, audio_path2, midi_path3, piano_roll_path3, audio_path3
54
+
55
+ def create_midi(self, chords, file_name):
56
+ mid = MidiFile()
57
+ track = MidiTrack()
58
+ mid.tracks.append(track)
59
+ delta_time = 480 * 4
60
+ for i_chord, chord in enumerate(chords):
61
+ for i, note in enumerate(chord):
62
+ if i == 0 and i_chord != 0:
63
+ track.append(Message('note_on', note=note.pitch.midi, velocity=64, time=1))
64
+ else:
65
+ track.append(Message('note_on', note=note.pitch.midi, velocity=64, time=0))
66
+ for i, note in enumerate(chord):
67
+ if i==0:
68
+ track.append(Message('note_off', note=note.pitch.midi, velocity=0, time=delta_time - 1))
69
+ else:
70
+ track.append(Message('note_off', note=note.pitch.midi, velocity=0, time=0))
71
+ midi_path = os.path.join(BASE_DIR, file_name)
72
+ mid.save(midi_path)
73
+ return midi_path
74
+
75
+
76
+ def convert_midi_to_audio(self, midi_path, file_name):
77
+ if not shutil.which("fluidsynth"):
78
+ try:
79
+ subprocess.run(["apt-get", "update"], check=True)
80
+ subprocess.run(["apt-get", "install", "-y", "fluidsynth"], check=True)
81
+ except Exception as e:
82
+ return f"Error installing Fluidsynth: {str(e)}"
83
+ wav_path = os.path.join(BASE_DIR, file_name + ".wav")
84
+ mp3_path = os.path.join(BASE_DIR, file_name + ".mp3")
85
+ soundfont_path = os.path.join(BASE_DIR, "soundfont.sf2")
86
+ if not os.path.exists(soundfont_path):
87
+ return "Error: SoundFont file not found. Please provide a valid .sf2 file."
88
+ try:
89
+ subprocess.run(["fluidsynth", "-ni", soundfont_path, midi_path, "-F", wav_path, "-r", "44100"], check=True)
90
+ AudioSegment.converter = "ffmpeg"
91
+ audio = AudioSegment.from_wav(wav_path)
92
+ audio.export(mp3_path, format="mp3")
93
+ return mp3_path
94
+ except Exception as e:
95
+ return f"Error converting MIDI to audio: {str(e)}"
96
+
97
+ def generate_png(self, midi_file, output_file_name):
98
+ self._hexachord.midi_to_png(midi_file, output_file_name)
99
+
100
+ def generate_piano_roll(self, chords, label):
101
+ fig, ax = plt.subplots(figsize=(8, 4))
102
+
103
+ for i, chord in enumerate(chords):
104
+ for note in chord:
105
+ ax.broken_barh([(i * 1, 0.8)], (note.pitch.midi - 0.4, 0.8), facecolors='blue')
106
+
107
+ ax.set_xlabel("Chord Progression")
108
+ ax.set_ylabel("MIDI Note Number")
109
+ min_min_chords = 128
110
+ max_max_chords = 0
111
+ for ch in chords:
112
+ for note in ch:
113
+ if note.pitch.midi < min_min_chords:
114
+ min_min_chords = note.pitch.midi
115
+ if note.pitch.midi > max_max_chords:
116
+ max_max_chords = note.pitch.midi
117
+ ax.set_yticks(range(min_min_chords, max_max_chords + 1, 2))
118
+ ax.set_xticks(range(len(chords)))
119
+ ax.set_xticklabels([f"Chord {i + 1}" for i in range(len(chords))])
120
+
121
+ plt.grid(True, linestyle='--', alpha=0.5)
122
+ plt.savefig(label)
123
+ return label
124
+
125
+ def launch_score_editor(self, midi_path):
126
+ try:
127
+ score = converter.parse(midi_path)
128
+ score.show('musicxml')
129
+ return "Opened MIDI file in the default score editor!"
130
+ except Exception as e:
131
+ return f"Error opening score editor: {str(e)}"
132
+
133
+ def process_hexachord(self, hexachord_str, itvl):
134
+ try:
135
+ notes = [note.Note(n) for n in hexachord_str.split()]
136
+ if len(notes) != 6 or len(set(notes)) != 6:
137
+ return "Please enter exactly 6 unique MIDI note numbers."
138
+ except ValueError:
139
+ return "Invalid input. Enter 6 MIDI note numbers separated by spaces."
140
+ self._hexachord = notes
141
+ chords = self.generate_chords(notes, itvl)
142
+ midi_path = self.create_midi(chords, "base_chords.mid")
143
+ # piano_roll_path = self.generate_piano_roll(chords, "base_chords.png")
144
+ score_path = self.generate_png (midi_path, "score_base_chords.png")
145
+ audio_path = self.convert_midi_to_audio(midi_path, "base_chords")
146
+
147
+ return midi_path, score_path, audio_path
148
+
149
+ def render(self):
150
+ with gr.Blocks() as ui:
151
+ gr.Markdown("# Hexachord-based Chord Generator")
152
+
153
+ with gr.Tabs():
154
+ with gr.TabItem("Hexachord Generator"):
155
+ with gr.Row():
156
+ hexachord_input = gr.Textbox(
157
+ label="Enter 6 notes (pitchclass plus octave, separated by spaces)",
158
+ value="C3 D3 E3 G3 A3 B3",
159
+ interactive = True
160
+ )
161
+ interval_switch = gr.Radio(
162
+ choices=["fourth", "fifth"],
163
+ label="Select Interval",
164
+ value="fifth"
165
+ )
166
+ generate_button = gr.Button("Generate Chords")
167
+ midi_output = gr.File(label="Download MIDI File")
168
+ # piano_roll_output = gr.Image(label="Piano Roll Visualization")
169
+ score_output = gr.Image(label="Score Visualization")
170
+ audio_output = gr.Audio(label="Play Generated Chords", value=None, interactive=False)
171
+
172
+ generate_button.click(
173
+ fn=self.process_hexachord,
174
+ inputs=[hexachord_input, interval_switch],
175
+ # outputs=[midi_output, piano_roll_output, audio_output]
176
+ outputs = [midi_output, score_output, audio_output]
177
+ )
178
+ # Pressing Enter in the textbox also triggers processing
179
+ hexachord_input.submit(
180
+ fn=self.process_hexachord,
181
+ inputs=[hexachord_input, interval_switch],
182
+ outputs=[midi_output, score_output, audio_output]
183
+ )
184
+
185
+ with gr.TabItem("Alternative Chord Realizations"):
186
+ gr.Markdown("Alternative Chord Realizations")
187
+
188
+ realization_button = gr.Button("Generate Alternative Realizations")
189
+ realization_outputs = []
190
+
191
+ for i in range(3): # Three alternative realizations
192
+ with gr.Group():
193
+ gr.Markdown(f"#### Realization {i + 1}")
194
+ midi_output = gr.File(label="Download MIDI File")
195
+ piano_roll = gr.Image(label=f"Piano Roll {i + 1}")
196
+ audio_player = gr.Audio(label=f"Play Chords {i + 1}", interactive=False)
197
+ realization_outputs.append((midi_output, piano_roll, audio_player))
198
+
199
+ # Clicking the button triggers realization generation
200
+ realization_button.click(
201
+ fn=self.generate_realizations,
202
+ inputs=[],
203
+ outputs=[output for trip in realization_outputs for output in trip]
204
+ )
205
+
206
+ with gr.TabItem("Settings"):
207
+ gr.Markdown("### Configuration Options")
208
+ setting_1 = gr.Checkbox(label="Enable Advanced Mode")
209
+ setting_2 = gr.Slider(0, 100, label="Complexity Level")
210
+ self.ui = ui
211
+
212
+
213
+ def launch_app():
214
+ hex = HexachordApp()
215
+ hex.is_fsynth_installed()
216
+ hex.render()
217
+ if hex.on_huggingface:
218
+ hex.ui.launch(server_name="0.0.0.0", server_port=7860)
219
+ else:
220
+ hex.ui.launch()
221
+
222
+
223
+ launch_app()