Source code for jdxi_editor.ui.editors.midi_player.midi_analyzer

"""
MIDI analysis for the file player: tempo, drum detection, track classification, channel selection.

Pure domain logic — no Qt, no UI. The editor calls these methods and applies results to state/UI.
"""

from typing import Optional

from jdxi_editor.midi.track.classification import classify_tracks
from jdxi_editor.midi.utils.drum_detection import detect_drum_tracks
from picomidi.constant import Midi
from picomidi.message.type import MidoMessageType


[docs] class MidiAnalyzer: """ MIDI file analysis: initial tempo, drum track detection, track classification, and preferred playback channel selection. """
[docs] def get_initial_tempo(self, midi_file) -> tuple[int, dict[int, int]]: """ Detect initial tempo from the first set_tempo message in the file. :param midi_file: mido.MidiFile :return: (tempo_initial_usec, initial_track_tempos) tempo_initial_usec: first tempo found or default 120 BPM initial_track_tempos: map track_number -> tempo (for tracks that have set_tempo) """ tempo_initial = Midi.tempo.BPM_120_USEC initial_track_tempos: dict[int, int] = {} for track_number, track in enumerate(midi_file.tracks): for msg in track: if msg.type == MidoMessageType.SET_TEMPO.value: tempo_initial = msg.tempo initial_track_tempos[track_number] = msg.tempo break else: continue break return (tempo_initial, initial_track_tempos)
[docs] def get_drum_tracks( self, midi_file, min_score: float = 70.0 ) -> list[tuple[int, dict]]: """ Detect drum tracks in the MIDI file. :param midi_file: mido.MidiFile :param min_score: minimum score to consider a track as drums :return: list of (track_index, analysis_dict) sorted by score descending """ return detect_drum_tracks(midi_file, min_score=min_score)
[docs] def get_classifications( self, midi_file, exclude_drum_indices: Optional[list[int]] = None, min_score: float = 30.0, ) -> dict: """ Classify non-drum tracks into Bass, Keys/Guitars, Strings. :param midi_file: mido.MidiFile :param exclude_drum_indices: track indices to exclude (e.g. drum tracks) :param min_score: minimum score for a track to be classified :return: dict with keys "bass", "keys_guitars", "strings", "unclassified"; each value is a list of (track_index, TrackStats) """ return classify_tracks( midi_file, exclude_drum_tracks=exclude_drum_indices or [], min_score=min_score, )
[docs] def get_preferred_channel( self, midi_file, preferred_channels: set[int] ) -> Optional[int]: """ Pick a playback channel from the file that is in the preferred set. :param midi_file: mido.MidiFile :param preferred_channels: set of channel numbers (e.g. 0, 1, 2, 9 for 1-based 1,2,3,10) :return: channel index 0–15, or None if no message uses a preferred channel """ for track in midi_file.tracks: for msg in track: if hasattr(msg, "channel") and msg.channel in preferred_channels: return msg.channel return None