Module: MIDICommunicationsWindows::Message
- Defined in:
- lib/midi-communications-windows/message.rb
Overview
Conversion between MIDI bytes and the packed 32-bit word WinMM uses for short messages.
WinMM does not pass short messages as byte strings. It packs them into a
DWORD: the status byte in the low-order byte, the first data byte next,
the second data byte after that, and the high-order byte unused. The same
packing is used in both directions — as dwMsg for midiOutShortMsg, and
as dwParam1 in the notification a client receives for input.
Why the length matters
A packed word carries no length. Nothing distinguishes a Clock, which is one
byte, from a Note On whose two data bytes happen to be zero: both arrive as
a word whose upper three bytes are zero. The length has to be derived from
the status byte, and getting it wrong is not a cosmetic problem — a Clock
delivered as [0xF8, 0, 0] is not a MIDI Clock message, and a parser
reading it will either reject it or invent two events that were never sent.
In MusaDSL that is the difference between a piece that follows the DAW's
tempo and one that does not start at all.
Why this file has no FFI in it
This is the only part of the gem with logic rather than plumbing, and it is
also the part most likely to be wrong. Keeping it free of any binding to
winmm.dll means it can be tested on any machine, by anyone, without
Windows and without a MIDI port — which is what lets test/message_test.rb
run anywhere.
Constant Summary collapse
- CHANNEL_MESSAGE_LENGTHS =
Number of bytes in a channel message, indexed by the status byte's high nibble. Program Change and Channel Pressure carry one data byte; every other channel message carries two.
{ 0x8 => 3, # Note Off 0x9 => 3, # Note On 0xA => 3, # Polyphonic Key Pressure 0xB => 3, # Control Change 0xC => 2, # Program Change 0xD => 2, # Channel Pressure 0xE => 3 # Pitch Bend Change }.freeze
- SYSTEM_COMMON_MESSAGE_LENGTHS =
Number of bytes in a System Common message, indexed by status byte. Everything from 0xF8 up is System Real Time and is always one byte, so it is not listed here.
{ 0xF1 => 2, # MIDI Time Code Quarter Frame 0xF2 => 3, # Song Position Pointer 0xF3 => 2, # Song Select 0xF6 => 1 # Tune Request }.freeze
Class Method Summary collapse
-
.length_of(status) ⇒ Integer
How many bytes the message with this status byte occupies.
-
.pack(bytes) ⇒ Integer
Packs a short message into the word
midiOutShortMsgexpects. -
.sysex?(bytes) ⇒ Boolean
Is this the start of a System Exclusive message?.
-
.unpack(word) ⇒ Array<Integer>
Unpacks the word WinMM delivers for an incoming short message.
Class Method Details
.length_of(status) ⇒ Integer
How many bytes the message with this status byte occupies.
68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/midi-communications-windows/message.rb', line 68 def length_of(status) raise ArgumentError, "not a status byte: #{format('0x%02X', status)}" if status < 0x80 || status > 0xFF # System Exclusive does not travel as a short message: WinMM delivers it # through a buffer instead, so a caller who reaches here with 0xF0 has # confused the two paths. raise ArgumentError, 'System Exclusive is not a short message' if status == 0xF0 return 1 if status >= 0xF8 return SYSTEM_COMMON_MESSAGE_LENGTHS.fetch(status, 1) if status >= 0xF0 CHANNEL_MESSAGE_LENGTHS.fetch(status >> 4) end |
.pack(bytes) ⇒ Integer
Packs a short message into the word midiOutShortMsg expects.
The length must be exactly what the status byte calls for. Too few bytes is plainly a mistake; too many is either a mistake or an attempt at running status, which WinMM does not accept on output. Quietly dropping the surplus would send something the caller did not ask for and give no sign of it.
114 115 116 117 118 119 120 121 122 123 |
# File 'lib/midi-communications-windows/message.rb', line 114 def pack(bytes) length = length_of(bytes.first) unless bytes.size == length raise ArgumentError, "#{format('0x%02X', bytes.first)} is a #{length}-byte message, got #{bytes.size} bytes" end bytes[0] | ((bytes[1] || 0) << 8) | ((bytes[2] || 0) << 16) end |
.sysex?(bytes) ⇒ Boolean
Is this the start of a System Exclusive message?
129 130 131 |
# File 'lib/midi-communications-windows/message.rb', line 129 def sysex?(bytes) bytes.first == 0xF0 end |
.unpack(word) ⇒ Array<Integer>
Unpacks the word WinMM delivers for an incoming short message.
93 94 95 96 97 |
# File 'lib/midi-communications-windows/message.rb', line 93 def unpack(word) bytes = [word & 0xFF, (word >> 8) & 0xFF, (word >> 16) & 0xFF] bytes.first(length_of(bytes.first)) end |