subsequence.midi_utils ====================== .. py:module:: subsequence.midi_utils .. autoapi-nested-parse:: MIDI device plumbing — discovering, opening, and registering hardware ports. Provides interactive/automatic output and input device selection, the multi-device registry used by the sequencer, and the ``bank_select()`` helper for addressing synth banks beyond the first 128 programs. Module Contents --------------- .. py:class:: MidiDeviceRegistry Ordered registry of named MIDI ports (output or input). Devices are stored in insertion order. Index 0 is always the first (or only) device — the default for all APIs that do not specify a device. Devices can be looked up by integer index or by name string. ``None`` always resolves to index 0. The registry is intended to be append-only once playback has started. All registered port objects must already be open. Create an empty registry; populate it with add(). .. py:method:: add(name: str, port: Any, latency_ms: float = 0.0) -> int Register a port under *name*. Returns the assigned integer index. *latency_ms* is the device's physical output latency (non-negative); see :meth:`set_latency`. .. py:method:: close_all() -> None Close every registered port and clear the registry. .. py:method:: get(device: DeviceId = None) -> Optional[Any] Return the port for *device*, or ``None`` if the registry is empty. ``None`` → index 0. ``int`` → direct index. ``str`` → name lookup. Returns ``None`` if the device cannot be resolved (empty registry, out-of-range index, unknown name). .. py:method:: index_of(device: DeviceId = None) -> int Resolve *device* to an integer index. Returns 0 for ``None``. Returns -1 if the name is unknown or the index is out of range. .. py:method:: latency_of(device: DeviceId = None) -> float Return the latency (ms) for *device*, or 0.0 if it cannot be resolved. Defensive on the hot dispatch path: an unknown device yields 0.0 rather than raising, so a stray event can never crash the send loop. .. py:method:: max_latency() -> float Return the largest latency across all registered devices (0.0 if empty). .. py:method:: replace(index: int, port: Any) -> None Replace the port object at *index* without changing the name or index mapping. Used by the backward-compat ``midi_out``/``midi_in`` setters to allow test code to inject a fake port after the registry has been populated. Raises ``IndexError`` if *index* is out of range. .. py:method:: set_latency(device: DeviceId, latency_ms: float) -> None Set the physical output latency (milliseconds) for *device*. *latency_ms* must be non-negative — a device cannot sound before it is triggered, so a negative output latency is meaningless. Raises ``ValueError`` for a negative value or an unknown device. .. py:function:: bank_select(bank: int) -> Tuple[int, int] Convert a 14-bit MIDI bank number to (MSB, LSB) for use with ``p.program_change()``. MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and CC 32 (Bank LSB). Together they encode a 14-bit bank number in the range 0–16,383: MSB = bank // 128 (upper 7 bits, sent on CC 0) LSB = bank % 128 (lower 7 bits, sent on CC 32) :param bank: Integer bank number, 0–16,383. Values outside this range are clamped. :returns: ``(msb, lsb)`` tuple, each value in 0–127. .. rubric:: Example .. code-block:: python msb, lsb = subsequence.bank_select(128) # → (1, 0) p.program_change(48, bank_msb=msb, bank_lsb=lsb) .. py:function:: select_input_device(device_name: Optional[str] = None, callback: Optional[Callable] = None) -> Tuple[Optional[str], Optional[Any]] Select and open a MIDI input device. If ``device_name`` is provided, attempts to open exactly that device. If ``device_name`` is None, returns None without prompting (input is optional/advanced). To enforce input, the caller should check the return value. A named device that is not present raises ValueError rather than falling back to another input: MIDI input drives clock-follow and live note capture, so silently listening to the wrong device would desynchronise or mis-record a performance. :returns: A tuple of (device_name, midi_in_object), or (None, None) when no name was given or the device failed to open. :raises ValueError: If *device_name* is not among the available inputs. .. py:function:: select_output_device(device_name: Optional[str] = None) -> Tuple[Optional[str], Optional[Any]] Select and open a MIDI output device. If ``device_name`` is provided, attempts to open that specific device. If ``device_name`` is None, auto-discovers available devices: - If exactly one device exists, it is selected automatically. - If multiple devices exist, prompts the user to choose one from the console. - If no devices exist, logs an error and returns None. :returns: A tuple of (device_name, midi_out_object) or (None, None) on failure.