Source code for jdxi_editor.midi.track.analyzer

from mido import MidiTrack

from jdxi_editor.midi.track.data import BASS_NOTE_MAX
from jdxi_editor.midi.track.stats import TrackStats


[docs] class TrackAnalyzer: """Track analyser to add tracks""" def __init__(self, track: MidiTrack, index: int):
[docs] self.track = track
[docs] self.stats = TrackStats(index)
[docs] self.time = 0
[docs] self.active_notes: dict[int, int] = {}
[docs] self.note_start: dict[int, int] = {}
[docs] self.durations: list[int] = []
[docs] def run(self) -> TrackStats: """run the analysis and send to dispatcher""" self._read_track_name() for msg in self.track: self.time += msg.time if msg.is_meta: continue self._dispatch(msg) self._finalize() return self.stats
[docs] def _dispatch(self, msg): """dispatch to handler""" if hasattr(msg, "channel"): self.stats.channels.add(msg.channel) handler = getattr(self, f"_on_{msg.type}", None) if handler: handler(msg)
[docs] def _on_note_on(self, msg): """on note on""" if msg.velocity == 0: return self._on_note_off(msg) s = self.stats note = msg.note s.note_count += 1 s.notes.append(note) s.velocities.append(msg.velocity) s.note_ons.append((self.time, note, msg.channel)) s.lowest_note = min(s.lowest_note, note) s.highest_note = max(s.highest_note, note) if note <= BASS_NOTE_MAX: s.bass_note_count += 1 elif note <= 72: s.mid_range_note_count += 1 else: s.high_note_count += 1 self.active_notes[note] = self.time s.max_simultaneous = max(s.max_simultaneous, len(self.active_notes)) self.note_start[note] = self.time
[docs] def _on_note_off(self, msg): """check for legato""" note = msg.note self.stats.note_offs.append((self.time, note)) start = self.note_start.pop(note, None) if start is None: return duration = self.time - start self.durations.append(duration) # legato detection for other_start in self.note_start.values(): if start < other_start < self.time: self.stats.legato_score += 1 self.active_notes.pop(note, None)
[docs] def _on_pitchwheel(self, msg): """check for bent notes""" self.stats.has_pitch_bend = True
[docs] def _on_control_change(self, msg): """add cc""" self.stats.has_control_change = True
[docs] def _on_program_change(self, msg): """add pc""" self.stats.program_changes.append(msg.program)
[docs] def _finalize(self): """finalize analysis""" s = self.stats if self.durations: s.avg_note_duration = sum(self.durations) / len(self.durations) if s.lowest_note < 127: s.note_range = s.highest_note - s.lowest_note if s.note_count: s.legato_score /= s.note_count # To avoid circular import error from jdxi_editor.midi.track.classification import calculate_scores calculate_scores(s)
[docs] def _read_track_name(self): """Extract track name from meta messages""" for msg in self.track: if msg.is_meta and msg.type == "track_name": self.stats.track_name = msg.name break