diff --git a/Makefile b/Makefile index fc948373..0feba436 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,16 @@ -.PHONY: convert-json convert-cairo +.PHONY: convert-json convert-cairo convert-to-midi -# Default MIDI file and output file paths -MIDI_FILE ?= path/to/default/midi/file.mid +# Default input file path (can be a MIDI file or a structured format for cairo_to_midi) +INPUT_FILE ?= path/to/default/input/file +# Default output file path OUTPUT_FILE ?= path/to/default/output convert-json: - python3 python/cli.py $(MIDI_FILE) $(OUTPUT_FILE).json + python3 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).json --format json convert-cairo: - python3 python/cli.py $(MIDI_FILE) $(OUTPUT_FILE).cairo --format cairo + python3 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).cairo --format cairo + +convert-to-midi: + # Assuming cairo_to_midi can handle both Cairo and JSON structured inputs + python3 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).mid --format midi diff --git a/README.md b/README.md index c306c66d..1618f2ca 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,17 @@ Autonomous Music library based on previous [work](https://github.com/caseywescot # Midi Conversion Convert Midi to JSON format: ```bash -make convert-json MIDI_FILE="path/to/midi/file.mid" OUTPUT_FILE="path/to/output" +make convert-json INPUT_FILE="path/to/midi/file.mid" OUTPUT_FILE="path/to/output" ``` Convert to Cairo format: ```bash -make convert-cairo MIDI_FILE="path/to/midi/file.mid" OUTPUT_FILE="path/to/output" +make convert-cairo INPUT_FILE="path/to/midi/file.mid" OUTPUT_FILE="path/to/output" ``` + +Convert to Midi format: + +```bash +make convert-to-midi INPUT_FILE="path/to/cairo/file.cairo" OUTPUT_FILE="path/to/output" +``` \ No newline at end of file diff --git a/python/cli.py b/python/cli.py index 9dc32cd2..f1ade8a3 100644 --- a/python/cli.py +++ b/python/cli.py @@ -1,28 +1,23 @@ import argparse -from midi_conversion import midi_to_cairo_struct, midi_to_json - +from midi_conversion import midi_to_cairo_struct, midi_to_json, cairo_struct_to_midi def main(): - parser = argparse.ArgumentParser( - description='Convert MIDI files to Cairo or JSON format') - parser.add_argument('midi_file', type=str, - help='Path to the input MIDI file') - parser.add_argument('output_file', type=str, - help='Path to the output file') - parser.add_argument( - '--format', choices=['cairo', 'json'], default='json', help='Output format: cairo or json') + parser = argparse.ArgumentParser(description='Convert MIDI files to and from Cairo or JSON format') + parser.add_argument('input_file', type=str, help='Path to the input file') + parser.add_argument('output_file', type=str, help='Path to the output file') + parser.add_argument('--format', choices=['cairo', 'json', 'midi'], default='json', help='Output format: cairo, json, or midi (for converting back to MIDI)') args = parser.parse_args() if args.format == 'cairo': - midi_to_cairo_struct(args.midi_file, args.output_file) - print( - f"Converted {args.midi_file} to Cairo format in {args.output_file} ✅") + midi_to_cairo_struct(args.input_file, args.output_file) + print(f"Converted {args.input_file} to Cairo format in {args.output_file} ✅") elif args.format == 'json': - midi_to_json(args.midi_file, args.output_file) - print( - f"Converted {args.midi_file} to JSON format in {args.output_file} ✅") - + midi_to_json(args.input_file, args.output_file) + print(f"Converted {args.input_file} to JSON format in {args.output_file} ✅") + elif args.format == 'midi': + cairo_struct_to_midi(args.input_file, args.output_file) + print(f"Converted {args.input_file} from Cairo/JSON format back to MIDI in {args.output_file} ✅") if __name__ == '__main__': main() diff --git a/python/midi_conversion.py b/python/midi_conversion.py index bd2603ba..eb6a92d2 100644 --- a/python/midi_conversion.py +++ b/python/midi_conversion.py @@ -1,16 +1,25 @@ -import mido import json +import re -import mido - +from mido import MidiFile, MidiTrack, MetaMessage, Message, tick2second, second2tick +from mido.midifiles import bpm2tempo def midi_to_cairo_struct(midi_file, output_file): - mid = mido.MidiFile(midi_file) + mid = MidiFile(midi_file) + current_tempo = 500000 # Default MIDI tempo (500000 microseconds per beat) cairo_events = [] for track in mid.tracks: + cumulative_time = 0 # Keep track of cumulative time in ticks for delta calculation + for msg in track: - time = format_fp32x32(msg.time) + # Update the current tempo if a tempo change message is encountered + if msg.type == 'set_tempo': + current_tempo = msg.tempo + + # Calculate the time for the event + time = format_fp32x32(cumulative_time, mid.ticks_per_beat, current_tempo) + cumulative_time += msg.time # Increment cumulative time if msg.type == 'note_on': cairo_events.append( @@ -20,7 +29,7 @@ def midi_to_cairo_struct(midi_file, output_file): f"Message::NOTE_OFF(NoteOff {{ channel: {msg.channel}, note: {msg.note}, velocity: {msg.velocity}, time: {time} }})") elif msg.type == 'set_tempo': cairo_events.append( - f"Message::SET_TEMPO(SetTempo {{ tempo: {format_fp32x32(msg.tempo)}, time: Option::Some({time}) }})") + f"Message::SET_TEMPO(SetTempo {{ tempo: {msg.tempo}, time: Option::Some({time}) }})") elif msg.type == 'time_signature': clocks_per_click = 24 cairo_events.append( @@ -47,9 +56,46 @@ def midi_to_cairo_struct(midi_file, output_file): with open(output_file, 'w') as file: file.write(full_cairo_code) +def cairo_struct_to_midi(cairo_file, output_file): + with open(cairo_file, 'r') as file: + cairo_data = file.read() + + # Regex patterns to match different MIDI event types in the Cairo data + note_on_pattern = re.compile(r"Message::NOTE_ON\(NoteOn \{ channel: (\d+), note: (\d+), velocity: (\d+), time: (.+?) \}\)") + note_off_pattern = re.compile(r"Message::NOTE_OFF\(NoteOff \{ channel: (\d+), note: (\d+), velocity: (\d+), time: (.+?) \}\)") + set_tempo_pattern = re.compile(r"Message::SET_TEMPO\(SetTempo \{ tempo: (.+?), time: (.+?) \}\)") + time_signature_pattern = re.compile(r"Message::TIME_SIGNATURE\(TimeSignature \{ numerator: (\d+), denominator: (\d+), clocks_per_click: (\d+), time: None \}\)") + control_change_pattern = re.compile(r"Message::CONTROL_CHANGE\(ControlChange \{ channel: (\d+), control: (\d+), value: (\d+), time: (.+?) \}\)") + + mid = MidiFile() + track = MidiTrack() + mid.tracks.append(track) + + for match in note_on_pattern.finditer(cairo_data): + channel, note, velocity, time = match.groups() + time = parse_fp32x32(time) + track.append(Message('note_on', note=int(note), velocity=int(velocity), time=time, channel=int(channel))) + + for match in note_off_pattern.finditer(cairo_data): + channel, note, velocity, time = match.groups() + time = parse_fp32x32(time) + track.append(Message('note_off', note=int(note), velocity=int(velocity), time=time, channel=int(channel))) + + for match in set_tempo_pattern.finditer(cairo_data): + tempo, _ = match.groups() + # Assume the tempo is directly usable or convert it as necessary + tempo = parse_fp32x32(tempo) # This may need adjustment based on your tempo representation + track.append(MetaMessage('set_tempo', tempo=tempo, time=0)) + + for match in time_signature_pattern.finditer(cairo_data): + numerator, denominator, clocks_per_click = match.groups() + # Assuming `mido` accepts time signature as integers directly + track.append(MetaMessage('time_signature', numerator=int(numerator), denominator=int(denominator), clocks_per_click=int(clocks_per_click), notated_32nd_notes_per_beat=8, time=0)) + + mid.save(output_file) def midi_to_json(midi_file, output_file): - mid = mido.MidiFile(midi_file) + mid = MidiFile(midi_file) events = [] for track in mid.tracks: @@ -89,6 +135,15 @@ def midi_to_json(midi_file, output_file): with open(output_file, 'w') as file: file.write(json_data) - -def format_fp32x32(time): - return f"FP32x32 {{ mag: {time}, sign: false }}" +def format_fp32x32(delta_ticks, ticks_per_beat, current_tempo): + delta_seconds = tick2second(delta_ticks, ticks_per_beat, current_tempo) + fp32x32_time = int(delta_seconds * 1e6) # Assuming we want microseconds precision + return f"FP32x32 {{ mag: {fp32x32_time}, sign: false }}" + +def parse_fp32x32(fp32x32_str): + # Extract the magnitude part from the FP32x32 formatted string + mag_match = re.search(r"mag: (\d+)", fp32x32_str) + if mag_match: + return int(mag_match.group(1)) + else: + return 0