pachet commited on
Commit
fc99099
·
1 Parent(s): d654f15

Update app.py, format_conversions.py, and hexachords.py

Browse files
Files changed (3) hide show
  1. app.py +41 -70
  2. format_conversions.py +28 -0
  3. hexachords.py +9 -7
app.py CHANGED
@@ -1,16 +1,13 @@
1
  import ast
2
- import shutil
3
  import gradio as gr
4
  import numpy as np
5
- from matplotlib import pyplot as plt
6
  from mido import Message, MidiFile, MidiTrack
7
  import os
8
- import subprocess
9
  from music21 import converter, note
10
- from pydub import AudioSegment
11
  import hexachords
12
  from format_conversions import Format_Converter
13
  import verovio
 
14
 
15
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
16
 
@@ -44,7 +41,6 @@ class HexachordApp:
44
 
45
  def __init__(self):
46
  self._hexachord = hexachords.Hexachord()
47
- self._hexachord_base_sequence = None
48
  self.ui = None
49
  self.on_huggingface = "HUGGINGFACE_SPACE" in os.environ
50
 
@@ -63,24 +59,21 @@ class HexachordApp:
63
  interval_21 = 'P5'
64
  if itvl == 'fourth':
65
  interval_21 = 'P4'
66
- self._hexachord_base_sequence = self._hexachord.generate_chord_sequence(note_names, intrvl=interval_21)
67
- return self._hexachord_base_sequence
68
 
69
  def generate_realizations(self):
70
- # returns 3 triples of midipath, piano roll, audio player
71
- reals = self._hexachord.generate_3_chords_realizations(self._hexachord_base_sequence)
 
72
  midi_path1 = self.create_midi(reals[0], "real1.mid")
73
- # piano_roll_path1 = self.generate_piano_roll(reals[0], "real1.png")
74
- score_path1 = self.generate_svg(midi_path1, "real1.svg")
75
- audio_path1 = self.convert_midi_to_audio(midi_path1, "real1")
76
  midi_path2 = self.create_midi(reals[1], "real2.mid")
77
- # piano_roll_path2 = self.generate_piano_roll(reals[1], "real2.png")
78
- score_path2 = self.generate_svg(midi_path2, "real2.svg")
79
- audio_path2 = self.convert_midi_to_audio(midi_path2, "real2")
80
  midi_path3 = self.create_midi(reals[2], "real3.mid")
81
- # piano_roll_path3 = self.generate_piano_roll(reals[2], "real3.png")
82
- score_path3 = self.generate_svg(midi_path3, "real3.svg")
83
- audio_path3 = self.convert_midi_to_audio(midi_path3, "real3")
84
  return midi_path1, score_path1, audio_path1, midi_path2, score_path2, audio_path2, midi_path3, score_path3, audio_path3
85
 
86
  def create_midi(self, chords, file_name):
@@ -105,56 +98,9 @@ class HexachordApp:
105
  mid.save(midi_path)
106
  return midi_path
107
 
108
-
109
- def convert_midi_to_audio(self, midi_path, file_name):
110
- if not shutil.which("fluidsynth"):
111
- try:
112
- subprocess.run(["apt-get", "update"], check=True)
113
- subprocess.run(["apt-get", "install", "-y", "fluidsynth"], check=True)
114
- except Exception as e:
115
- return f"Error installing Fluidsynth: {str(e)}"
116
- wav_path = os.path.join(BASE_DIR, file_name + ".wav")
117
- mp3_path = os.path.join(BASE_DIR, file_name + ".mp3")
118
- soundfont_path = os.path.join(BASE_DIR, "soundfont.sf2")
119
- if not os.path.exists(soundfont_path):
120
- return "Error: SoundFont file not found. Please provide a valid .sf2 file."
121
- try:
122
- subprocess.run(["fluidsynth", "-ni", soundfont_path, midi_path, "-F", wav_path, "-r", "44100"], check=True)
123
- AudioSegment.converter = "ffmpeg"
124
- audio = AudioSegment.from_wav(wav_path)
125
- audio.export(mp3_path, format="mp3")
126
- return mp3_path
127
- except Exception as e:
128
- return f"Error converting MIDI to audio: {str(e)}"
129
-
130
  def generate_svg(self, midi_file, output_file_name):
131
  return Format_Converter().midi_to_svg_file(tk, midi_file, output_file_name)
132
 
