Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementations for json cairo midi interconversion functions #47

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
.PHONY: convert-json convert-cairo
.PHONY: convert-midi-to-json convert-json-to-midi convert-midi-to-cairo convert-cairo-to-midi convert-json-to-cairo convert-cairo-to-json

# Default MIDI file and output file paths
MIDI_FILE ?= path/to/default/midi/file.mid
# Default input file path
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
convert-midi-to-json:
python3.11 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).json --conversion midi-to-json

convert-cairo:
python3 python/cli.py $(MIDI_FILE) $(OUTPUT_FILE).cairo --format cairo
convert-json-to-midi:
python3.11 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).mid --conversion json-to-midi

convert-midi-to-cairo:
python3 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).cairo --conversion midi-to-cairo

convert-cairo-to-midi:
python3 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).mid --conversion cairo-to-midi

convert-json-to-cairo:
python3 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).cairo --conversion json-to-cairo

convert-cairo-to-json:
python3 python/cli.py $(INPUT_FILE) $(OUTPUT_FILE).json --conversion cairo-to-json
Binary file added input.mid
Binary file not shown.
42 changes: 23 additions & 19 deletions python/cli.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import argparse
from midi_conversion import midi_to_cairo_struct, midi_to_json

from midi_conversion import midi_to_cairo_struct, cairo_struct_to_midi, cairo_struct_to_json, json_to_cairo_struct, midi_to_json, json_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 between MIDI, Cairo, and JSON formats')
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('--conversion', choices=['midi-to-json', 'midi-to-cairo', 'cairo-to-midi', 'cairo-to-json', 'json-to-cairo', 'json-to-midi'], help='Conversion type')

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} ✅")
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} ✅")

if args.conversion == 'midi-to-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.conversion == 'json-to-midi':
json_to_midi(args.input_file, args.output_file)
print(f"Converted {args.input_file} from JSON format back to MIDI in {args.output_file} ✅")
elif args.conversion == 'midi-to-cairo':
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.conversion == 'cairo-to-midi':
cairo_struct_to_midi(args.input_file, args.output_file)
print(f"Converted {args.input_file} from Cairo format back to MIDI in {args.output_file} ✅")
elif args.conversion == 'json-to-cairo':
json_to_cairo_struct(args.input_file, args.output_file)
print(f"Converted {args.input_file} from JSON to Cairo format in {args.output_file} ✅")
elif args.conversion == 'cairo-to-json':
cairo_struct_to_json(args.input_file, args.output_file)
print(f"Converted {args.input_file} from Cairo to JSON format in {args.output_file} ✅")

if __name__ == '__main__':
main()
206 changes: 198 additions & 8 deletions python/midi_conversion.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import mido
import json

import mido
import re
from mido import MidiFile, MetaMessage, MidiTrack, Message


def midi_to_cairo_struct(midi_file, output_file):
mid = mido.MidiFile(midi_file)
mid = MidiFile(midi_file)
cairo_events = []

for track in mid.tracks:
cumulative_time = 0 # Keep track of the cumulative time in ticks
for msg in track:
time = format_fp32x32(msg.time)
cumulative_time += msg.time
time = format_fp32x32(cumulative_time)

if msg.type == 'note_on':
cairo_events.append(
Expand Down Expand Up @@ -47,9 +48,189 @@ 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)

last_time = 0 # This will keep track of the last event's time to calculate delta times

