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,486 @@
|
|
|
1
|
+
module MIDICommunicationsWindows
|
|
2
|
+
# A MIDI input port: somewhere messages arrive from.
|
|
3
|
+
#
|
|
4
|
+
# @example Read from the first input
|
|
5
|
+
# input = MIDICommunicationsWindows::Input.first
|
|
6
|
+
# input.open
|
|
7
|
+
# input.gets
|
|
8
|
+
# # => [{ data: [144, 60, 100], timestamp: 1789123456.789 }]
|
|
9
|
+
#
|
|
10
|
+
# ## `gets` blocks, and that is part of the contract
|
|
11
|
+
#
|
|
12
|
+
# {#gets} waits until at least one message has arrived and then returns
|
|
13
|
+
# everything that accumulated. It does not return an empty array.
|
|
14
|
+
#
|
|
15
|
+
# This is not an incidental property. `Musa::Clock::InputMidiClock` reads its
|
|
16
|
+
# MIDI Clock in a loop with no delay of its own, relying on `gets` to be where
|
|
17
|
+
# the thread waits. An implementation that returned immediately would turn
|
|
18
|
+
# that loop into a spin on a full core, and would do it silently — the notes
|
|
19
|
+
# would still play.
|
|
20
|
+
#
|
|
21
|
+
# @see Output for sending
|
|
22
|
+
#
|
|
23
|
+
# @api public
|
|
24
|
+
class Input
|
|
25
|
+
extend Device::ClassMethods
|
|
26
|
+
include Device::InstanceMethods
|
|
27
|
+
|
|
28
|
+
# @return [Symbol] `:input`
|
|
29
|
+
def self.direction
|
|
30
|
+
:input
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param id [Integer] the port's WinMM index among inputs
|
|
34
|
+
# @param name [String] the port's name as reported by WinMM
|
|
35
|
+
# @api private
|
|
36
|
+
def initialize(id, name)
|
|
37
|
+
super
|
|
38
|
+
|
|
39
|
+
@queue = Queue.new
|
|
40
|
+
@sysex = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Reads the messages that have arrived.
|
|
44
|
+
#
|
|
45
|
+
# Blocks until there is at least one. See the note on the class.
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<Hash>] each with `:data`, an array of numeric bytes, and
|
|
48
|
+
# `:timestamp`, a Float of seconds
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# input.gets
|
|
52
|
+
# # => [{ data: [248], timestamp: 1789123456.789 },
|
|
53
|
+
# # { data: [144, 60, 100], timestamp: 1789123456.812 }]
|
|
54
|
+
def gets
|
|
55
|
+
# Queue#pop is where the thread waits, and it is the whole of the waiting
|
|
56
|
+
# mechanism on purpose. The obvious alternative — test whether the queue
|
|
57
|
+
# is empty, then sleep, and have the producer Thread#run the sleeper — has
|
|
58
|
+
# a window between the test and the sleep in which a message can arrive
|
|
59
|
+
# and its wake-up be delivered to a thread that is not sleeping yet. The
|
|
60
|
+
# reader then sleeps forever, and the thread it happens on is the one
|
|
61
|
+
# carrying the MIDI clock. Queue does the same job with no such window.
|
|
62
|
+
messages = [@queue.pop]
|
|
63
|
+
messages << @queue.pop until @queue.empty?
|
|
64
|
+
|
|
65
|
+
messages
|
|
66
|
+
end
|
|
67
|
+
alias read gets
|
|
68
|
+
|
|
69
|
+
# Reads the messages that have arrived, with their data as hex strings.
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<Hash>] as {#gets}, but `:data` is a String
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# input.gets_s
|
|
75
|
+
# # => [{ data: 'F8', timestamp: 1789123456.789 }]
|
|
76
|
+
def gets_s
|
|
77
|
+
gets.each do |message|
|
|
78
|
+
message[:data] = TypeConversion.numeric_bytes_to_hex_string(message[:data])
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
alias gets_bytestr gets_s
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Accepts one message from whichever mechanism is delivering them, waking
|
|
86
|
+
# whatever thread is waiting in {#gets}.
|
|
87
|
+
#
|
|
88
|
+
# Both candidate delivery mechanisms end here, which is the point: they
|
|
89
|
+
# differ only in how bytes reach this method.
|
|
90
|
+
#
|
|
91
|
+
# @param bytes [Array<Integer>] one complete message
|
|
92
|
+
# @param timestamp [Float] seconds, as `Time.now.to_f`
|
|
93
|
+
# @return [void]
|
|
94
|
+
# @api private
|
|
95
|
+
def enqueue(bytes, timestamp)
|
|
96
|
+
@queue << { data: bytes, timestamp: timestamp }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Accepts a fragment of System Exclusive, emitting the message once it ends.
|
|
100
|
+
#
|
|
101
|
+
# WinMM fills a buffer at a time, so a System Exclusive message longer than
|
|
102
|
+
# the buffer arrives in pieces. They are collected until the 0xF7 that ends
|
|
103
|
+
# the message, and only then handed on: a caller asking for messages should
|
|
104
|
+
# get messages, not the arbitrary lengths a buffer size happened to impose.
|
|
105
|
+
#
|
|
106
|
+
# Measured: a 200-byte message through 128-byte buffers arrives as 128 then
|
|
107
|
+
# 72, contiguous and exact. WinMM does not require a buffer large enough for
|
|
108
|
+
# the whole message.
|
|
109
|
+
#
|
|
110
|
+
# Empty fragments must never reach here. Closing a port makes `midiInReset`
|
|
111
|
+
# hand back every buffer that was queued and unused, each reported with
|
|
112
|
+
# `dwBytesRecorded` of zero; passing those on would emit a stray empty
|
|
113
|
+
# message, or worse, terminate a partial one.
|
|
114
|
+
#
|
|
115
|
+
# @param bytes [Array<Integer>] one buffer's worth
|
|
116
|
+
# @param timestamp [Float] seconds
|
|
117
|
+
# @return [void]
|
|
118
|
+
# @api private
|
|
119
|
+
def enqueue_sysex(bytes, timestamp)
|
|
120
|
+
@sysex.concat(bytes)
|
|
121
|
+
|
|
122
|
+
return unless @sysex.last == 0xF7
|
|
123
|
+
|
|
124
|
+
enqueue(@sysex.dup, timestamp)
|
|
125
|
+
@sysex.clear
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Opens the port and starts delivery.
|
|
129
|
+
#
|
|
130
|
+
# @return [void]
|
|
131
|
+
# @raise [Error]
|
|
132
|
+
# @api private
|
|
133
|
+
def connect
|
|
134
|
+
start_receiving
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Stops delivery and closes the port.
|
|
138
|
+
#
|
|
139
|
+
# @return [void]
|
|
140
|
+
# @raise [Error]
|
|
141
|
+
# @api private
|
|
142
|
+
def disconnect
|
|
143
|
+
stop_receiving
|
|
144
|
+
|
|
145
|
+
# A System Exclusive message interrupted by the close is not going to be
|
|
146
|
+
# completed by anything, and leaving its bytes in the buffer would prepend
|
|
147
|
+
# them to the first message of the next session.
|
|
148
|
+
@sysex.clear
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
# Delivery: CALLBACK_THREAD
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
#
|
|
155
|
+
# WinMM offers two ways to hand a client its input, and both were measured
|
|
156
|
+
# working against a system loopback on Windows 11 25H2. This is the one that
|
|
157
|
+
# was chosen, and why.
|
|
158
|
+
#
|
|
159
|
+
# Under `CALLBACK_FUNCTION` the driver calls an FFI function pointer from a
|
|
160
|
+
# thread of its own. It is what RtMidi, PortMidi and midi-winmm all do. But
|
|
161
|
+
# a callback arriving on a thread Ruby does not own runs on a thread FFI
|
|
162
|
+
# supplies while the driver's thread waits inside the callback, and if
|
|
163
|
+
# anything else in the process is holding the GVL inside a winmm call, the
|
|
164
|
+
# two deadlock. What that produces is not an exception: it is duplicate
|
|
165
|
+
# deliveries, a second or two late, with the send reporting an error. See
|
|
166
|
+
# the note in {API} for the measurement and its controls. Declaring every
|
|
167
|
+
# winmm call `blocking: true` avoids it — but that is a rule someone has to
|
|
168
|
+
# keep obeying, and the failure it prevents is silent.
|
|
169
|
+
#
|
|
170
|
+
# Under `CALLBACK_THREAD` WinMM posts to the message queue of a thread this
|
|
171
|
+
# library owns. The driver's thread never enters Ruby, so a Ruby callback
|
|
172
|
+
# can never be waiting on a GVL that a sending thread holds: the failure
|
|
173
|
+
# above stops being something to guard against and becomes something that
|
|
174
|
+
# cannot happen. The reader thread owns the port and may call winmm freely,
|
|
175
|
+
# which is what makes {#requeue} ordinary code rather than the thing
|
|
176
|
+
# Microsoft's documentation prohibits inside `midiInProc`.
|
|
177
|
+
#
|
|
178
|
+
# The price is `dwParam2`, the driver's own millisecond count, which is not
|
|
179
|
+
# delivered this way. Nothing consumes it: `InputMidiClock` reads only
|
|
180
|
+
# `message[:data]`, `MIDIRecorder` stamps with the sequencer's position, and
|
|
181
|
+
# the macOS layer already takes `Time.now.to_f` from inside its own callback
|
|
182
|
+
# — arrival at Ruby, not a driver stamp. So the timestamp is taken here the
|
|
183
|
+
# same way, and both platforms mean the same thing by it.
|
|
184
|
+
|
|
185
|
+
# How many buffers to keep queued for System Exclusive, and how large.
|
|
186
|
+
#
|
|
187
|
+
# WinMM fills whatever it is given and splits a message across buffers, so
|
|
188
|
+
# the size is not a limit on message length; it only decides how often the
|
|
189
|
+
# reader is woken. Keeping several queued means the next fragment has
|
|
190
|
+
# somewhere to go while this one is being copied out.
|
|
191
|
+
BUFFER_COUNT = 4
|
|
192
|
+
BUFFER_SIZE = 1024
|
|
193
|
+
|
|
194
|
+
# How long to wait for `midiInReset` to hand the buffers back, in seconds.
|
|
195
|
+
RESET_TIMEOUT = 1.0
|
|
196
|
+
|
|
197
|
+
# Opens the port and starts delivering, from a thread of our own.
|
|
198
|
+
#
|
|
199
|
+
# The port is opened *inside* the reader thread because `midiInOpen` is
|
|
200
|
+
# given that thread's id and posts to that thread's queue. The caller waits
|
|
201
|
+
# here until the thread reports that it opened, so that a failure to open
|
|
202
|
+
# reaches whoever called {#open} instead of disappearing into a thread.
|
|
203
|
+
#
|
|
204
|
+
# @return [void]
|
|
205
|
+
# @raise [Error]
|
|
206
|
+
# @api private
|
|
207
|
+
def start_receiving
|
|
208
|
+
@returned = Queue.new
|
|
209
|
+
@started = Queue.new
|
|
210
|
+
|
|
211
|
+
@reader = Thread.new { receive_loop }
|
|
212
|
+
|
|
213
|
+
failure = @started.pop
|
|
214
|
+
raise failure if failure
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Stops delivery and closes the port.
|
|
218
|
+
#
|
|
219
|
+
# The order matters, and one step is easy to leave out. `midiInReset` hands
|
|
220
|
+
# every queued buffer back **as messages in the reader thread's queue**, so
|
|
221
|
+
# the thread has to keep pumping until they arrive. Posting `WM_QUIT` first
|
|
222
|
+
# loses them, and then `midiInUnprepareHeader` is called on buffers the
|
|
223
|
+
# driver has not released.
|
|
224
|
+
#
|
|
225
|
+
# A thread blocked in a `blocking: true` call cannot be interrupted by
|
|
226
|
+
# `Thread#raise`, so `WM_QUIT` is the only way to end the loop.
|
|
227
|
+
#
|
|
228
|
+
# @return [void]
|
|
229
|
+
# @raise [Error]
|
|
230
|
+
# @api private
|
|
231
|
+
def stop_receiving
|
|
232
|
+
API.check!(API.midiInStop(@handle), :midiInStop, :input)
|
|
233
|
+
API.check!(API.midiInReset(@handle), :midiInReset, :input)
|
|
234
|
+
|
|
235
|
+
await_returned_buffers
|
|
236
|
+
|
|
237
|
+
API.PostThreadMessageW(@thread_id, API::WM_QUIT, 0, 0)
|
|
238
|
+
@reader.join
|
|
239
|
+
@reader = nil
|
|
240
|
+
|
|
241
|
+
release_buffers
|
|
242
|
+
|
|
243
|
+
API.check!(API.midiInClose(@handle), :midiInClose, :input)
|
|
244
|
+
@handle = nil
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# The reader thread: owns the port, and is the only thread that touches it
|
|
248
|
+
# while it is open.
|
|
249
|
+
#
|
|
250
|
+
# @return [void]
|
|
251
|
+
# @api private
|
|
252
|
+
def receive_loop
|
|
253
|
+
message = API::MSG.new
|
|
254
|
+
|
|
255
|
+
# A thread has no message queue until it asks for one, and WinMM cannot
|
|
256
|
+
# post to a queue that does not exist: without this, `midiInOpen` is given
|
|
257
|
+
# a thread id that rejects messages and the first notifications are lost.
|
|
258
|
+
API.PeekMessageW(message, nil, 0, 0, API::PM_NOREMOVE)
|
|
259
|
+
|
|
260
|
+
begin
|
|
261
|
+
open_port
|
|
262
|
+
prepare_buffers
|
|
263
|
+
API.check!(API.midiInStart(@handle), :midiInStart, :input)
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
# Whatever was opened before the failure has to be given back here:
|
|
266
|
+
# nobody else can, because {#open} will not mark the port enabled and
|
|
267
|
+
# so {#close} will never run.
|
|
268
|
+
abandon_port
|
|
269
|
+
@started << e
|
|
270
|
+
return
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
@started << nil
|
|
274
|
+
|
|
275
|
+
pump(message)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Releases whatever an interrupted {#receive_loop} had managed to take.
|
|
279
|
+
#
|
|
280
|
+
# @return [void]
|
|
281
|
+
# @api private
|
|
282
|
+
def abandon_port
|
|
283
|
+
return if @handle.nil?
|
|
284
|
+
|
|
285
|
+
API.midiInReset(@handle)
|
|
286
|
+
@buffers&.each { |buffer| API.midiInUnprepareHeader(@handle, buffer[:header], API::MIDIHdr.size) }
|
|
287
|
+
API.midiInClose(@handle)
|
|
288
|
+
|
|
289
|
+
@buffers = nil
|
|
290
|
+
@handle = nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# @return [void]
|
|
294
|
+
# @raise [Error]
|
|
295
|
+
# @api private
|
|
296
|
+
def open_port
|
|
297
|
+
@thread_id = API.GetCurrentThreadId
|
|
298
|
+
|
|
299
|
+
handle_pointer = FFI::MemoryPointer.new(:uintptr_t)
|
|
300
|
+
|
|
301
|
+
API.check!(API.midiInOpen(handle_pointer, @id, @thread_id, 0, API::CALLBACK_THREAD),
|
|
302
|
+
:midiInOpen, :input)
|
|
303
|
+
|
|
304
|
+
@handle = handle_pointer.read(:uintptr_t)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Hands WinMM the buffers it will fill with System Exclusive.
|
|
308
|
+
#
|
|
309
|
+
# Both the header and the memory it points at are kept, because letting
|
|
310
|
+
# either be collected while the driver still holds a pointer to it is not a
|
|
311
|
+
# Ruby error but a corrupted process.
|
|
312
|
+
#
|
|
313
|
+
# @return [void]
|
|
314
|
+
# @raise [Error]
|
|
315
|
+
# @api private
|
|
316
|
+
def prepare_buffers
|
|
317
|
+
@buffers = Array.new(BUFFER_COUNT) do
|
|
318
|
+
memory = FFI::MemoryPointer.new(:uint8, BUFFER_SIZE)
|
|
319
|
+
|
|
320
|
+
header = API::MIDIHdr.new
|
|
321
|
+
header[:lpData] = memory
|
|
322
|
+
header[:dwBufferLength] = BUFFER_SIZE
|
|
323
|
+
|
|
324
|
+
API.check!(API.midiInPrepareHeader(@handle, header, API::MIDIHdr.size),
|
|
325
|
+
:midiInPrepareHeader, :input)
|
|
326
|
+
API.check!(API.midiInAddBuffer(@handle, header, API::MIDIHdr.size),
|
|
327
|
+
:midiInAddBuffer, :input)
|
|
328
|
+
|
|
329
|
+
{ header: header, memory: memory }
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Reads the thread's queue until `WM_QUIT`.
|
|
334
|
+
#
|
|
335
|
+
# `GetMessageW` returns above zero for a message, zero for `WM_QUIT` and -1
|
|
336
|
+
# for an error, so the loop ends on anything that is not positive.
|
|
337
|
+
#
|
|
338
|
+
# Only the two MIDI notifications are acted on. The queue carries other
|
|
339
|
+
# traffic — see {API::WM_USER} — and treating everything that arrives as
|
|
340
|
+
# MIDI would read those as messages.
|
|
341
|
+
#
|
|
342
|
+
# @param message [API::MSG] reused for every read
|
|
343
|
+
# @return [void]
|
|
344
|
+
# @api private
|
|
345
|
+
def pump(message)
|
|
346
|
+
loop do
|
|
347
|
+
break unless API.GetMessageW(message, nil, 0, 0).positive?
|
|
348
|
+
|
|
349
|
+
begin
|
|
350
|
+
case message[:message]
|
|
351
|
+
when API::MM_MIM_DATA then deliver_short(message[:lParam])
|
|
352
|
+
when API::MM_MIM_LONGDATA then deliver_long(message[:lParam])
|
|
353
|
+
when API::MM_MIM_LONGERROR then discard_long(message[:lParam])
|
|
354
|
+
when API::MM_MIM_ERROR then report_invalid(message[:lParam])
|
|
355
|
+
end
|
|
356
|
+
rescue StandardError => e
|
|
357
|
+
# One bad message must not end the loop. A reader thread that dies
|
|
358
|
+
# here takes every later message with it, and leaves gets blocked
|
|
359
|
+
# forever on a queue nothing will ever fill again -- which looks like
|
|
360
|
+
# a hung program, not like an error.
|
|
361
|
+
warn "[midi-communications-windows] #{@name}: #{e.class}: #{e.message}"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# A short message: `lParam` is the message itself, packed into a word.
|
|
367
|
+
#
|
|
368
|
+
# @param word [Integer]
|
|
369
|
+
# @return [void]
|
|
370
|
+
# @api private
|
|
371
|
+
def deliver_short(word)
|
|
372
|
+
enqueue(Message.unpack(word & 0xFFFF_FFFF), Time.now.to_f)
|
|
373
|
+
rescue ArgumentError => e
|
|
374
|
+
# WinMM should never deliver a word this cannot size. If it does, the
|
|
375
|
+
# port keeps working: one message nobody can read is a smaller loss than
|
|
376
|
+
# a reader thread that dies and takes every later message with it.
|
|
377
|
+
warn "[midi-communications-windows] ignoring an unreadable message on #{@name}: #{e.message}"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# A System Exclusive fragment: `lParam` points at the buffer WinMM filled.
|
|
381
|
+
#
|
|
382
|
+
# @param header_pointer [Integer] address of a `MIDIHDR`
|
|
383
|
+
# @return [void]
|
|
384
|
+
# @api private
|
|
385
|
+
def deliver_long(header_pointer)
|
|
386
|
+
header = API::MIDIHdr.new(FFI::Pointer.new(header_pointer))
|
|
387
|
+
recorded = header[:dwBytesRecorded]
|
|
388
|
+
|
|
389
|
+
# An empty one is a buffer coming back from midiInReset, not a fragment.
|
|
390
|
+
# The two are indistinguishable except by this field, and passing one on
|
|
391
|
+
# would either emit a message of nothing or truncate a partial one.
|
|
392
|
+
if recorded.zero?
|
|
393
|
+
@returned << header_pointer
|
|
394
|
+
return
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
enqueue_sysex(header[:lpData].read_array_of_uint8(recorded), Time.now.to_f)
|
|
398
|
+
|
|
399
|
+
requeue(header)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# A System Exclusive buffer WinMM could not fill: the data in it is invalid.
|
|
403
|
+
#
|
|
404
|
+
# This has never been observed — sending a message larger than the whole
|
|
405
|
+
# buffer queue did not provoke one — and it is handled anyway because of the
|
|
406
|
+
# shape its failure would take rather than its likelihood. A fragment lost
|
|
407
|
+
# mid-message means {#enqueue_sysex} never sees the 0xF7 that ends it, so it
|
|
408
|
+
# would hold every later fragment too and {#gets} would wait forever. That
|
|
409
|
+
# reads as a hung program, not as a corrupt message.
|
|
410
|
+
#
|
|
411
|
+
# So the partial message is abandoned, loudly, and the buffer goes back into
|
|
412
|
+
# the queue. One message is lost, which is what actually happened.
|
|
413
|
+
#
|
|
414
|
+
# @param header_pointer [Integer] address of a `MIDIHDR`
|
|
415
|
+
# @return [void]
|
|
416
|
+
# @api private
|
|
417
|
+
def discard_long(header_pointer)
|
|
418
|
+
header = API::MIDIHdr.new(FFI::Pointer.new(header_pointer))
|
|
419
|
+
|
|
420
|
+
unless @sysex.empty?
|
|
421
|
+
warn "[midi-communications-windows] #{@name}: discarding #{@sysex.size} bytes of an " \
|
|
422
|
+
'incomplete System Exclusive message after a device error'
|
|
423
|
+
@sysex.clear
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
requeue(header) unless header[:dwBytesRecorded].zero?
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Invalid MIDI data arrived; `lParam` holds what WinMM made of it.
|
|
430
|
+
#
|
|
431
|
+
# There is nothing to hand on and nothing to recycle, but staying silent
|
|
432
|
+
# about a port emitting bytes that are not MIDI helps nobody.
|
|
433
|
+
#
|
|
434
|
+
# @param word [Integer]
|
|
435
|
+
# @return [void]
|
|
436
|
+
# @api private
|
|
437
|
+
def report_invalid(word)
|
|
438
|
+
warn format('[midi-communications-windows] %s: ignoring invalid MIDI data 0x%08X', @name, word & 0xFFFF_FFFF)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Gives a buffer back to WinMM so it can be filled again.
|
|
442
|
+
#
|
|
443
|
+
# This is the call that decided the delivery mechanism: it happens on the
|
|
444
|
+
# reader thread, inside its own message loop, which is legal here and is
|
|
445
|
+
# what Microsoft's documentation forbids inside `midiInProc`. The queue is
|
|
446
|
+
# first in, first out, so a recycled buffer goes to the back and is used
|
|
447
|
+
# again once the ones ahead of it have been.
|
|
448
|
+
#
|
|
449
|
+
# @param header [API::MIDIHdr]
|
|
450
|
+
# @return [void]
|
|
451
|
+
# @raise [Error]
|
|
452
|
+
# @api private
|
|
453
|
+
def requeue(header)
|
|
454
|
+
header[:dwBytesRecorded] = 0
|
|
455
|
+
header[:dwFlags] &= ~API::MHDR_DONE & 0xFFFF_FFFF
|
|
456
|
+
|
|
457
|
+
API.check!(API.midiInAddBuffer(@handle, header, API::MIDIHdr.size), :midiInAddBuffer, :input)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Waits for `midiInReset` to return the queued buffers.
|
|
461
|
+
#
|
|
462
|
+
# Bounded, because closing a port must end whether or not the driver plays
|
|
463
|
+
# its part; a device that has been unplugged will not return anything.
|
|
464
|
+
#
|
|
465
|
+
# @return [void]
|
|
466
|
+
# @api private
|
|
467
|
+
def await_returned_buffers
|
|
468
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + RESET_TIMEOUT
|
|
469
|
+
|
|
470
|
+
while @returned.size < @buffers.size &&
|
|
471
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
472
|
+
sleep 0.001
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# @return [void]
|
|
477
|
+
# @api private
|
|
478
|
+
def release_buffers
|
|
479
|
+
@buffers.each do |buffer|
|
|
480
|
+
API.midiInUnprepareHeader(@handle, buffer[:header], API::MIDIHdr.size)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
@buffers = nil
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module MIDICommunicationsWindows
|
|
2
|
+
# Conversion between MIDI bytes and the packed 32-bit word WinMM uses for
|
|
3
|
+
# short messages.
|
|
4
|
+
#
|
|
5
|
+
# WinMM does not pass short messages as byte strings. It packs them into a
|
|
6
|
+
# `DWORD`: the status byte in the low-order byte, the first data byte next,
|
|
7
|
+
# the second data byte after that, and the high-order byte unused. The same
|
|
8
|
+
# packing is used in both directions — as `dwMsg` for `midiOutShortMsg`, and
|
|
9
|
+
# as `dwParam1` in the notification a client receives for input.
|
|
10
|
+
#
|
|
11
|
+
# ## Why the length matters
|
|
12
|
+
#
|
|
13
|
+
# A packed word carries no length. Nothing distinguishes a Clock, which is one
|
|
14
|
+
# byte, from a Note On whose two data bytes happen to be zero: both arrive as
|
|
15
|
+
# a word whose upper three bytes are zero. The length has to be derived from
|
|
16
|
+
# the status byte, and getting it wrong is not a cosmetic problem — a Clock
|
|
17
|
+
# delivered as `[0xF8, 0, 0]` is not a MIDI Clock message, and a parser
|
|
18
|
+
# reading it will either reject it or invent two events that were never sent.
|
|
19
|
+
# In MusaDSL that is the difference between a piece that follows the DAW's
|
|
20
|
+
# tempo and one that does not start at all.
|
|
21
|
+
#
|
|
22
|
+
# ## Why this file has no FFI in it
|
|
23
|
+
#
|
|
24
|
+
# This is the only part of the gem with logic rather than plumbing, and it is
|
|
25
|
+
# also the part most likely to be wrong. Keeping it free of any binding to
|
|
26
|
+
# `winmm.dll` means it can be tested on any machine, by anyone, without
|
|
27
|
+
# Windows and without a MIDI port — which is what lets `test/message_test.rb`
|
|
28
|
+
# run anywhere.
|
|
29
|
+
#
|
|
30
|
+
# @api public
|
|
31
|
+
module Message
|
|
32
|
+
# Number of bytes in a channel message, indexed by the status byte's high
|
|
33
|
+
# nibble. Program Change and Channel Pressure carry one data byte; every
|
|
34
|
+
# other channel message carries two.
|
|
35
|
+
CHANNEL_MESSAGE_LENGTHS = {
|
|
36
|
+
0x8 => 3, # Note Off
|
|
37
|
+
0x9 => 3, # Note On
|
|
38
|
+
0xA => 3, # Polyphonic Key Pressure
|
|
39
|
+
0xB => 3, # Control Change
|
|
40
|
+
0xC => 2, # Program Change
|
|
41
|
+
0xD => 2, # Channel Pressure
|
|
42
|
+
0xE => 3 # Pitch Bend Change
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# Number of bytes in a System Common message, indexed by status byte.
|
|
46
|
+
# Everything from 0xF8 up is System Real Time and is always one byte, so it
|
|
47
|
+
# is not listed here.
|
|
48
|
+
SYSTEM_COMMON_MESSAGE_LENGTHS = {
|
|
49
|
+
0xF1 => 2, # MIDI Time Code Quarter Frame
|
|
50
|
+
0xF2 => 3, # Song Position Pointer
|
|
51
|
+
0xF3 => 2, # Song Select
|
|
52
|
+
0xF6 => 1 # Tune Request
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
# How many bytes the message with this status byte occupies.
|
|
58
|
+
#
|
|
59
|
+
# @param status [Integer] a MIDI status byte, 0x80..0xFF
|
|
60
|
+
# @return [Integer] 1, 2 or 3
|
|
61
|
+
# @raise [ArgumentError] if the byte is not a status byte, or begins a
|
|
62
|
+
# System Exclusive message, which is never delivered as a short message
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# Message.length_of(0x90) # => 3 Note On
|
|
66
|
+
# Message.length_of(0xC0) # => 2 Program Change
|
|
67
|
+
# Message.length_of(0xF8) # => 1 Clock
|
|
68
|
+
def length_of(status)
|
|
69
|
+
raise ArgumentError, "not a status byte: #{format('0x%02X', status)}" if status < 0x80 || status > 0xFF
|
|
70
|
+
|
|
71
|
+
# System Exclusive does not travel as a short message: WinMM delivers it
|
|
72
|
+
# through a buffer instead, so a caller who reaches here with 0xF0 has
|
|
73
|
+
# confused the two paths.
|
|
74
|
+
raise ArgumentError, 'System Exclusive is not a short message' if status == 0xF0
|
|
75
|
+
|
|
76
|
+
return 1 if status >= 0xF8
|
|
77
|
+
return SYSTEM_COMMON_MESSAGE_LENGTHS.fetch(status, 1) if status >= 0xF0
|
|
78
|
+
|
|
79
|
+
CHANNEL_MESSAGE_LENGTHS.fetch(status >> 4)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Unpacks the word WinMM delivers for an incoming short message.
|
|
83
|
+
#
|
|
84
|
+
# @param word [Integer] the `dwParam1` of an `MM_MIM_DATA` notification
|
|
85
|
+
# @return [Array<Integer>] the message's bytes, of the length its status
|
|
86
|
+
# byte calls for
|
|
87
|
+
#
|
|
88
|
+
# @example A Clock arrives as a word whose upper bytes are zero
|
|
89
|
+
# Message.unpack(0x0000_00F8) # => [0xF8]
|
|
90
|
+
#
|
|
91
|
+
# @example A Note On carries all three
|
|
92
|
+
# Message.unpack(0x0064_3C90) # => [0x90, 0x3C, 0x64]
|
|
93
|
+
def unpack(word)
|
|
94
|
+
bytes = [word & 0xFF, (word >> 8) & 0xFF, (word >> 16) & 0xFF]
|
|
95
|
+
|
|
96
|
+
bytes.first(length_of(bytes.first))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Packs a short message into the word `midiOutShortMsg` expects.
|
|
100
|
+
#
|
|
101
|
+
# The length must be exactly what the status byte calls for. Too few bytes
|
|
102
|
+
# is plainly a mistake; too many is either a mistake or an attempt at
|
|
103
|
+
# running status, which WinMM does not accept on output. Quietly dropping
|
|
104
|
+
# the surplus would send something the caller did not ask for and give no
|
|
105
|
+
# sign of it.
|
|
106
|
+
#
|
|
107
|
+
# @param bytes [Array<Integer>] the message, status byte first
|
|
108
|
+
# @return [Integer] the packed word
|
|
109
|
+
# @raise [ArgumentError] if the message is not the length its status byte
|
|
110
|
+
# calls for
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# Message.pack([0x90, 0x3C, 0x64]) # => 0x00643C90
|
|
114
|
+
def pack(bytes)
|
|
115
|
+
length = length_of(bytes.first)
|
|
116
|
+
|
|
117
|
+
unless bytes.size == length
|
|
118
|
+
raise ArgumentError,
|
|
119
|
+
"#{format('0x%02X', bytes.first)} is a #{length}-byte message, got #{bytes.size} bytes"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
bytes[0] | ((bytes[1] || 0) << 8) | ((bytes[2] || 0) << 16)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Is this the start of a System Exclusive message?
|
|
126
|
+
#
|
|
127
|
+
# @param bytes [Array<Integer>] a message, status byte first
|
|
128
|
+
# @return [Boolean]
|
|
129
|
+
def sysex?(bytes)
|
|
130
|
+
bytes.first == 0xF0
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|