133
- def generate_piano_roll(self, chords, label):
134
- fig, ax = plt.subplots(figsize=(8, 4))
135
-
136
- for i, chord in enumerate(chords):
137
- for note in chord:
138
- ax.broken_barh([(i * 1, 0.8)], (note.pitch.midi - 0.4, 0.8), facecolors='blue')
139
-
140
- ax.set_xlabel("Chord Progression")
141
- ax.set_ylabel("MIDI Note Number")
142
- min_min_chords = 128
143
- max_max_chords = 0
144
- for ch in chords:
145
- for note in ch:
146
- if note.pitch.midi < min_min_chords:
147
- min_min_chords = note.pitch.midi
148
- if note.pitch.midi > max_max_chords:
149
- max_max_chords = note.pitch.midi
150
- ax.set_yticks(range(min_min_chords, max_max_chords + 1, 2))
151
- ax.set_xticks(range(len(chords)))
152
- ax.set_xticklabels([f"Chord {i + 1}" for i in range(len(chords))])
153
-
154
- plt.grid(True, linestyle='--', alpha=0.5)
155
- plt.savefig(label)
156
- return label
157
-
158
  def launch_score_editor(self, midi_path):
159
  try:
160
  score = converter.parse(midi_path)
@@ -179,19 +125,30 @@ class HexachordApp:
179
 
180
  def process_hexachord(self, hexachord_str, itvl):
181
  try:
182
- # notes = [note.Note(n) for n in hexachord_str.split()]
183
  notes = self.build_octave_dependent_notes_from_string(hexachord_str)
184
  if len(notes) != 6 or len(set(notes)) != 6:
185
  return "Please enter exactly 6 unique notes."
186
  except ValueError:
187
  return "Invalid input. Enter 6 notes separated by spaces."
 
188
  chords = self.generate_chords(notes, itvl)
189
  midi_path = self.create_midi(chords, "base_chords.mid")
190
- score_path = self.generate_svg (midi_path, "score_base_chords.svg")
191
- audio_path = self.convert_midi_to_audio(midi_path, "base_chords")
192
- # return midi_path, piano_roll_path, audio_path
193
  return (midi_path, score_path, audio_path) + self.generate_realizations()
194
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  def render(self):
196
  with gr.Blocks() as ui:
197
  gr.Markdown("# Hexachord-based Chord Generator")
@@ -245,7 +202,21 @@ class HexachordApp:
245
 
246
  with gr.TabItem("Movements"):
247
  gr.Markdown("Movements")
248
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
  with gr.TabItem("Settings"):
251
  gr.Markdown("### Configuration Options")
 
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
 
7
  import hexachords
8
  from format_conversions import Format_Converter
9
  import verovio
10
+ import subprocess
11
 
12
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
13
 
 
41
 
42
  def __init__(self):
43
  self._hexachord = hexachords.Hexachord()
 
44
  self.ui = None
45
  self.on_huggingface = "HUGGINGFACE_SPACE" in os.environ
46
 
 
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
+ fm = Format_Converter()
67
+ reals = self._hexachord.generate_3_chords_realizations(self._hexachord._base_sequence)
68
  midi_path1 = self.create_midi(reals[0], "real1.mid")
69
+ score_path1 = fm.midi_to_svg_file(tk, midi_path1, "real1.svg")
70
+ audio_path1 = fm.convert_midi_to_audio(midi_path1, "real1")
 
71
  midi_path2 = self.create_midi(reals[1], "real2.mid")
72
+ score_path2 = fm.midi_to_svg_file(tk, midi_path2, "real2.svg")
73
+ audio_path2 = fm.convert_midi_to_audio(midi_path2, "real2")
 
74
  midi_path3 = self.create_midi(reals[2], "real3.mid")
75
+ score_path3 = fm.midi_to_svg_file(tk, midi_path3, "real3.svg")
76
+ audio_path3 = fm.convert_midi_to_audio(midi_path3, "real3")
 
77
  return midi_path1, score_path1, audio_path1, midi_path2, score_path2, audio_path2, midi_path3, score_path3, audio_path3
78
 
79
  def create_midi(self, chords, file_name):
 
98
  mid.save(midi_path)
99
  return midi_path
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  def generate_svg(self, midi_file, output_file_name):
102
  return Format_Converter().midi_to_svg_file(tk, midi_file, output_file_name)
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  def launch_score_editor(self, midi_path):
105
  try:
106
  score = converter.parse(midi_path)
 
125
 
126
  def process_hexachord(self, hexachord_str, itvl):
127
  try:
 
128
  notes = self.build_octave_dependent_notes_from_string(hexachord_str)
129
  if len(notes) != 6 or len(set(notes)) != 6:
130
  return "Please enter exactly 6 unique notes."
131
  except ValueError:
132
  return "Invalid input. Enter 6 notes separated by spaces."