for match in note_on_pattern.finditer(cairo_data):
channel, note, velocity, time_str = match.groups()
time = parse_fp32x32(time_str)
delta_time = time - last_time
if delta_time < 0:
print(f"Error: Negative delta_time encountered. time: {time}, last_time: {last_time}, delta_time: {delta_time}")
print(f"Event: {match.group(0)}")
continue # Skip this event or handle it appropriately
last_time = time
track.append(Message('note_on', note=int(note), velocity=int(velocity), time=delta_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()
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 cairo_struct_to_json(cairo_file, output_file):
with open(cairo_file, 'r') as file:
cairo_data = file.read()

# Regex patterns for parsing the Cairo structured data
# Adjust these regexes to match your specific Cairo structure accurately
message_pattern = re.compile(r"Message::(\w+)\((\w+) \{ ([^\}]+) \}\)")

# Function to convert Cairo FP32x32 format to a standard numerical representation
def parse_fp32x32(value):
match = re.match(r"FP32x32 \{ mag: (\d+), sign: (true|false) \}", value)
if match:
magnitude = int(match.group(1))
sign = -1 if match.group(2) == 'true' else 1
return magnitude * sign
return value # Return original value if the pattern does not match

events = []
for match in message_pattern.finditer(cairo_data):
message_type, _, param_str = match.groups()
params = {}
for param in param_str.split(', '):
key, value = param.split(': ', 1)
if "FP32x32" in value:
value = parse_fp32x32(value)
elif value.isdigit():
value = int(value) # Convert numeric strings to integers
params[key] = value

# Create the event dictionary with the message type as the key
event = {message_type: params}
events.append(event)

# Convert the list of events to JSON format
json_data = json.dumps({"events": events}, indent=4)

# Write the JSON data to the output file
with open(output_file, 'w') as file:
file.write(json_data)

def json_to_cairo_struct(json_file, output_file):
with open(json_file, 'r') as file:
json_data = json.load(file)

cairo_events = []
for event in json_data["events"]:
for message_type, params in event.items():
if params.get("time") is not None:
params["time"] = f'FP32x32 {{ mag: {params["time"]}, sign: false }}'
if "velocity" in params:
params["velocity"] = f'{params["velocity"]}'
if "tempo" in params:
params["tempo"] = f'FP32x32 {{ mag: {params["tempo"]}, sign: false }}'
if "value" in params:
params["value"] = f'{params["value"]}'
if "pitch" in params:
params["pitch"] = f'{params["pitch"]}'

param_str = ', '.join([f"{key}: {value}" for key, value in params.items() if value is not None])
cairo_event = f"Message::{message_type}({message_type} {{ {param_str} }})"
cairo_events.append(cairo_event)

# Assemble the Cairo code structure based on the events
cairo_code_start = "use koji::midi::types::{Midi, Message, NoteOn, NoteOff, SetTempo, TimeSignature, ControlChange, PitchWheel, AfterTouch, PolyTouch, Modes};\nuse orion::numbers::FP32x32;\n\nfn midi() -> Midi {\n Midi {\n events: vec![\n"
cairo_code_events = ',\n'.join(cairo_events)
cairo_code_end = "\n ]\n }\n}"

full_cairo_code = cairo_code_start + cairo_code_events + cairo_code_end

with open(output_file, 'w') as file:
file.write(full_cairo_code)


def json_to_midi(json_file, output_file):
with open(json_file, 'r') as file:
json_data = json.load(file)

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)

for event in json_data["events"]:
for message_type, params in event.items():
if "TIME_SIGNATURE" in event:
e = event["TIME_SIGNATURE"]
track.append(MetaMessage('time_signature', numerator=e["numerator"], denominator=e["denominator"], clocks_per_click=e.get("clocks_per_click", 24), notated_32nd_notes_per_beat=8, time=0))
elif "SET_TEMPO" in event:
e = event["SET_TEMPO"]
track.append(MetaMessage('set_tempo', tempo=e["tempo"], time=0))
elif "CONTROL_CHANGE" in event:
e = event["CONTROL_CHANGE"]
track.append(Message('control_change', channel=e["channel"], control=e["control"], value=e["value"], time=e.get("time", 0)))
elif message_type == "NOTE_ON":
track.append(Message('note_on', note=int(params["note"]), velocity=int(params["velocity"]), time=int(params["time"]), channel=int(params["channel"])))
elif message_type == "NOTE_OFF":
track.append(Message('note_off', note=int(params["note"]), velocity=int(params["velocity"]), time=int(params["time"]), channel=int(params["channel"])))

mid.save(output_file)

def json_to_midi(json_file, output_midi_file):
with open(json_file, 'r') as file:
data = json.load(file)

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)

for event in data['events']:
if "TIME_SIGNATURE" in event:
e = event["TIME_SIGNATURE"]
track.append(MetaMessage('time_signature', numerator=e["numerator"], denominator=e["denominator"], clocks_per_click=e.get("clocks_per_click", 24), notated_32nd_notes_per_beat=8, time=0))
elif "SET_TEMPO" in event:
e = event["SET_TEMPO"]
track.append(MetaMessage('set_tempo', tempo=e["tempo"], time=0))
elif "CONTROL_CHANGE" in event:
e = event["CONTROL_CHANGE"]
track.append(Message('control_change', channel=e["channel"], control=e["control"], value=e["value"], time=e.get("time", 0)))
elif "NOTE_ON" in event:
e = event["NOTE_ON"]
track.append(Message('note_on', note=e["note"], velocity=e["velocity"], time=e.get("time", 0), channel=e["channel"]))
elif "NOTE_OFF" in event:
e = event["NOTE_OFF"]
track.append(Message('note_off', note=e["note"], velocity=e["velocity"], time=e.get("time", 0), channel=e["channel"]))
elif "PITCH_WHEEL" in event:
e = event["PITCH_WHEEL"]
track.append(Message('pitchwheel', channel=e["channel"], pitch=e["pitch"], time=e.get("time", 0)))
elif "AFTER_TOUCH" in event:
e = event["AFTER_TOUCH"]
track.append(Message('aftertouch', channel=e["channel"], value=e["value"], time=e.get("time", 0)))
elif "POLY_TOUCH" in event:
e = event["POLY_TOUCH"]
track.append(Message('polytouch', note=e["note"], value=e["value"], channel=e["channel"], time=e.get("time", 0)))

mid.save(output_midi_file)


def midi_to_json(midi_file, output_file):
mid = mido.MidiFile(midi_file)
mid = MidiFile(midi_file)
events = []

for track in mid.tracks:
Expand Down Expand Up @@ -89,6 +270,15 @@ def midi_to_json(midi_file, output_file):
with open(output_file, 'w') as file:
file.write(json_data)

def format_fp32x32(ticks):
# Convert ticks to FP32x32 format
return f"FP32x32 {{ mag: {ticks}, sign: false }}"

def format_fp32x32(time):
return f"FP32x32 {{ mag: {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:
ticks = int(mag_match.group(1))
return ticks
else:
return 0
Loading
Loading