jdxi_editor.midi.playback.controller ==================================== .. py:module:: jdxi_editor.midi.playback.controller .. autoapi-nested-parse:: Pattern Playback Controller Module Manages MIDI pattern playback using the PlaybackEngine. Handles: - Starting/stopping/pausing playback - Building MIDI files from patterns - UI synchronization during playback - Muting/unmuting channels - Shuffle play functionality Classes ------- .. autoapisummary:: jdxi_editor.midi.playback.controller.PlaybackConfig jdxi_editor.midi.playback.controller.PlaybackPosition jdxi_editor.midi.playback.controller.PatternPlaybackController Module Contents --------------- .. py:class:: PlaybackConfig(ticks_per_beat: int = 480, beats_per_measure: int = 4, measure_beats: int = 16, default_bpm: int = 120, playback_interval_ms: int = 20) Configuration for playback controller. .. py:attribute:: ticks_per_beat :value: 480 .. py:attribute:: beats_per_measure :value: 4 .. py:attribute:: measure_beats :value: 16 .. py:attribute:: default_bpm :value: 120 .. py:attribute:: playback_interval_ms :value: 20 .. py:property:: ticks_per_measure :type: int Calculate ticks per bar. .. py:class:: PlaybackPosition(global_step: int = 0, bar_index: int = 0, step_in_bar: int = 0) Represents the current playback position. .. py:attribute:: global_step :value: 0 .. py:attribute:: bar_index :value: 0 .. py:attribute:: step_in_bar :value: 0 .. py:class:: PatternPlaybackController(config: Optional[PlaybackConfig] = None, playback_engine: Optional[picomidi.playback.engine.PlaybackEngine] = None, scope: str = 'PatternPlaybackController') Bases: :py:obj:`PySide6.QtCore.QObject` Controls pattern playback and synchronization. Manages: - PlaybackEngine operation - UI updates during playback (bar/step highlighting) - Muting/unmuting channels - Pause/resume functionality - Shuffle play .. py:attribute:: config .. py:attribute:: playback_engine .. py:attribute:: scope :value: 'PatternPlaybackController' .. py:attribute:: is_playing :value: False .. py:attribute:: is_paused :value: False .. py:attribute:: current_bpm :value: 120 .. py:attribute:: current_position .. py:attribute:: last_bar_index :value: -1 .. py:attribute:: last_step_in_bar :value: -1 .. py:attribute:: muted_channels :type: List[int] :value: [] .. py:attribute:: timer :type: Optional[PySide6.QtCore.QTimer] :value: None .. py:attribute:: worker .. py:attribute:: on_playback_started :type: Optional[Callable[[], None]] :value: None .. py:attribute:: on_playback_stopped :type: Optional[Callable[[], None]] :value: None .. py:attribute:: on_playback_paused :type: Optional[Callable[[], None]] :value: None .. py:attribute:: on_playback_resumed :type: Optional[Callable[[], None]] :value: None .. py:attribute:: on_position_changed :type: Optional[Callable[[PlaybackPosition], None]] :value: None .. py:attribute:: on_bar_changed :type: Optional[Callable[[int], None]] :value: None .. py:attribute:: on_step_changed :type: Optional[Callable[[int], None]] :value: None .. py:attribute:: on_midi_event :type: Optional[Callable[[mido.Message], Any]] :value: None .. py:method:: start_playback(measures: List, bpm: Optional[int] = None) -> bool Start pattern playback. :param measures: List of PatternMeasure objects to play :param bpm: Optional tempo override :return: True if playback started successfully .. py:method:: _on_worker_finished() -> None Handle worker.finished when engine stops playing. .. py:method:: stop_playback() -> None Stop pattern playback. .. py:method:: pause_playback() -> None Pause pattern playback (can be resumed). .. py:method:: resume_playback() -> None Resume paused playback. .. py:method:: toggle_pause() -> None Toggle pause/resume state. .. py:method:: shuffle_play(measures: List, bpm: Optional[int] = None) -> bool Select a random bar and start playback. :param measures: List of PatternMeasure objects :param bpm: Optional tempo override :return: True if playback started .. py:method:: mute_channel(channel: int, mute: bool = True) -> None Mute or unmute a specific MIDI channel. :param channel: MIDI channel (0-15) :param mute: True to mute, False to unmute .. py:method:: mute_row(row: int, mute: bool = True) -> None Mute or unmute a sequencer row. Row to channel mapping: - Row 0: Channel 0 (Digital Synth 1) - Row 1: Channel 1 (Digital Synth 2) - Row 2: Channel 2 (Analog Synth) - Row 3: Channel 9 (Drums) :param row: Row index (0-3) :param mute: True to mute, False to unmute .. py:method:: toggle_mute_row(row: int) -> bool Toggle mute for a row. :param row: Row index (0-3) :return: New mute state (True if now muted) .. py:method:: is_row_muted(row: int) -> bool Check if a row is muted. :param row: Row index (0-3) :return: True if row is muted .. py:method:: process_playback_tick(total_steps: int) -> Optional[PlaybackPosition] Process a playback timer tick. Called by timer. Updates engine and returns current position. :param total_steps: Total steps in pattern (bars * steps_per_bar) :return: Updated PlaybackPosition or None if playback has stopped .. py:method:: get_current_position() -> PlaybackPosition Get the current playback position. :return: PlaybackPosition object .. py:method:: get_playback_state() -> Dict[str, bool] Get the current playback state. :return: Dictionary with is_playing, is_paused, muted_channels .. py:method:: set_tempo(bpm: int) -> None Set playback tempo. :param bpm: Tempo in BPM .. py:method:: reload_playback_with_tempo(measures: List, bpm: int) -> bool Rebuild MIDI with new tempo and resume from current position. Call when tempo changes during playback. :param measures: Current pattern measures :param bpm: New tempo in BPM :return: True if reload succeeded .. py:method:: get_tempo() -> int Get current playback tempo. .. py:method:: _build_midi_file_for_playback(measures: List) -> mido.MidiFile Build a MIDI file from the pattern for playback. :param measures: List of PatternMeasure objects :return: MidiFile ready for playback .. py:method:: _collect_sequencer_events(measures: List) -> List[picomidi.sequencer.event.SequencerEvent] Collect all note events from measures. :param measures: List of PatternMeasure objects :return: List of SequencerEvent objects .. py:method:: _sequencer_event(channel: int, duration_ticks: int, spec: picomidi.ui.widget.button.note.NoteButtonEvent, tick: int, velocity: int) -> picomidi.sequencer.event.SequencerEvent add sequencer event .. py:method:: _ms_to_ticks(duration_ms: float) -> int Convert milliseconds to MIDI ticks. Formula: ticks = (duration_ms / 1000) * (bpm / 60) * ticks_per_beat :param duration_ms: Duration in milliseconds :return: Duration in MIDI ticks .. py:method:: _get_button_note_spec(button) Get note specification from a button. :param button: SequencerButton instance :return: NoteButtonSpec with note, duration_ms, velocity, is_active .. py:method:: _get_engine_tick() -> int Get the current tick position from the playback engine. :return: Absolute tick position .. py:method:: _start_timer() -> None Start the playback timer. .. py:method:: _stop_timer() -> None Stop the playback timer. .. py:method:: _update_timer_interval() -> None Update timer interval based on current tempo. .. py:method:: set_config(config: PlaybackConfig) -> None Update controller configuration. :param config: New PlaybackConfig .. py:method:: get_engine() -> picomidi.playback.engine.PlaybackEngine Get the underlying PlaybackEngine. Useful for advanced control if needed. :return: PlaybackEngine instance