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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.version +6 -0
  4. data/.yardoc/checksums +8 -0
  5. data/.yardoc/complete +0 -0
  6. data/.yardoc/object_types +0 -0
  7. data/.yardoc/objects/root.dat +0 -0
  8. data/.yardoc/proxy_types +0 -0
  9. data/.yardopts +8 -0
  10. data/Gemfile +3 -0
  11. data/LICENSE +165 -0
  12. data/README.md +176 -0
  13. data/Rakefile +13 -0
  14. data/doc/MIDICommunicationsWindows/API/MIDIHdr.html +144 -0
  15. data/doc/MIDICommunicationsWindows/API/MIDIInCaps.html +142 -0
  16. data/doc/MIDICommunicationsWindows/API/MIDIOutCaps.html +140 -0
  17. data/doc/MIDICommunicationsWindows/API/MSG.html +143 -0
  18. data/doc/MIDICommunicationsWindows/API.html +981 -0
  19. data/doc/MIDICommunicationsWindows/Device/ClassMethods.html +365 -0
  20. data/doc/MIDICommunicationsWindows/Device/InstanceMethods.html +1096 -0
  21. data/doc/MIDICommunicationsWindows/Device.html +988 -0
  22. data/doc/MIDICommunicationsWindows/Error.html +438 -0
  23. data/doc/MIDICommunicationsWindows/Input.html +1554 -0
  24. data/doc/MIDICommunicationsWindows/Message.html +700 -0
  25. data/doc/MIDICommunicationsWindows/Output.html +1697 -0
  26. data/doc/MIDICommunicationsWindows/TypeConversion.html +369 -0
  27. data/doc/MIDICommunicationsWindows.html +210 -0
  28. data/doc/_index.html +262 -0
  29. data/doc/class_list.html +54 -0
  30. data/doc/css/common.css +1 -0
  31. data/doc/css/full_list.css +206 -0
  32. data/doc/css/style.css +1089 -0
  33. data/doc/file.README.html +240 -0
  34. data/doc/file.testing-on-windows.html +253 -0
  35. data/doc/file_list.html +64 -0
  36. data/doc/frames.html +22 -0
  37. data/doc/index.html +240 -0
  38. data/doc/js/app.js +801 -0
  39. data/doc/js/full_list.js +334 -0
  40. data/doc/js/jquery.js +4 -0
  41. data/doc/method_list.html +382 -0
  42. data/doc/top-level-namespace.html +112 -0
  43. data/docs/testing-on-windows.md +222 -0
  44. data/examples/input.rb +27 -0
  45. data/examples/list_ports.rb +17 -0
  46. data/examples/output.rb +22 -0
  47. data/examples/sysex_output.rb +15 -0
  48. data/lib/midi-communications-windows/api.rb +340 -0
  49. data/lib/midi-communications-windows/device.rb +310 -0
  50. data/lib/midi-communications-windows/input.rb +486 -0
  51. data/lib/midi-communications-windows/message.rb +133 -0
  52. data/lib/midi-communications-windows/output.rb +206 -0
  53. data/lib/midi-communications-windows/type_conversion.rb +20 -0
  54. data/lib/midi-communications-windows/version.rb +4 -0
  55. data/lib/midi-communications-windows.rb +76 -0
  56. data/midi-communications-windows.gemspec +32 -0
  57. 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