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