Spaces:
Sleeping
Sleeping
| import ast | |
| import shutil | |
| import gradio as gr | |
| import numpy as np | |
| from matplotlib import pyplot as plt | |
| from mido import Message, MidiFile, MidiTrack | |
| import os | |
| import subprocess | |
| from music21 import converter, note, interval | |
| from pydub import AudioSegment | |
| import hexachords | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| class HexachordApp: | |
| def __init__(self): | |
| self._hexachord = hexachords.Hexachord() | |
| 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 | |
| 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 generate_png(self, midi_file, output_file_name): | |
| self._hexachord.midi_to_png(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." | |
| 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.generate_png (midi_path, "score_base_chords.png") | |
| audio_path = self.convert_midi_to_audio(midi_path, "base_chords") | |
| return midi_path, piano_roll_path, audio_path | |
| 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_selector = gr.Dropdown(label="Select Known Hexachord", | |
| choices=self.get_known_hexachords_choice(), value=None, interactive=True) | |
| 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) | |
| hexachord_selector.change( | |
| fn=self.get_selected_hexachord, | |
| inputs=[hexachord_selector], | |
| outputs=[hexachord_input] | |
| ) | |
| # generate_button.click( | |
| # fn=self.process_hexachord, | |
| # inputs=[hexachord_selector, interval_switch], | |
| # outputs=[midi_output, score_output, audio_output] | |
| # ) | |
| 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 get_known_hexachords_choice(self): | |
| return self._hexachord.known_hexachords | |
| def get_selected_hexachord(self, x): | |
| # lambda x: {"Hexachord 1": "C3 D3 E3 G3 A3 B3", "Hexachord 2": "D3 E3 F3 A3 B3 C4", | |
| # "Hexachord 3": "E3 G3 A3 C4 D4 F4"}.get(x, "") | |
| item = x[x.index('['):x.index(']')+1] | |
| int_array = np.array(ast.literal_eval(item)) | |
| hexa_string = '' | |
| start_note = note.Note('C3') | |
| for i in int_array: | |
| add_note = start_note.transpose(int(i)) | |
| hexa_string = hexa_string + ' ' + add_note.nameWithOctave | |
| return hexa_string | |
| 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() | |