Skip to content
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
15 changes: 10 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
29 changes: 12 additions & 17 deletions python/cli.py
Original file line number Diff line number Diff line change
@@ -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()
75 changes: 65 additions & 10 deletions python/midi_conversion.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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