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.
79 lines
2.8 KiB
79 lines
2.8 KiB
import mido
|
|
from .midi_utils import has_musical_messages
|
|
|
|
CC_NAMES = {
|
|
0: "Bank Select", 1: "Modulation", 2: "Breath Controller", 4: "Foot Controller",
|
|
5: "Portamento Time", 7: "Volume", 10: "Pan", 11: "Expression",
|
|
64: "Sustain Pedal", 65: "Portamento", 66: "Sostenuto", 67: "Soft Pedal",
|
|
71: "Resonance", 72: "Release Time", 73: "Attack Time", 74: "Cutoff Frequency",
|
|
91: "Reverb", 93: "Chorus", 94: "Detune",
|
|
}
|
|
|
|
|
|
def get_track_detail(midi: mido.MidiFile, track_index: int) -> dict | None:
|
|
"""Extract detailed time-series data for a specific musical track."""
|
|
musical_idx = 0
|
|
target_track = None
|
|
for track in midi.tracks:
|
|
if not has_musical_messages(track):
|
|
continue
|
|
if musical_idx == track_index:
|
|
target_track = track
|
|
break
|
|
musical_idx += 1
|
|
|
|
if target_track is None:
|
|
return None
|
|
|
|
ticks_per_beat = midi.ticks_per_beat
|
|
|
|
track_name = f"Track {track_index + 1}"
|
|
control_changes = {} # cc_number -> list of [tick, value]
|
|
pitch_bend = [] # list of [tick, value]
|
|
velocities = [] # list of [tick, velocity]
|
|
ongoing_notes = {} # (note, channel) -> (start_tick, velocity)
|
|
notes = [] # list of [note, start, end, velocity]
|
|
|
|
absolute_tick = 0
|
|
|
|
for msg in target_track:
|
|
absolute_tick += msg.time
|
|
|
|
if msg.type == 'track_name':
|
|
track_name = msg.name
|
|
elif msg.type == 'note_on' and msg.velocity > 0:
|
|
velocities.append([absolute_tick, msg.velocity])
|
|
key = (msg.note, msg.channel)
|
|
ongoing_notes[key] = (absolute_tick, msg.velocity)
|
|
elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0):
|
|
key = (msg.note, msg.channel)
|
|
if key in ongoing_notes:
|
|
start_tick, vel = ongoing_notes.pop(key)
|
|
notes.append([msg.note, start_tick, absolute_tick, vel])
|
|
elif msg.type == 'control_change':
|
|
cc = msg.control
|
|
if cc not in control_changes:
|
|
control_changes[cc] = {
|
|
"name": CC_NAMES.get(cc, f"CC {cc}"),
|
|
"data": []
|
|
}
|
|
control_changes[cc]["data"].append([absolute_tick, msg.value])
|
|
elif msg.type == 'pitchwheel':
|
|
pitch_bend.append([absolute_tick, msg.pitch])
|
|
|
|
# Convert cc keys to strings for JSON
|
|
cc_out = {}
|
|
for cc_num, cc_data in sorted(control_changes.items()):
|
|
cc_out[str(cc_num)] = cc_data
|
|
|
|
total_ticks = absolute_tick
|
|
|
|
return {
|
|
"track_name": track_name,
|
|
"ticks_per_beat": ticks_per_beat,
|
|
"total_ticks": total_ticks,
|
|
"notes": notes,
|
|
"control_changes": cc_out,
|
|
"pitch_bend": pitch_bend,
|
|
"velocities": velocities
|
|
}
|