hexachords_docker / format_conversions.py
pachet's picture
added smallmuse and movements
c2f178d
import copy
import os
from pathlib import Path
from music21 import converter, musicxml, stream, note, chord, clef
import verovio
import shutil
import subprocess
from pydub import AudioSegment
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
class Format_Converter:
def midi_to_musicxml_string_two_staves(midi_path, split_pitch=60):
score = converter.parse(midi_path)
merged = score.flatten()
treble = stream.Part()
bass = stream.Part()
treble.insert(0, clef.TrebleClef())
bass.insert(0, clef.BassClef())
for el in merged.recurse().notesAndRests:
true_offset = el.getOffsetInHierarchy(score)
if isinstance(el, note.Rest):
treble.insert(true_offset, copy.deepcopy(el))
bass.insert(true_offset, copy.deepcopy(el))
elif isinstance(el, note.Note):
target = treble if el.pitch.midi >= split_pitch else bass
target.insert(true_offset, copy.deepcopy(el))
elif isinstance(el, chord.Chord):
treble_pitches = [p for p in el.pitches if p.midi >= split_pitch]
bass_pitches = [p for p in el.pitches if p.midi < split_pitch]
if treble_pitches:
treble_chord = chord.Chord(treble_pitches)
treble_chord.quarterLength = el.quarterLength
treble_chord.volume = el.volume
treble.insert(true_offset, treble_chord)
if bass_pitches:
bass_chord = chord.Chord(bass_pitches)
bass_chord.quarterLength = el.quarterLength
bass_chord.volume = el.volume
bass.insert(true_offset, bass_chord)
final_score = stream.Score()
final_score.insert(0, treble) # order: top staff first
final_score.insert(0, bass)
exporter = musicxml.m21ToXml.GeneralObjectExporter(final_score)
musicxml_str = exporter.parse().decode('utf-8')
return musicxml_str
@classmethod
def midi_to_musicxml_string(cls, midi_path):
# Parse MIDI file into a music21 stream
score = converter.parse(midi_path)
# Export to MusicXML string
exporter = musicxml.m21ToXml.GeneralObjectExporter(score)
return exporter.parse().decode('utf-8') # parse() returns bytes
@classmethod
def xml_to_svg_file(cls, tk, musicxml, output_file):
try:
mei_str = cls.xml_to_mei_string(tk, musicxml)
# Load MEI and render SVG
tk.loadData(mei_str)
svg = tk.renderToSVGFile(output_file)
return output_file
except Exception as e:
print("error in midi_to_svg_file")
# return f"<p style='color:red'>Error: {e}</p>"
return output_file
@classmethod
def midi_to_svg_file(cls, tk, midi_path, output_file):
musicxml = cls.midi_to_musicxml_string_two_staves(midi_path)
return cls.xml_to_svg_file(tk, musicxml, output_file)
@classmethod
def xml_to_mei_string(cls, tk, xmlstring):
tk.loadData(xmlstring)
return tk.getMEI()
@classmethod
def midi_to_svg(cls, tk, midi_path):
try:
# Convert MIDI to MEI using music21
musicxmlobj = cls.midi_to_musicxml_string(midi_path)
mei_str = cls.xml_to_mei_string(tk, musicxmlobj)
# Load MEI and render SVG
tk.loadData(mei_str)
svg = tk.renderToSVG(1)
return svg
except Exception as e:
return f"<p style='color:red'>Error: {e}</p>"
@classmethod
def m21_to_xml(cls, m21):
# Export to MusicXML string
exporter = musicxml.m21ToXml.GeneralObjectExporter()
return exporter.parse(m21).decode('utf-8')
@classmethod
def m21_to_svg_file(cls, m21, output_file):
# Export to MusicXML string
exporter = musicxml.m21ToXml.GeneralObjectExporter()
musicxmlobj = exporter.parse(m21).decode('utf-8')
try:
tk = verovio.toolkit()
mei_str = cls.xml_to_mei_string(tk, musicxmlobj)
# Load MEI and render SVG
tk.loadData(mei_str)
svg = tk.renderToSVGFile(output_file)
return output_file
except Exception as e:
print("error in midi_to_svg_file")
# return f"<p style='color:red'>Error: {e}</p>"
return output_file
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 musescore_midi_to_svg(midi_path, svg_path, mscore_path="/Applications/MuseScore 4.app/Contents/MacOS/mscore"):
subprocess.run([mscore_path, midi_path, "-o", svg_path, "-T"], check=True)
def musescore_midi_to_musicxml(
midi_path: str,
output_path: str = None,
musescore_path: str = "/Applications/MuseScore 4.app/Contents/MacOS/mscore"
):
midi_file = Path(midi_path)
if not midi_file.exists():
raise FileNotFoundError(f"MIDI file not found: {midi_path}")
if output_path is None:
output_path = midi_file.with_suffix(".musicxml")
else:
output_path = Path(output_path)
command = [musescore_path, str(midi_file), "-o", str(output_path)]
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
print("✅ MuseScore conversion successful.")
print(f"➡️ Output: {output_path}")
except subprocess.CalledProcessError as e:
print("❌ MuseScore conversion failed.")
print("stderr:", e.stderr)
print("stdout:", e.stdout)
raise
if __name__ == '__main__':
svg_string = Format_Converter().midi_to_svg(verovio.toolkit(), 'output.mid')