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.
 
 
 
 

122 lines
5.0 KiB

import mido
from .midi_utils import get_instrument_name, has_musical_messages, collect_tempo_changes
def analyze_midi(midi: mido.MidiFile, filename: str = "") -> dict:
analysis = {
"song_title": filename,
"tempo": {
"min_bpm": None,
"max_bpm": None
},
"pitch_bend": {
"min_semitones": None,
"max_semitones": None
},
"tracks": [],
"notes": "",
"song_offset": 0
}
tempo_changes = collect_tempo_changes(midi)
tempos = [bpm for (_, bpm) in tempo_changes]
analysis["tempo"]["min_bpm"] = min(tempos)
analysis["tempo"]["max_bpm"] = max(tempos)
musical_track_count = 0
channel_rpn_state = {channel: {'selected_rpn_msb': None, 'selected_rpn_lsb': None, 'rpn_selected': None} for channel in range(16)}
channel_pitch_bend_range = {channel: 2 for channel in range(16)}
global_pitch_bends = []
for track in midi.tracks:
if not has_musical_messages(track):
continue
musical_track_count += 1
track_info = {
"track_name": f"Track {musical_track_count}",
"Channel Assignment": [],
"Pitch Bend Sensitivity": {},
"Max Note Velocity": "N/A",
"Min Note Velocity": "N/A",
"Uses Program Change": False
}
program_changes = []
pitch_bends_semitones = []
absolute_time = 0
for msg in track:
absolute_time += msg.time
if msg.type == 'track_name':
track_info["track_name"] = msg.name
elif hasattr(msg, 'channel'):
channel = msg.channel
if (channel + 1) not in track_info["Channel Assignment"]:
track_info["Channel Assignment"].append(channel + 1)
if msg.type == 'note_on' and msg.velocity > 0:
velocity = msg.velocity
if track_info["Max Note Velocity"] == "N/A" or velocity > track_info["Max Note Velocity"]:
track_info["Max Note Velocity"] = velocity
if track_info["Min Note Velocity"] == "N/A" or velocity < track_info["Min Note Velocity"]:
track_info["Min Note Velocity"] = velocity
elif msg.type == 'program_change':
track_info["Uses Program Change"] = True
program_changes.append({
"program_number": msg.program,
"instrument_name": get_instrument_name(msg.program),
"tick": absolute_time
})
elif msg.type == 'control_change':
if msg.control == 101:
channel_rpn_state[channel]['selected_rpn_msb'] = msg.value
if msg.value != 0:
channel_rpn_state[channel]['rpn_selected'] = None
elif msg.control == 100:
channel_rpn_state[channel]['selected_rpn_lsb'] = msg.value
if (channel_rpn_state[channel]['selected_rpn_msb'] == 0 and
channel_rpn_state[channel]['selected_rpn_lsb'] == 0):
channel_rpn_state[channel]['rpn_selected'] = 'pitch_bend_range'
else:
channel_rpn_state[channel]['rpn_selected'] = None
elif msg.control == 6:
if channel_rpn_state[channel].get('rpn_selected') == 'pitch_bend_range':
channel_pitch_bend_range[channel] = msg.value
elif msg.control == 38:
pass
elif msg.type == 'pitchwheel':
current_range = channel_pitch_bend_range[channel]
semitones = (msg.pitch / 8192) * current_range
pitch_bends_semitones.append(semitones)
global_pitch_bends.append(semitones)
for channel in track_info["Channel Assignment"]:
sensitivity = channel_pitch_bend_range[channel - 1]
track_info["Pitch Bend Sensitivity"][f"Channel {channel}"] = round(sensitivity, 2)
if pitch_bends_semitones:
track_info["pitch_bend"] = {
"min_semitones": round(min(pitch_bends_semitones), 2),
"max_semitones": round(max(pitch_bends_semitones), 2)
}
else:
track_info["pitch_bend"] = {
"min_semitones": 0.0,
"max_semitones": 0.0
}
if program_changes:
track_info["Program Changes"] = program_changes
analysis["tracks"].append(track_info)
if global_pitch_bends:
analysis["pitch_bend"]["min_semitones"] = round(min(global_pitch_bends), 2)
analysis["pitch_bend"]["max_semitones"] = round(max(global_pitch_bends), 2)
else:
analysis["pitch_bend"]["min_semitones"] = 0.0
analysis["pitch_bend"]["max_semitones"] = 0.0
return analysis