midi-communications-windows 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.version +6 -0
- data/.yardoc/checksums +8 -0
- data/.yardoc/complete +0 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/.yardopts +8 -0
- data/Gemfile +3 -0
- data/LICENSE +165 -0
- data/README.md +176 -0
- data/Rakefile +13 -0
- data/doc/MIDICommunicationsWindows/API/MIDIHdr.html +144 -0
- data/doc/MIDICommunicationsWindows/API/MIDIInCaps.html +142 -0
- data/doc/MIDICommunicationsWindows/API/MIDIOutCaps.html +140 -0
- data/doc/MIDICommunicationsWindows/API/MSG.html +143 -0
- data/doc/MIDICommunicationsWindows/API.html +981 -0
- data/doc/MIDICommunicationsWindows/Device/ClassMethods.html +365 -0
- data/doc/MIDICommunicationsWindows/Device/InstanceMethods.html +1096 -0
- data/doc/MIDICommunicationsWindows/Device.html +988 -0
- data/doc/MIDICommunicationsWindows/Error.html +438 -0
- data/doc/MIDICommunicationsWindows/Input.html +1554 -0
- data/doc/MIDICommunicationsWindows/Message.html +700 -0
- data/doc/MIDICommunicationsWindows/Output.html +1697 -0
- data/doc/MIDICommunicationsWindows/TypeConversion.html +369 -0
- data/doc/MIDICommunicationsWindows.html +210 -0
- data/doc/_index.html +262 -0
- data/doc/class_list.html +54 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +206 -0
- data/doc/css/style.css +1089 -0
- data/doc/file.README.html +240 -0
- data/doc/file.testing-on-windows.html +253 -0
- data/doc/file_list.html +64 -0
- data/doc/frames.html +22 -0
- data/doc/index.html +240 -0
- data/doc/js/app.js +801 -0
- data/doc/js/full_list.js +334 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +382 -0
- data/doc/top-level-namespace.html +112 -0
- data/docs/testing-on-windows.md +222 -0
- data/examples/input.rb +27 -0
- data/examples/list_ports.rb +17 -0
- data/examples/output.rb +22 -0
- data/examples/sysex_output.rb +15 -0
- data/lib/midi-communications-windows/api.rb +340 -0
- data/lib/midi-communications-windows/device.rb +310 -0
- data/lib/midi-communications-windows/input.rb +486 -0
- data/lib/midi-communications-windows/message.rb +133 -0
- data/lib/midi-communications-windows/output.rb +206 -0
- data/lib/midi-communications-windows/type_conversion.rb +20 -0
- data/lib/midi-communications-windows/version.rb +4 -0
- data/lib/midi-communications-windows.rb +76 -0
- data/midi-communications-windows.gemspec +32 -0
- metadata +220 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
module MIDICommunicationsWindows
|
|
2
|
+
# Enumeration of the MIDI ports WinMM offers, and the attributes every port
|
|
3
|
+
# has whichever direction it runs in.
|
|
4
|
+
#
|
|
5
|
+
# ## What a port's identity is here, and what it is not
|
|
6
|
+
#
|
|
7
|
+
# WinMM identifies a port by its **index within its direction**: the same
|
|
8
|
+
# integer is passed to `midiInGetDevCapsW` and to `midiInOpen`, and there is
|
|
9
|
+
# nothing else to identify a port by. So the index is the identity, and
|
|
10
|
+
# {InstanceMethods#id} is that index, unmodified.
|
|
11
|
+
#
|
|
12
|
+
# This differs from Core MIDI, where endpoints are numbered from a single
|
|
13
|
+
# counter shared by sources and destinations, and an input and an output
|
|
14
|
+
# therefore never share an id. Here they do: input 0 and output 0 are both
|
|
15
|
+
# valid and unrelated. Nothing in `midi-communications` compares an id across
|
|
16
|
+
# directions — `Input.all` and `Output.all` each search their own list — and
|
|
17
|
+
# shifting the output indices to imitate Core MIDI would replace the real
|
|
18
|
+
# identity with an invented one.
|
|
19
|
+
#
|
|
20
|
+
# ## A port's name does not identify it
|
|
21
|
+
#
|
|
22
|
+
# WinMM stores 31 characters of a name and silently drops the rest, so two
|
|
23
|
+
# ports whose names differ only past that point arrive with the same name.
|
|
24
|
+
# Measured: two loopback endpoints created as "Reloj Bitwig ñ prueba de
|
|
25
|
+
# longitud" and "Reloj Bitwig ñ prueba de longitud DOS" come back through
|
|
26
|
+
# `midiInGetDevCapsW` identical in every field — same name, same `wMid`, same
|
|
27
|
+
# `wPid`, same `vDriverVersion`, same `wTechnology`. Nothing in the structure
|
|
28
|
+
# tells them apart. Only the index does.
|
|
29
|
+
#
|
|
30
|
+
# This is not a contrived case: two ports of one interface whose long names
|
|
31
|
+
# differ at the end collapse the same way. Windows does have a stable unique
|
|
32
|
+
# identifier for each endpoint — the device interface id, which the newer APIs
|
|
33
|
+
# expose — but `midiInGetDevCapsW` does not carry it, so it is not reachable
|
|
34
|
+
# from here.
|
|
35
|
+
#
|
|
36
|
+
# A consumer matching ports by name should know it may be matching the wrong
|
|
37
|
+
# one.
|
|
38
|
+
#
|
|
39
|
+
# ## Manufacturer and model are nil, deliberately
|
|
40
|
+
#
|
|
41
|
+
# WinMM reports `wMid` and `wPid`: numeric codes from the MMSYSTEM
|
|
42
|
+
# manufacturer registry, which stopped being maintained in the 1990s.
|
|
43
|
+
# Measured across every port on a Windows 11 machine — a software synth and
|
|
44
|
+
# two loopback endpoints — `wMid` was 1, Microsoft, every time. `wPid` was no
|
|
45
|
+
# better: 25 for every input and 26 for every output, the same for two
|
|
46
|
+
# different devices, so it names a generic class and a direction rather than a
|
|
47
|
+
# model. Turning those into text would label every controller on the machine
|
|
48
|
+
# as made by Microsoft — a fact about the code table, presented as a fact
|
|
49
|
+
# about the hardware.
|
|
50
|
+
#
|
|
51
|
+
# So both are nil. A caller filtering by manufacturer finds nothing and can
|
|
52
|
+
# see that it found nothing, which is the truthful outcome; a caller offered
|
|
53
|
+
# an empty string or an invented name would match, or not match, for reasons
|
|
54
|
+
# that are not real.
|
|
55
|
+
#
|
|
56
|
+
# @api public
|
|
57
|
+
module Device
|
|
58
|
+
# Methods on {Input} and {Output} themselves.
|
|
59
|
+
#
|
|
60
|
+
# @api public
|
|
61
|
+
module ClassMethods
|
|
62
|
+
# Every port of this direction.
|
|
63
|
+
#
|
|
64
|
+
# WinMM is asked afresh on every call, so a port that appeared or went
|
|
65
|
+
# away since the last one is reflected. A port that is still there comes
|
|
66
|
+
# back as the same object as before; see {Device.enumerate}.
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Input>, Array<Output>]
|
|
69
|
+
def all
|
|
70
|
+
Device.all_by_type[direction]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The first port of this direction, or nil if there are none.
|
|
74
|
+
# @return [Input, Output, nil]
|
|
75
|
+
def first
|
|
76
|
+
all.first
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The last port of this direction, or nil if there are none.
|
|
80
|
+
# @return [Input, Output, nil]
|
|
81
|
+
def last
|
|
82
|
+
all.last
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Methods on an individual port.
|
|
87
|
+
#
|
|
88
|
+
# @api public
|
|
89
|
+
module InstanceMethods
|
|
90
|
+
# @!attribute [r] id
|
|
91
|
+
# @return [Integer] the port's WinMM index **within its direction**;
|
|
92
|
+
# see the note on identity in {Device}
|
|
93
|
+
# @!attribute [r] name
|
|
94
|
+
# @return [String] the port's name, as WinMM reports it, truncated to
|
|
95
|
+
# 31 characters. **Not unique**: see the note on names in {Device}.
|
|
96
|
+
# @!attribute [r] enabled
|
|
97
|
+
# @return [Boolean] whether the port is currently open
|
|
98
|
+
attr_reader :id, :name, :enabled
|
|
99
|
+
|
|
100
|
+
# @!method enabled?
|
|
101
|
+
# @return [Boolean] alias for {#enabled}
|
|
102
|
+
alias enabled? enabled
|
|
103
|
+
|
|
104
|
+
# @param id [Integer] the port's WinMM index within its direction
|
|
105
|
+
# @param name [String] the port's name as reported by WinMM
|
|
106
|
+
# @api private
|
|
107
|
+
def initialize(id, name)
|
|
108
|
+
@id = id
|
|
109
|
+
@name = name
|
|
110
|
+
@enabled = false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The port's direction.
|
|
114
|
+
#
|
|
115
|
+
# @return [Symbol] `:input` or `:output`
|
|
116
|
+
def type
|
|
117
|
+
self.class.direction
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Who made the device.
|
|
121
|
+
#
|
|
122
|
+
# Always nil on Windows. See the note in {Device} for why this is not
|
|
123
|
+
# derived from `wMid`.
|
|
124
|
+
#
|
|
125
|
+
# @return [nil]
|
|
126
|
+
def manufacturer
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The device model.
|
|
131
|
+
#
|
|
132
|
+
# Always nil on Windows, for the same reason as {#manufacturer}.
|
|
133
|
+
#
|
|
134
|
+
# @return [nil]
|
|
135
|
+
def model
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The name to show a person choosing a port.
|
|
140
|
+
#
|
|
141
|
+
# This is the port name unchanged. On macOS the display name is built as
|
|
142
|
+
# "manufacturer model (name)", which here would render as a name wrapped
|
|
143
|
+
# in the punctuation of two absent fields.
|
|
144
|
+
#
|
|
145
|
+
# @return [String]
|
|
146
|
+
def display_name
|
|
147
|
+
@name
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Opens the port.
|
|
151
|
+
#
|
|
152
|
+
# Opening twice is not an error and does nothing the second time, which
|
|
153
|
+
# is what `midi-communications` relies on when it opens a port a caller
|
|
154
|
+
# may already hold.
|
|
155
|
+
#
|
|
156
|
+
# @yield [self] if a block is given, the port is closed when it returns
|
|
157
|
+
# @return [self]
|
|
158
|
+
# @raise [Error] if WinMM refuses to open the port
|
|
159
|
+
def open
|
|
160
|
+
unless @enabled
|
|
161
|
+
connect
|
|
162
|
+
@enabled = true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if block_given?
|
|
166
|
+
begin
|
|
167
|
+
yield self
|
|
168
|
+
ensure
|
|
169
|
+
close
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
alias enable open
|
|
176
|
+
alias start open
|
|
177
|
+
|
|
178
|
+
# Closes the port.
|
|
179
|
+
#
|
|
180
|
+
# @return [Boolean] true if it was open, false if it already was not
|
|
181
|
+
def close
|
|
182
|
+
return false unless @enabled
|
|
183
|
+
|
|
184
|
+
disconnect
|
|
185
|
+
@enabled = false
|
|
186
|
+
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @return [String]
|
|
191
|
+
def to_s
|
|
192
|
+
"#{self.class.name.split('::').last} #{@id}: #{@name}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Wrappers already handed out, so that a port that is still there is still
|
|
197
|
+
# the same object. See {Device.enumerate}.
|
|
198
|
+
@ports = { input: [], output: [] }
|
|
199
|
+
@ports_semaphore = Mutex.new
|
|
200
|
+
|
|
201
|
+
module_function
|
|
202
|
+
|
|
203
|
+
# Every port, grouped by direction.
|
|
204
|
+
#
|
|
205
|
+
# This is the shape `midi-communications` asks its platform adapters for.
|
|
206
|
+
#
|
|
207
|
+
# @return [Hash{Symbol => Array<Input>, Array<Output>}] with `:input` and
|
|
208
|
+
# `:output` keys
|
|
209
|
+
#
|
|
210
|
+
# @example
|
|
211
|
+
# MIDICommunicationsWindows::Device.all_by_type[:output].each { |o| puts o.name }
|
|
212
|
+
def all_by_type
|
|
213
|
+
{ input: inputs, output: outputs }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Every port, of both directions.
|
|
217
|
+
# @return [Array<Input, Output>]
|
|
218
|
+
def all
|
|
219
|
+
all_by_type.values.flatten
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Every input port WinMM offers.
|
|
223
|
+
# @return [Array<Input>]
|
|
224
|
+
# @raise [Error] if WinMM refuses to describe a port it has just counted
|
|
225
|
+
def inputs
|
|
226
|
+
enumerate(:input, API.midiInGetNumDevs, Input)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Every output port WinMM offers.
|
|
230
|
+
#
|
|
231
|
+
# Note that this does not include the MIDI Mapper, which WinMM addresses by
|
|
232
|
+
# the reserved id `-1` and does not count among its devices.
|
|
233
|
+
#
|
|
234
|
+
# @return [Array<Output>]
|
|
235
|
+
# @raise [Error] if WinMM refuses to describe a port it has just counted
|
|
236
|
+
def outputs
|
|
237
|
+
enumerate(:output, API.midiOutGetNumDevs, Output)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Asks WinMM what ports exist, reusing the wrapper for each one that was
|
|
241
|
+
# already there.
|
|
242
|
+
#
|
|
243
|
+
# The list is read afresh every time, so a port that appeared or went away
|
|
244
|
+
# is reflected — but a port that is still present comes back as the object
|
|
245
|
+
# it came back as last time. Otherwise `Input.first.open` and
|
|
246
|
+
# `Input.first.gets` would be two different objects, and the second would
|
|
247
|
+
# not be open.
|
|
248
|
+
#
|
|
249
|
+
# "Still present" means the same index reporting the same name. The index
|
|
250
|
+
# alone is not enough: WinMM renumbers, so index 1 after a device is
|
|
251
|
+
# unplugged may be a different port than index 1 before, and handing back a
|
|
252
|
+
# wrapper holding a handle to the old one would be worse than making a new
|
|
253
|
+
# object.
|
|
254
|
+
#
|
|
255
|
+
# @param direction [Symbol] `:input` or `:output`
|
|
256
|
+
# @param count [Integer] how many ports WinMM reports for that direction
|
|
257
|
+
# @param klass [Class] {Input} or {Output}
|
|
258
|
+
# @return [Array<Input>, Array<Output>]
|
|
259
|
+
# @api private
|
|
260
|
+
def enumerate(direction, count, klass)
|
|
261
|
+
@ports_semaphore.synchronize do
|
|
262
|
+
known = @ports[direction].to_h { |port| [port.id, port] }
|
|
263
|
+
|
|
264
|
+
@ports[direction] = Array.new(count) do |id|
|
|
265
|
+
name = direction == :input ? input_name(id) : output_name(id)
|
|
266
|
+
previous = known[id]
|
|
267
|
+
|
|
268
|
+
previous && previous.name == name ? previous : klass.new(id, name)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# The name of an input port.
|
|
274
|
+
#
|
|
275
|
+
# @param id [Integer] the port's WinMM index
|
|
276
|
+
# @return [String]
|
|
277
|
+
# @api private
|
|
278
|
+
def input_name(id)
|
|
279
|
+
capabilities = API::MIDIInCaps.new
|
|
280
|
+
|
|
281
|
+
API.check!(API.midiInGetDevCapsW(id, capabilities, API::MIDIInCaps.size),
|
|
282
|
+
:midiInGetDevCapsW, :input)
|
|
283
|
+
|
|
284
|
+
port_name(capabilities)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# The name of an output port.
|
|
288
|
+
#
|
|
289
|
+
# @param id [Integer] the port's WinMM index
|
|
290
|
+
# @return [String]
|
|
291
|
+
# @api private
|
|
292
|
+
def output_name(id)
|
|
293
|
+
capabilities = API::MIDIOutCaps.new
|
|
294
|
+
|
|
295
|
+
API.check!(API.midiOutGetDevCapsW(id, capabilities, API::MIDIOutCaps.size),
|
|
296
|
+
:midiOutGetDevCapsW, :output)
|
|
297
|
+
|
|
298
|
+
port_name(capabilities)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Reads `szPname` out of a capabilities structure.
|
|
302
|
+
#
|
|
303
|
+
# @param capabilities [API::MIDIInCaps, API::MIDIOutCaps]
|
|
304
|
+
# @return [String]
|
|
305
|
+
# @api private
|
|
306
|
+
def port_name(capabilities)
|
|
307
|
+
API.read_wide_string(capabilities.to_ptr + capabilities.offset_of(:szPname), API::MAXPNAMELEN)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|