133
+ fm = Format_Converter()
134
  chords = self.generate_chords(notes, itvl)
135
  midi_path = self.create_midi(chords, "base_chords.mid")
136
+ score_path = fm.midi_to_svg_file(tk, midi_path, "score_base_chords.svg")
137
+ audio_path = fm.convert_midi_to_audio(midi_path, "base_chords")
 
138
  return (midi_path, score_path, audio_path) + self.generate_realizations()
139
 
140
+ def generate_movements(self):
141
+ # take 2 realizations of the same root
142
+ everyone = ()
143
+ for index_of_chord in range(6):
144
+ chords = [real[index_of_chord] for real in self._hexachord._realizations]
145
+ fm = Format_Converter()
146
+ midi_path = self.create_midi(chords, "movement1.mid")
147
+ score_path = fm.midi_to_svg_file(tk, midi_path, "movement1.svg")
148
+ audio_path = fm.convert_midi_to_audio(midi_path, "movement1")
149
+ everyone = everyone + (midi_path, score_path, audio_path)
150
+ return everyone
151
+
152
  def render(self):
153
  with gr.Blocks() as ui:
154
  gr.Markdown("# Hexachord-based Chord Generator")
 
202
 
203
  with gr.TabItem("Movements"):
204
  gr.Markdown("Movements")
205
+ with gr.Row():
206
+ generate_mvmt_button = gr.Button("Generate Movements")
207
+ realization_outputs = []
208
+ for i in range(6): # Three alternative realizations
209
+ with gr.Row():
210
+ gr.Markdown(f"#### Movement {i + 1}")
211
+ midi_output = gr.File(label="Download MIDI File", scale=1)
212
+ piano_roll = gr.Image(label=f"Piano Roll {i + 1}", scale=3)
213
+ audio_player = gr.Audio(label=f"Play Chords {i + 1}", interactive=False, scale=3)
214
+ realization_outputs += (midi_output, piano_roll, audio_player)
215
+ generate_mvmt_button.click(
216
+ fn=self.generate_movements,
217
+ inputs=[],
218
+ outputs = realization_outputs
219
+ )
220
 
221
  with gr.TabItem("Settings"):
222
  gr.Markdown("### Configuration Options")
format_conversions.py CHANGED
@@ -1,6 +1,12 @@
1
  import copy
 
2
  from music21 import converter, musicxml, stream, note, chord, clef
3
  import verovio
 
 
 
 
 
4
 
5
  class Format_Converter:
6
 
@@ -142,5 +148,27 @@ class Format_Converter:
142
  # return f"<p style='color:red'>Error: {e}</p>"
143
  return output_file
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  if __name__ == '__main__':
146
  svg_string = Format_Converter().midi_to_svg(verovio.toolkit(), 'output.mid')
 
1
  import copy
2
+ import os
3
  from music21 import converter, musicxml, stream, note, chord, clef
4
  import verovio
5
+ import shutil
6
+ import subprocess
7
+ from pydub import AudioSegment
8
+
9
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
10
 
11
  class Format_Converter:
12
 
 
148
  # return f"<p style='color:red'>Error: {e}</p>"
149
  return output_file
150
 
151
+ def convert_midi_to_audio(self, midi_path, file_name):
152
+ if not shutil.which("fluidsynth"):
153
+ try:
154
+ subprocess.run(["apt-get", "update"], check=True)
155
+ subprocess.run(["apt-get", "install", "-y", "fluidsynth"], check=True)
156
+ except Exception as e:
157
+ return f"Error installing Fluidsynth: {str(e)}"
158
+ wav_path = os.path.join(BASE_DIR, file_name + ".wav")
159
+ mp3_path = os.path.join(BASE_DIR, file_name + ".mp3")
160
+ soundfont_path = os.path.join(BASE_DIR, "soundfont.sf2")
161
+ if not os.path.exists(soundfont_path):
162
+ return "Error: SoundFont file not found. Please provide a valid .sf2 file."
163
+ try:
164
+ subprocess.run(["fluidsynth", "-ni", soundfont_path, midi_path, "-F", wav_path, "-r", "44100"], check=True)
165
+ AudioSegment.converter = "ffmpeg"
166
+ audio = AudioSegment.from_wav(wav_path)
167
+ audio.export(mp3_path, format="mp3")
168
+ return mp3_path
169
+ except Exception as e:
170
+ return f"Error converting MIDI to audio: {str(e)}"
171
+
172
+
173
  if __name__ == '__main__':
174
  svg_string = Format_Converter().midi_to_svg(verovio.toolkit(), 'output.mid')
