MIDI Tools - Tesla Coil MIDI Processing Suite
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

171 lines
5.5 KiB

import mido
from mido import MidiFile, MidiTrack, Message, MetaMessage
import sys
import os
import argparse
def parse_args():
parser = argparse.ArgumentParser(description='Bake tempo changes into MIDI file with constant tempo.')
parser.add_argument('input_file', type=str, help='Path to the input MIDI file.')
return parser.parse_args()
class MIDIMessage:
def __init__(self, message, track, abs_tick, abs_time):
self.message = message
self.track = track
self.abs_tick = abs_tick
self.abs_time = abs_time
self.new_tick = None # To be calculated later
def compute_absolute_times(mid):
"""
Compute absolute time for each message across all tracks, considering tempo changes.
Returns a list of MIDIMessage instances with absolute times.
"""
ticks_per_beat = mid.ticks_per_beat
DEFAULT_TEMPO = 500000 # Default tempo (microseconds per beat)
# Collect all messages with their absolute ticks and track index
all_msgs = []
for track_index, track in enumerate(mid.tracks):
abs_tick = 0
for msg in track:
abs_tick += msg.time
all_msgs.append(MIDIMessage(msg, track_index, abs_tick, 0.0))
# Sort all messages by absolute tick
all_msgs.sort(key=lambda m: m.abs_tick)
# Now, compute absolute times
current_tempo = DEFAULT_TEMPO
abs_time = 0.0
prev_tick = 0
for msg in all_msgs:
delta_ticks = msg.abs_tick - prev_tick
delta_time = mido.tick2second(delta_ticks, ticks_per_beat, current_tempo)
abs_time += delta_time
msg.abs_time = abs_time
# If the message is a tempo change, update the current tempo
if msg.message.type == 'set_tempo':
current_tempo = msg.message.tempo
prev_tick = msg.abs_tick
return all_msgs
def bake_tempo(all_msgs, ticks_per_beat, constant_tempo):
"""
Assign new ticks to each message based on absolute time and constant tempo.
"""
for msg in all_msgs:
# Calculate new tick based on absolute time and constant tempo
# new_tick = abs_time / seconds_per_tick
# seconds_per_tick = constant_tempo / ticks_per_beat / 1e6
seconds_per_tick = constant_tempo / ticks_per_beat / 1e6
msg.new_tick = int(round(msg.abs_time / seconds_per_tick))
return all_msgs
def assign_ticks_to_tracks(all_msgs, mid, ticks_per_beat):
"""
Assign new delta ticks to each track based on the baked tempo.
Returns a list of tracks with updated messages.
"""
# Prepare a list for each track
new_tracks = [[] for _ in mid.tracks]
# Sort messages back into their respective tracks
for msg in all_msgs:
if msg.message.type == 'set_tempo':
# Skip tempo messages
continue
new_tracks[msg.track].append(msg)
# Now, for each track, sort messages by new_tick and assign delta ticks
for track_index, track_msgs in enumerate(new_tracks):
# Sort messages by new_tick
track_msgs.sort(key=lambda m: m.new_tick)
# Assign delta ticks
prev_tick = 0
new_track = []
for msg in track_msgs:
delta_tick = msg.new_tick - prev_tick
prev_tick = msg.new_tick
# Create a copy of the message to avoid modifying the original
new_msg = msg.message.copy(time=delta_tick)
new_track.append(new_msg)
# Ensure the track ends with an end_of_track message
if not new_track or new_track[-1].type != 'end_of_track':
new_track.append(MetaMessage('end_of_track', time=0))
new_tracks[track_index] = new_track
return new_tracks
def get_initial_tempo(all_msgs, default_tempo=500000):
"""
Retrieve the initial tempo from the list of MIDI messages.
If no set_tempo message is found, return the default tempo.
"""
for msg in all_msgs:
if msg.message.type == 'set_tempo':
return msg.message.tempo
return default_tempo
def main():
args = parse_args()
input_path = args.input_file
if not os.path.isfile(input_path):
print(f"Error: File '{input_path}' does not exist.")
sys.exit(1)
try:
mid = MidiFile(input_path)
except Exception as e:
print(f"Error reading MIDI file: {e}")
sys.exit(1)
ticks_per_beat = mid.ticks_per_beat
# Compute absolute times for all messages
all_msgs = compute_absolute_times(mid)
# Get the initial tempo (first set_tempo message or default)
initial_tempo = get_initial_tempo(all_msgs)
# Bake the tempo by assigning new ticks based on constant tempo
all_msgs = bake_tempo(all_msgs, ticks_per_beat, initial_tempo)
# Assign new delta ticks to each track
new_tracks = assign_ticks_to_tracks(all_msgs, mid, ticks_per_beat)
# Create a new MIDI file
new_mid = MidiFile(ticks_per_beat=ticks_per_beat)
# Create a tempo track with the initial tempo
tempo_track = MidiTrack()
tempo_track.append(MetaMessage('set_tempo', tempo=initial_tempo, time=0))
new_mid.tracks.append(tempo_track)
# Add the updated performance tracks
for track in new_tracks:
new_mid.tracks.append(track)
# Define output file name
base, ext = os.path.splitext(input_path)
output_path = f"{base}_tempo{ext}"
try:
new_mid.save(output_path)
print(f"Successfully saved baked MIDI file as '{output_path}'.")
except Exception as e:
print(f"Error saving MIDI file: {e}")
sys.exit(1)
if __name__ == "__main__":
main()