From 7e33436ce41bc72a3acc1f3b9bc9e99a31d51039 Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Tue, 19 Mar 2024 18:52:06 +0530 Subject: [PATCH 1/4] add cairo_struct_to_midi functionality --- Makefile | 15 +++++++---- python/cli.py | 31 +++++++++++------------ python/midi_conversion.py | 53 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 24 deletions(-) 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/python/cli.py b/python/cli.py index 9dc32cd2..2947eea8 100644 --- a/python/cli.py +++ b/python/cli.py @@ -1,28 +1,25 @@ 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} ✅") + # Assuming the input is always a MIDI file when converting to cairo or json + 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': + # Assuming the input for midi format is a structured format or json that needs to be converted back to MIDI + cairo_struct_to_midi(args.input_file, args.output_file) # Implement this functionality based on your data structure + 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..6a38b15a 100644 --- a/python/midi_conversion.py +++ b/python/midi_conversion.py @@ -1,8 +1,8 @@ import mido import json +import re -import mido - +from mido import MidiFile, MidiTrack, Message def midi_to_cairo_struct(midi_file, output_file): mid = mido.MidiFile(midi_file) @@ -47,6 +47,43 @@ 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 \}\)") + + 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(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(mido.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(mido.MetaMessage('time_signature', numerator=int(numerator), denominator=int(denominator), clocks_per_metronome_click=int(clocks_per_click), thirty_seconds_per_24_clocks=8, time=0)) + track.append(mido.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) @@ -92,3 +129,15 @@ def midi_to_json(midi_file, output_file): def format_fp32x32(time): return f"FP32x32 {{ mag: {time}, sign: false }}" + +# Helper function to parse FP32x32 formatted string (needed for time, tempo, etc.) +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: + mag = int(mag_match.group(1)) + # Assuming the magnitude directly represents the value we want + return mag + else: + # Return a default value if parsing fails + return 0 From e46e7007ecaa52c8fc9275aee3f346f77be65f5e Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Tue, 19 Mar 2024 19:00:33 +0530 Subject: [PATCH 2/4] update README --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 From 64c06263157a33ac584da81a925ff900a3d34705 Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Wed, 27 Mar 2024 15:53:03 +0530 Subject: [PATCH 3/4] fix: update parse(time) to parse_fp32x32 --- python/midi_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/midi_conversion.py b/python/midi_conversion.py index 6a38b15a..621de210 100644 --- a/python/midi_conversion.py +++ b/python/midi_conversion.py @@ -68,7 +68,7 @@ def cairo_struct_to_midi(cairo_file, output_file): for match in note_off_pattern.finditer(cairo_data): channel, note, velocity, time = match.groups() - time = parse(time) + 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): From c5c88f1629b122375cc6861a3cf3303c3ec2dcbd Mon Sep 17 00:00:00 2001 From: Aryan Godara Date: Wed, 27 Mar 2024 17:27:28 +0530 Subject: [PATCH 4/4] minor fixes related to set_tempo --- python/cli.py | 4 +--- python/midi_conversion.py | 40 ++++++++++++++++++++++----------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/python/cli.py b/python/cli.py index 2947eea8..f1ade8a3 100644 --- a/python/cli.py +++ b/python/cli.py @@ -10,15 +10,13 @@ def main(): args = parser.parse_args() if args.format == 'cairo': - # Assuming the input is always a MIDI file when converting to cairo or json 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.input_file, args.output_file) print(f"Converted {args.input_file} to JSON format in {args.output_file} ✅") elif args.format == 'midi': - # Assuming the input for midi format is a structured format or json that needs to be converted back to MIDI - cairo_struct_to_midi(args.input_file, args.output_file) # Implement this functionality based on your data structure + 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__': diff --git a/python/midi_conversion.py b/python/midi_conversion.py index 621de210..eb6a92d2 100644 --- a/python/midi_conversion.py +++ b/python/midi_conversion.py @@ -1,16 +1,25 @@ -import mido import json import re -from mido import MidiFile, MidiTrack, Message +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( @@ -56,6 +65,7 @@ def cairo_struct_to_midi(cairo_file, output_file): 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() @@ -75,18 +85,17 @@ def cairo_struct_to_midi(cairo_file, output_file): 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(mido.MetaMessage('set_tempo', tempo=tempo, time=0)) + 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(mido.MetaMessage('time_signature', numerator=int(numerator), denominator=int(denominator), clocks_per_metronome_click=int(clocks_per_click), thirty_seconds_per_24_clocks=8, time=0)) - track.append(mido.MetaMessage('time_signature', numerator=int(numerator), denominator=int(denominator), clocks_per_click=int(clocks_per_click), notated_32nd_notes_per_beat=8, time=0)) + 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: @@ -126,18 +135,15 @@ def midi_to_json(midi_file, output_file): with open(output_file, 'w') as file: file.write(json_data) +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 format_fp32x32(time): - return f"FP32x32 {{ mag: {time}, sign: false }}" - -# Helper function to parse FP32x32 formatted string (needed for time, tempo, etc.) 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: - mag = int(mag_match.group(1)) - # Assuming the magnitude directly represents the value we want - return mag + return int(mag_match.group(1)) else: - # Return a default value if parsing fails return 0