Spaces:
Sleeping
Sleeping
| import shutil | |
| import gradio as gr | |
| from matplotlib import pyplot as plt | |
| from mido import Message, MidiFile, MidiTrack | |
| import os | |
| import subprocess | |
| from music21 import converter, note | |
| from pydub import AudioSegment | |
| import hexachords | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| os.environ["VEROVIO_FONT_PATH"] = "/usr/local/share/verovio/fonts" | |
| def install_verovio(): | |
| verovio_dir = "verovio" | |
| build_dir = os.path.join(verovio_dir, "build") | |
| cmake_source_dir = os.path.join(verovio_dir, "cmake") # The correct directory containing CMakeLists.txt | |
| verovio_executable = os.path.join(build_dir, "verovio") | |
| if not os.path.exists(verovio_executable): | |
| print("Installing Verovio...") | |
| try: | |
| # Clone the Verovio repository | |
| subprocess.run(["git", "clone", "--recursive", "https://github.com/rism-digital/verovio.git"], check=True) | |
| # Ensure build directory exists | |
| os.makedirs(build_dir, exist_ok=True) | |
| # Move into the build directory | |
| os.chdir(build_dir) | |
| # Run CMake using the correct source path (`../cmake`) | |
| subprocess.run(["cmake", "../cmake"], check=True) | |
| # Compile Verovio | |
| subprocess.run(["make", "-j4"], check=True) | |
| # Move back to the original directory | |
| os.chdir("../../") | |
| except subprocess.CalledProcessError as e: | |
| print(f"An error occurred during Verovio installation: {e}") | |
| os.chdir("../../") # Ensure returning to the original directory in case of error | |
| else: | |
| print("Verovio is already installed.") | |
| install_verovio() # Ensure Verovio is installed before running Gradio | |
| # Ensure Verovio has the Bravura font | |
| def install_bravura(): | |
| font_path = "/usr/local/share/verovio/fonts" | |
| os.makedirs(font_path, exist_ok=True) # Ensure the folder exists | |
| bravura_font = os.path.join(font_path, "Bravura.otf") | |
| if not os.path.exists(bravura_font): | |
| print("Downloading Bravura font...") | |
| os.system(f"curl -L -o {bravura_font} https://github.com/steinbergmedia/bravura/releases/download/v1.392/Bravura-1.392.otf") | |
| else: | |
| print("Bravura font already installed.") | |
| install_bravura() | |
| class HexachordApp: | |
| def __init__(self): | |
| self._hexachord = None | |
| self._hexachord_base_sequence = None | |
| self.ui = None | |
| self.on_huggingface = "HUGGINGFACE_SPACE" in os.environ | |
| def is_fsynth_installed(self): | |
| """ Check to make sure fluidsynth exists in the PATH """ | |
| for path in os.environ['PATH'].split(os.pathsep): | |
| f = os.path.join(path, 'fluidsynth') | |
| if os.path.exists(f) and os.access(f, os.X_OK): | |
| print('fluidsynth is installed') | |
| return True | |
| print('fluidsynth is NOT installed') | |
| return False | |
| def generate_chords(self, note_names, itvl): | |
| # Placeholder for your actual chord generation function | |
| # Assuming hexachord is a list of MIDI note numbers | |
| self._hexachord = hexachords.Hexachord() | |
| interval_21 = 'P5' | |
| if itvl == 'fourth': | |
| interval_21 = 'P4' | |
| self._hexachord_base_sequence = self._hexachord.generate_chord_sequence(note_names, intrvl=interval_21) | |
| return self._hexachord_base_sequence | |
| def generate_realizations(self): | |
| # returns 3 triples of midipath, piano roll, audio player | |
| reals = self._hexachord.generate_3_chords_realizations(self._hexachord_base_sequence) | |
| midi_path1 = self.create_midi(reals[0], "real1.mid") | |
| piano_roll_path1 = self.generate_piano_roll(reals[0], "real1.png") | |
| audio_path1 = self.convert_midi_to_audio(midi_path1, "real1") | |
| midi_path2 = self.create_midi(reals[1], "real2.mid") | |
| piano_roll_path2 = self.generate_piano_roll(reals[1], "real2.png") | |
| audio_path2 = self.convert_midi_to_audio(midi_path2, "real2") | |
| midi_path3 = self.create_midi(reals[2], "real3.mid") | |
| piano_roll_path3 = self.generate_piano_roll(reals[2], "real3.png") | |
| audio_path3 = self.convert_midi_to_audio(midi_path3, "real3") | |
| return midi_path1, piano_roll_path1, audio_path1, midi_path2, piano_roll_path2, audio_path2, midi_path3, piano_roll_path3, audio_path3 | |
| def create_midi(self, chords, file_name): | |
| mid = MidiFile() | |
| track = MidiTrack() | |
| mid.tracks.append(track) | |
| delta_time = 480 * 4 | |
| for i_chord, chord in enumerate(chords): | |
| for i, note in enumerate(chord): | |
| if i == 0 and i_chord != 0: | |
| track.append(Message('note_on', note=note.pitch.midi, velocity=64, time=1)) | |
| else: | |
| track.append(Message('note_on', note=note.pitch.midi, velocity=64, time=0)) | |
| for i, note in enumerate(chord): | |
| if i==0: | |
| track.append(Message('note_off', note=note.pitch.midi, velocity=0, time=delta_time - 1)) | |
| else: | |
| track.append(Message('note_off', note=note.pitch.midi, velocity=0, time=0)) | |
| midi_path = os.path.join(BASE_DIR, file_name) | |
| mid.save(midi_path) | |
| return midi_path | |
| def convert_midi_to_audio(self, midi_path, file_name): | |
| if not shutil.which("fluidsynth"): | |
| try: | |
| subprocess.run(["apt-get", "update"], check=True) | |
| subprocess.run(["apt-get", "install", "-y", "fluidsynth"], check=True) | |
| except Exception as e: | |
| return f"Error installing Fluidsynth: {str(e)}" | |
| wav_path = os.path.join(BASE_DIR, file_name + ".wav") | |
| mp3_path = os.path.join(BASE_DIR, file_name + ".mp3") | |
| soundfont_path = os.path.join(BASE_DIR, "soundfont.sf2") | |
| if not os.path.exists(soundfont_path): | |
| return "Error: SoundFont file not found. Please provide a valid .sf2 file." | |
| try: | |
| subprocess.run(["fluidsynth", "-ni", soundfont_path, midi_path, "-F", wav_path, "-r", "44100"], check=True) | |
| AudioSegment.converter = "ffmpeg" | |
| audio = AudioSegment.from_wav(wav_path) | |
| audio.export(mp3_path, format="mp3") | |
| return mp3_path | |
| except Exception as e: | |
| return f"Error converting MIDI to audio: {str(e)}" | |
| def midi_to_svg(self, midi_file, output_file_name): | |
| return self._hexachord.midi_to_svg(midi_file, output_file_name) | |
| def generate_piano_roll(self, chords, label): | |
| fig, ax = plt.subplots(figsize=(8, 4)) | |
| for i, chord in enumerate(chords): | |
| for note in chord: | |
| ax.broken_barh([(i * 1, 0.8)], (note.pitch.midi - 0.4, 0.8), facecolors='blue') | |
| ax.set_xlabel("Chord Progression") | |
| ax.set_ylabel("MIDI Note Number") | |
| min_min_chords = 128 | |
| max_max_chords = 0 | |
| for ch in chords: | |
| for note in ch: | |
| if note.pitch.midi < min_min_chords: | |
| min_min_chords = note.pitch.midi | |
| if note.pitch.midi > max_max_chords: | |
| max_max_chords = note.pitch.midi | |
| ax.set_yticks(range(min_min_chords, max_max_chords + 1, 2)) | |
| ax.set_xticks(range(len(chords))) | |
| ax.set_xticklabels([f"Chord {i + 1}" for i in range(len(chords))]) | |
| plt.grid(True, linestyle='--', alpha=0.5) | |
| plt.savefig(label) | |
| return label | |
| def launch_score_editor(self, midi_path): | |
| try: | |
| score = converter.parse(midi_path) | |
| score.show('musicxml') | |
| return "Opened MIDI file in the default score editor!" | |
| except Exception as e: | |
| return f"Error opening score editor: {str(e)}" | |
| def process_hexachord(self, hexachord_str, itvl): | |
| try: | |
| notes = [note.Note(n) for n in hexachord_str.split()] | |
| if len(notes) != 6 or len(set(notes)) != 6: | |
| return "Please enter exactly 6 unique MIDI note numbers." | |
| except ValueError: | |
| return "Invalid input. Enter 6 MIDI note numbers separated by spaces." | |
| self._hexachord = notes | |
| chords = self.generate_chords(notes, itvl) | |
| midi_path = self.create_midi(chords, "base_chords.mid") | |
| # piano_roll_path = self.generate_piano_roll(chords, "base_chords.png") | |
| score_path = self.midi_to_svg (midi_path, "score_base_chords.svg") | |
| audio_path = self.convert_midi_to_audio(midi_path, "base_chords") | |
| return midi_path, score_path, audio_path | |
| def midi_to_musicxml_string(self, midi_file): | |
| # midi_file_path = "example.mid" # Replace with your MIDI file | |
| # musicxml_string = midi_to_musicxml_string(midi_file_path) | |
| # print(musicxml_string) | |
| """Convert MIDI to MusicXML string using music21.""" | |
| score = converter.parse(midi_file) | |
| return score.write('musicxml') | |
| # Example usage | |
| def render(self): | |
| with gr.Blocks() as ui: | |
| gr.Markdown("# Hexachord-based Chord Generator") | |
| with gr.Tabs(): | |
| with gr.TabItem("Hexachord Generator"): | |
| with gr.Row(): | |
| hexachord_input = gr.Textbox( | |
| label="Enter 6 notes (pitchclass plus octave, separated by spaces)", | |
| value="C3 D3 E3 G3 A3 B3", | |
| interactive = True | |
| ) | |
| interval_switch = gr.Radio( | |
| choices=["fourth", "fifth"], | |
| label="Select Interval", | |
| value="fifth" | |
| ) | |
| generate_button = gr.Button("Generate Chords") | |
| midi_output = gr.File(label="Download MIDI File") | |
| # piano_roll_output = gr.Image(label="Piano Roll Visualization") | |
| score_output = gr.Image(label="Score Visualization") | |
| audio_output = gr.Audio(label="Play Generated Chords", value=None, interactive=False) | |
| generate_button.click( | |
| fn=self.process_hexachord, | |
| inputs=[hexachord_input, interval_switch], | |
| # outputs=[midi_output, piano_roll_output, audio_output] | |
| outputs = [midi_output, score_output, audio_output] | |
| ) | |
| # Pressing Enter in the textbox also triggers processing | |
| hexachord_input.submit( | |
| fn=self.process_hexachord, | |
| inputs=[hexachord_input, interval_switch], | |
| outputs=[midi_output, score_output, audio_output] | |
| ) | |
| with gr.TabItem("Alternative Chord Realizations"): | |
| gr.Markdown("Alternative Chord Realizations") | |
| realization_button = gr.Button("Generate Alternative Realizations") | |
| realization_outputs = [] | |
| for i in range(3): # Three alternative realizations | |
| with gr.Group(): | |
| gr.Markdown(f"#### Realization {i + 1}") | |
| midi_output = gr.File(label="Download MIDI File") | |
| piano_roll = gr.Image(label=f"Piano Roll {i + 1}") | |
| audio_player = gr.Audio(label=f"Play Chords {i + 1}", interactive=False) | |
| realization_outputs.append((midi_output, piano_roll, audio_player)) | |
| # Clicking the button triggers realization generation | |
| realization_button.click( | |
| fn=self.generate_realizations, | |
| inputs=[], | |
| outputs=[output for trip in realization_outputs for output in trip] | |
| ) | |
| with gr.TabItem("Settings"): | |
| gr.Markdown("### Configuration Options") | |
| setting_1 = gr.Checkbox(label="Enable Advanced Mode") | |
| setting_2 = gr.Slider(0, 100, label="Complexity Level") | |
| self.ui = ui | |
| def launch_app(): | |
| hex = HexachordApp() | |
| hex.is_fsynth_installed() | |
| hex.render() | |
| if hex.on_huggingface: | |
| hex.ui.launch(server_name="0.0.0.0", server_port=7860) | |
| else: | |
| hex.ui.launch() | |
| launch_app() | |