hexachords.py CHANGED
@@ -1,7 +1,6 @@
1
- from music21 import note, stream, interval, meter, chord, converter, metadata
2
  from ortools.sat.python import cp_model
3
 
4
-
5
  class Hexachord:
6
  known_hexachords = [
7
  "6-1 [0,1,2,3,4,5] Chromatic hexachord",
@@ -24,12 +23,13 @@ class Hexachord:
24
  "6-Z44A [0,1,2,5,6,9] Schoenberg hexachord",
25
  "6-Z46A [0,1,2,4,6,9] Scale of harmonics",
26
  "6-Z47B [0,2,3,4,7,9] Blues scale"]
 
 
27
 
28
  def generate_chord_sequence_from_midi_pitches(self, list_of_mp, intrvl="P5"):
29
- return self.generate_chord_sequence([note.Note(mp).nameWithOctave for mp in list_of_mp], intrvl=intrvl)
30
-
31
- def generate_chord_sequence(self, six_notes, intrvl="P5"):
32
 
 
33
  fifth = interval.Interval(intrvl) # Perfect fifth
34
  all_pc = [n.pitch.pitchClass for n in six_notes]
35
  all_chords = []
@@ -43,6 +43,7 @@ class Hexachord:
43
  current_note = current_note.transpose(-12)
44
  ch.add(current_note)
45
  all_chords.append(ch)
 
46
  return all_chords
47
 
48
  def generate_3_chords_realizations(self, chord_seq):
@@ -78,7 +79,8 @@ class Hexachord:
78
  else:
79
  new_ch.add(n)
80
  res3.append(new_ch)
81
- return res1, res2, res3
 
82
 
83
  def chords_to_stream(self, chords, file_name):
84
  s = stream.Stream()
@@ -162,7 +164,7 @@ if __name__ == '__main__':
162
  hexa = Hexachord()
163
  note_names = ["C3", "Eb3", "E3", "F#3", "G3", "Bb3"]
164
  notes = [note.Note(n) for n in note_names]
165
- cs1 = hexa.generate_chord_sequence(notes, intrvl="P4")
166
  # cs1 = generate_chord_sequence(["E3", "G3", "Ab3", "B3", "C4", "Eb4"])
167
  # cs2 = generate_chord_sequence(["C3", "F3", "F#3", "A3", "B4", "E4"])
168
  # alternation = alternate_chords(cs1, cs2)
 
1
+ from music21 import note, stream, interval, meter, chord
2
  from ortools.sat.python import cp_model
3
 
 
4
  class Hexachord:
5
  known_hexachords = [
6
  "6-1 [0,1,2,3,4,5] Chromatic hexachord",
 
23
  "6-Z44A [0,1,2,5,6,9] Schoenberg hexachord",
24
  "6-Z46A [0,1,2,4,6,9] Scale of harmonics",
25
  "6-Z47B [0,2,3,4,7,9] Blues scale"]
26
+ _base_sequence = None
27
+ _realizations = []
28
 
29
  def generate_chord_sequence_from_midi_pitches(self, list_of_mp, intrvl="P5"):
30
+ return self.generate_base_sequence([note.Note(mp).nameWithOctave for mp in list_of_mp], intrvl=intrvl)
 
 
31
 
32
+ def generate_base_sequence(self, six_notes, intrvl="P5"):
33
  fifth = interval.Interval(intrvl) # Perfect fifth
34
  all_pc = [n.pitch.pitchClass for n in six_notes]
35
  all_chords = []
 
43
  current_note = current_note.transpose(-12)
44
  ch.add(current_note)
45
  all_chords.append(ch)
46
+ self._base_sequence = all_chords
47
  return all_chords
48
 
49
  def generate_3_chords_realizations(self, chord_seq):
 
79
  else:
80
  new_ch.add(n)
81
  res3.append(new_ch)
82
+ self._realizations = res1, res2, res3
83
+ return self._realizations
84
 
85
  def chords_to_stream(self, chords, file_name):
86
  s = stream.Stream()
 
164
  hexa = Hexachord()
165
  note_names = ["C3", "Eb3", "E3", "F#3", "G3", "Bb3"]
166
  notes = [note.Note(n) for n in note_names]
167
+ cs1 = hexa.generate_base_sequence(notes, intrvl="P4")
168
  # cs1 = generate_chord_sequence(["E3", "G3", "Ab3", "B3", "C4", "Eb4"])
169
  # cs2 = generate_chord_sequence(["C3", "F3", "F#3", "A3", "B4", "E4"])
170
  # alternation = alternate_chords(cs1, cs2)