Module: MIDICommunicationsWindows::Device

Defined in:
lib/midi-communications-windows/device.rb

Overview

Enumeration of the MIDI ports WinMM offers, and the attributes every port has whichever direction it runs in.

What a port's identity is here, and what it is not

WinMM identifies a port by its index within its direction: the same integer is passed to midiInGetDevCapsW and to midiInOpen, and there is nothing else to identify a port by. So the index is the identity, and InstanceMethods#id is that index, unmodified.

This differs from Core MIDI, where endpoints are numbered from a single counter shared by sources and destinations, and an input and an output therefore never share an id. Here they do: input 0 and output 0 are both valid and unrelated. Nothing in midi-communications compares an id across directions — Input.all and Output.all each search their own list — and shifting the output indices to imitate Core MIDI would replace the real identity with an invented one.

A port's name does not identify it

WinMM stores 31 characters of a name and silently drops the rest, so two ports whose names differ only past that point arrive with the same name. Measured: two loopback endpoints created as "Reloj Bitwig ñ prueba de longitud" and "Reloj Bitwig ñ prueba de longitud DOS" come back through midiInGetDevCapsW identical in every field — same name, same wMid, same wPid, same vDriverVersion, same wTechnology. Nothing in the structure tells them apart. Only the index does.

This is not a contrived case: two ports of one interface whose long names differ at the end collapse the same way. Windows does have a stable unique identifier for each endpoint — the device interface id, which the newer APIs expose — but midiInGetDevCapsW does not carry it, so it is not reachable from here.

A consumer matching ports by name should know it may be matching the wrong one.

Manufacturer and model are nil, deliberately

WinMM reports wMid and wPid: numeric codes from the MMSYSTEM manufacturer registry, which stopped being maintained in the 1990s. Measured across every port on a Windows 11 machine — a software synth and two loopback endpoints — wMid was 1, Microsoft, every time. wPid was no better: 25 for every input and 26 for every output, the same for two different devices, so it names a generic class and a direction rather than a model. Turning those into text would label every controller on the machine as made by Microsoft — a fact about the code table, presented as a fact about the hardware.

So both are nil. A caller filtering by manufacturer finds nothing and can see that it found nothing, which is the truthful outcome; a caller offered an empty string or an invented name would match, or not match, for reasons that are not real.

Defined Under Namespace

Modules: ClassMethods, InstanceMethods

Class Method Summary collapse

Class Method Details

.all ⇒ Array<Input, Output>

Every port, of both directions.

Returns:



218
219
220
# File 'lib/midi-communications-windows/device.rb', line 218

def all
  all_by_type.values.flatten
end

.all_by_type ⇒ Hash{Symbol => Array<Input>, Array<Output>}

Every port, grouped by direction.

This is the shape midi-communications asks its platform adapters for.

Examples:

MIDICommunicationsWindows::Device.all_by_type[:output].each { |o| puts o.name }

Returns:

  • (Hash{Symbol => Array<Input>, Array<Output>}) —

    with :input and :output keys



212
213
214
# File 'lib/midi-communications-windows/device.rb', line 212

def all_by_type
  { input: inputs, output: outputs }
end

.enumerate(direction, count, klass) ⇒ Array<Input>, Array<Output>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Asks WinMM what ports exist, reusing the wrapper for each one that was already there.

The list is read afresh every time, so a port that appeared or went away is reflected — but a port that is still present comes back as the object it came back as last time. Otherwise Input.first.open and Input.first.gets would be two different objects, and the second would not be open.

"Still present" means the same index reporting the same name. The index alone is not enough: WinMM renumbers, so index 1 after a device is unplugged may be a different port than index 1 before, and handing back a wrapper holding a handle to the old one would be worse than making a new object.

Parameters:

  • direction (Symbol) —

    :input or :output

  • count (Integer) —

    how many ports WinMM reports for that direction

  • klass (Class) —

Returns:



260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/midi-communications-windows/device.rb', line 260

def enumerate(direction, count, klass)
  @ports_semaphore.synchronize do
    known = @ports[direction].to_h { |port| [port.id, port] }

    @ports[direction] = Array.new(count) do |id|
      name = direction == :input ? input_name(id) : output_name(id)
      previous = known[id]

      previous && previous.name == name ? previous : klass.new(id, name)
    end
  end
end

.input_name(id) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The name of an input port.

Parameters:

  • id (Integer) —

    the port's WinMM index

Returns:

  • (String)


278
279
280
281
282
283
284
285
# File 'lib/midi-communications-windows/device.rb', line 278

def input_name(id)
  capabilities = API::MIDIInCaps.new

  API.check!(API.midiInGetDevCapsW(id, capabilities, API::MIDIInCaps.size),
             :midiInGetDevCapsW, :input)

  port_name(capabilities)
end

.inputs ⇒ Array<Input>

Every input port WinMM offers.

Returns:

Raises:

  • (Error) —

    if WinMM refuses to describe a port it has just counted



225
226
227
# File 'lib/midi-communications-windows/device.rb', line 225

def inputs
  enumerate(:input, API.midiInGetNumDevs, Input)
end

.output_name(id) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The name of an output port.

Parameters:

  • id (Integer) —

    the port's WinMM index

Returns:

  • (String)


292
293
294
295
296
297
298
299
# File 'lib/midi-communications-windows/device.rb', line 292

def output_name(id)
  capabilities = API::MIDIOutCaps.new

  API.check!(API.midiOutGetDevCapsW(id, capabilities, API::MIDIOutCaps.size),
             :midiOutGetDevCapsW, :output)

  port_name(capabilities)
end

.outputs ⇒ Array<Output>

Every output port WinMM offers.

Note that this does not include the MIDI Mapper, which WinMM addresses by the reserved id -1 and does not count among its devices.

Returns:

Raises:

  • (Error) —

    if WinMM refuses to describe a port it has just counted



236
237
238
# File 'lib/midi-communications-windows/device.rb', line 236

def outputs
  enumerate(:output, API.midiOutGetNumDevs, Output)
end

.port_name(capabilities) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Reads szPname out of a capabilities structure.

Parameters:

Returns:

  • (String)


306
307
308
# File 'lib/midi-communications-windows/device.rb', line 306

def port_name(capabilities)
  API.read_wide_string(capabilities.to_ptr + capabilities.offset_of(:szPname), API::MAXPNAMELEN)
end