File size: 6,804 Bytes
d877082
fc99099
c2f178d
 
d877082
a3cb78c
fc99099
 
 
 
 
a3cb78c
 
 
d877082
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3cb78c
 
 
 
 
 
6e4f2b4
a3cb78c
c2f178d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3cb78c
 
 
 
 
 
 
 
 
6e4f2b4
 
a3cb78c
 
 
 
 
 
 
c2f178d
52d7d5e
6e4f2b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc99099
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2f178d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc99099
a3cb78c
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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')