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