Module: MIDICommunications::PhysicalLayer

Defined in:
lib/midi-communications/physical_layer.rb

Overview

The contract a platform adapter's device objects must satisfy.

This module defines no behaviour. It exists because the contract between midi-communications and the low-level gem underneath it was, until now, implicit: spread across Loader, Device, Input::StreamReader and Output, and discoverable only by reading all four and inferring what they assume. That is workable while one adapter exists. It stops being workable the moment a second one is written, because the second author reads the documentation rather than the macOS gem's source, and the documentation did not say any of this.

An adapter supplies a loader module answering inputs and outputs with arrays of device objects, and optionally refresh (see Loader.refresh). Each device object must behave as described below. Input and Output wrap them; nothing else in this library touches them.

Attributes, readable before the device is opened

Input and Output read these in their constructor, which runs while the device list is being built and long before anyone opens anything. An adapter that only knows a device's name once it is open does not satisfy this contract.

Method Type Meaning
id Integer identifies the device; see the note on uniqueness below
name String the device's name
display_name String the name to show a person choosing a device
manufacturer String, nil nil when the platform does not report it
model String, nil nil when the platform does not report it
type Symbol :input/:source, or :output/:destination

manufacturer and model are nullable on purpose. Core MIDI reports both as strings; the Windows Multimedia API reports numeric codes from a registry that stopped being maintained in the 1990s, from which no honest string can be derived. An adapter in that position returns nil rather than an empty string or an invention, so that a consumer filtering on either can tell the difference between "does not match" and "not known here".

On the uniqueness of name

name is a label, not an identifier. Two devices may report the same one, and a consumer matching on it may therefore be matching the wrong device.

This is measured, not defensive. The Windows Multimedia API stores 31 characters of a name and drops the rest without saying so, and two endpoints whose names differed only past that point came back identical in every field it reports — same name, same manufacturer code, same product code, same driver version — distinguishable only by their index. Two ports of one interface whose long names differ at the end collapse the same way.

Device::ClassMethods#find_by_name returns the first match, which is all it can do.

On the uniqueness of id

id is unique within a direction. It is not necessarily unique across both: on Windows a device is identified by its index among inputs or among outputs, so input 0 and output 0 are different devices and both are valid. Core MIDI happens to number endpoints of both directions from a single counter, but that is a property of Core MIDI and not something a consumer may rely on. Nothing in this library compares an id across directions — Input.all and Output.all each search their own list.

Lifecycle

  • open(*args) — makes the device usable. Opening an already-open device must succeed and do nothing, because Device::InstanceMethods#open may be called on a device a caller already holds open.
  • close(*args) — releases it. Closing an already-closed device must succeed.

Input

  • getsblocks until at least one message has arrived, then returns every message accumulated, as an Array of Hashes with:
    • :data, an Array of Integer bytes making up one complete message, System Exclusive included, already split per message;
    • :timestamp, a Float of seconds — when the library received the message, not a stamp applied by the driver. Both existing adapters take it with Time.now.to_f at the moment the message reaches Ruby.
  • gets_s — the same, with :data as a hex String.

The blocking is the part most easily got wrong, and it is load-bearing. Musa::Clock::InputMidiClock reads MIDI Clock in a loop with no delay of its own, relying on gets to be where the thread waits. An adapter whose gets returned an empty array immediately would turn that loop into a spin on a full core — and would do it silently, because the music would still play.

Output

  • puts_bytes(*bytes) — sends one message given as Integer bytes.
  • puts_s(hex_string) — sends one message given as hex.

Both must accept System Exclusive.