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
-
gets— blocks 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 withTime.now.to_fat the moment the message reaches Ruby.
gets_s— the same, with:dataas 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.