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
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()
|