webmidi 0.1.0
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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/Rakefile +48 -0
- data/lib/webmidi/access.rb +170 -0
- data/lib/webmidi/callback_subscription.rb +26 -0
- data/lib/webmidi/clock.rb +129 -0
- data/lib/webmidi/configuration.rb +43 -0
- data/lib/webmidi/error.rb +26 -0
- data/lib/webmidi/message/base.rb +64 -0
- data/lib/webmidi/message/channel.rb +238 -0
- data/lib/webmidi/message/parser.rb +308 -0
- data/lib/webmidi/message/system.rb +162 -0
- data/lib/webmidi/message/ump.rb +675 -0
- data/lib/webmidi/message.rb +154 -0
- data/lib/webmidi/middleware/base.rb +16 -0
- data/lib/webmidi/middleware/channel_map.rb +36 -0
- data/lib/webmidi/middleware/filter.rb +22 -0
- data/lib/webmidi/middleware/logger.rb +17 -0
- data/lib/webmidi/middleware/note_range_filter.rb +34 -0
- data/lib/webmidi/middleware/panic.rb +73 -0
- data/lib/webmidi/middleware/pipeline.rb +19 -0
- data/lib/webmidi/middleware/recorder.rb +123 -0
- data/lib/webmidi/middleware/split_by_channel.rb +66 -0
- data/lib/webmidi/middleware/stack.rb +55 -0
- data/lib/webmidi/middleware/timing_gate.rb +58 -0
- data/lib/webmidi/middleware/transpose.rb +30 -0
- data/lib/webmidi/middleware/velocity_clamp.rb +37 -0
- data/lib/webmidi/middleware/velocity_scale.rb +55 -0
- data/lib/webmidi/middleware.rb +21 -0
- data/lib/webmidi/music/chord.rb +90 -0
- data/lib/webmidi/music/note.rb +102 -0
- data/lib/webmidi/music/rhythm.rb +92 -0
- data/lib/webmidi/music/scale.rb +85 -0
- data/lib/webmidi/music.rb +24 -0
- data/lib/webmidi/network/apple_midi.rb +189 -0
- data/lib/webmidi/network/osc.rb +205 -0
- data/lib/webmidi/network/rtp.rb +410 -0
- data/lib/webmidi/network.rb +10 -0
- data/lib/webmidi/port/base.rb +89 -0
- data/lib/webmidi/port/input.rb +158 -0
- data/lib/webmidi/port/map.rb +65 -0
- data/lib/webmidi/port/output.rb +208 -0
- data/lib/webmidi/port.rb +11 -0
- data/lib/webmidi/smf/event.rb +206 -0
- data/lib/webmidi/smf/reader.rb +237 -0
- data/lib/webmidi/smf/sequence.rb +135 -0
- data/lib/webmidi/smf/tempo_map.rb +107 -0
- data/lib/webmidi/smf/track.rb +130 -0
- data/lib/webmidi/smf/writer.rb +121 -0
- data/lib/webmidi/smf.rb +13 -0
- data/lib/webmidi/transport/adapter.rb +46 -0
- data/lib/webmidi/transport/base.rb +59 -0
- data/lib/webmidi/transport/device_info.rb +7 -0
- data/lib/webmidi/transport/null.rb +81 -0
- data/lib/webmidi/transport/virtual.rb +184 -0
- data/lib/webmidi/transport.rb +80 -0
- data/lib/webmidi/version.rb +5 -0
- data/lib/webmidi/virtual/loopback.rb +45 -0
- data/lib/webmidi/virtual/port.rb +48 -0
- data/lib/webmidi/virtual.rb +9 -0
- data/lib/webmidi.rb +19 -0
- data/webmidi.gemspec +32 -0
- metadata +108 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Webmidi
|
|
4
|
+
module Message
|
|
5
|
+
module Channel
|
|
6
|
+
class Base < Message::Base
|
|
7
|
+
attr_reader :channel
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def validate_channel!(channel)
|
|
12
|
+
unless channel.is_a?(Integer) && channel.between?(0, 15)
|
|
13
|
+
raise InvalidMessageError, "Channel must be between 0 and 15, got #{channel.inspect}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate_byte!(value, name)
|
|
18
|
+
unless value.is_a?(Integer) && value.between?(0, 127)
|
|
19
|
+
raise InvalidMessageError, "#{name} must be between 0 and 127, got #{value.inspect}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class NoteOn < Base
|
|
25
|
+
attr_reader :note, :velocity
|
|
26
|
+
|
|
27
|
+
def initialize(note:, velocity: 100, channel: 0, timestamp: nil)
|
|
28
|
+
validate_channel!(channel)
|
|
29
|
+
validate_byte!(note, "Note")
|
|
30
|
+
validate_byte!(velocity, "Velocity")
|
|
31
|
+
@note = note
|
|
32
|
+
@velocity = velocity
|
|
33
|
+
@channel = channel
|
|
34
|
+
super(timestamp: timestamp)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_bytes
|
|
38
|
+
[0x90 | @channel, @note, @velocity]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deconstruct_keys(keys)
|
|
42
|
+
{note: @note, velocity: @velocity, channel: @channel}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class NoteOff < Base
|
|
47
|
+
attr_reader :note, :velocity
|
|
48
|
+
|
|
49
|
+
def initialize(note:, velocity: 0, channel: 0, timestamp: nil)
|
|
50
|
+
validate_channel!(channel)
|
|
51
|
+
validate_byte!(note, "Note")
|
|
52
|
+
validate_byte!(velocity, "Velocity")
|
|
53
|
+
@note = note
|
|
54
|
+
@velocity = velocity
|
|
55
|
+
@channel = channel
|
|
56
|
+
super(timestamp: timestamp)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_bytes
|
|
60
|
+
[0x80 | @channel, @note, @velocity]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def deconstruct_keys(keys)
|
|
64
|
+
{note: @note, velocity: @velocity, channel: @channel}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class PolyphonicPressure < Base
|
|
69
|
+
attr_reader :note, :pressure
|
|
70
|
+
|
|
71
|
+
def initialize(note:, pressure:, channel: 0, timestamp: nil)
|
|
72
|
+
validate_channel!(channel)
|
|
73
|
+
validate_byte!(note, "Note")
|
|
74
|
+
validate_byte!(pressure, "Pressure")
|
|
75
|
+
@note = note
|
|
76
|
+
@pressure = pressure
|
|
77
|
+
@channel = channel
|
|
78
|
+
super(timestamp: timestamp)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_bytes
|
|
82
|
+
[0xA0 | @channel, @note, @pressure]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def deconstruct_keys(keys)
|
|
86
|
+
{note: @note, pressure: @pressure, channel: @channel}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class ControlChange < Base
|
|
91
|
+
CONTROLLERS = {
|
|
92
|
+
bank_select: 0,
|
|
93
|
+
modulation: 1,
|
|
94
|
+
breath_controller: 2,
|
|
95
|
+
foot_controller: 4,
|
|
96
|
+
portamento_time: 5,
|
|
97
|
+
data_entry_msb: 6,
|
|
98
|
+
volume: 7,
|
|
99
|
+
balance: 8,
|
|
100
|
+
pan: 10,
|
|
101
|
+
expression: 11,
|
|
102
|
+
sustain: 64,
|
|
103
|
+
portamento: 65,
|
|
104
|
+
sostenuto: 66,
|
|
105
|
+
soft_pedal: 67,
|
|
106
|
+
legato: 68,
|
|
107
|
+
hold_2: 69,
|
|
108
|
+
sound_variation: 70,
|
|
109
|
+
resonance: 71,
|
|
110
|
+
release_time: 72,
|
|
111
|
+
attack_time: 73,
|
|
112
|
+
brightness: 74,
|
|
113
|
+
all_sound_off: 120,
|
|
114
|
+
reset_all_controllers: 121,
|
|
115
|
+
local_control: 122,
|
|
116
|
+
all_notes_off: 123,
|
|
117
|
+
omni_off: 124,
|
|
118
|
+
omni_on: 125,
|
|
119
|
+
mono_on: 126,
|
|
120
|
+
poly_on: 127
|
|
121
|
+
}.freeze
|
|
122
|
+
|
|
123
|
+
ALL_NOTES_OFF = CONTROLLERS[:all_notes_off]
|
|
124
|
+
|
|
125
|
+
attr_reader :cc, :value
|
|
126
|
+
|
|
127
|
+
def initialize(cc:, value:, channel: 0, timestamp: nil)
|
|
128
|
+
validate_channel!(channel)
|
|
129
|
+
cc = self.class.controller_number(cc)
|
|
130
|
+
validate_byte!(cc, "CC")
|
|
131
|
+
validate_byte!(value, "Value")
|
|
132
|
+
@cc = cc
|
|
133
|
+
@value = value
|
|
134
|
+
@channel = channel
|
|
135
|
+
super(timestamp: timestamp)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def to_bytes
|
|
139
|
+
[0xB0 | @channel, @cc, @value]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def deconstruct_keys(keys)
|
|
143
|
+
{cc: @cc, value: @value, channel: @channel}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.controller_number(controller)
|
|
147
|
+
return controller if controller.is_a?(Integer)
|
|
148
|
+
|
|
149
|
+
key = controller.to_sym if controller.respond_to?(:to_sym)
|
|
150
|
+
return CONTROLLERS[key] if key && CONTROLLERS.key?(key)
|
|
151
|
+
|
|
152
|
+
raise InvalidMessageError, "Unknown control change controller: #{controller.inspect}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
class ProgramChange < Base
|
|
157
|
+
attr_reader :program
|
|
158
|
+
|
|
159
|
+
def initialize(program:, channel: 0, timestamp: nil)
|
|
160
|
+
validate_channel!(channel)
|
|
161
|
+
validate_byte!(program, "Program")
|
|
162
|
+
@program = program
|
|
163
|
+
@channel = channel
|
|
164
|
+
super(timestamp: timestamp)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def to_bytes
|
|
168
|
+
[0xC0 | @channel, @program]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def deconstruct_keys(keys)
|
|
172
|
+
{program: @program, channel: @channel}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class ChannelPressure < Base
|
|
177
|
+
attr_reader :pressure
|
|
178
|
+
|
|
179
|
+
def initialize(pressure:, channel: 0, timestamp: nil)
|
|
180
|
+
validate_channel!(channel)
|
|
181
|
+
validate_byte!(pressure, "Pressure")
|
|
182
|
+
@pressure = pressure
|
|
183
|
+
@channel = channel
|
|
184
|
+
super(timestamp: timestamp)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def to_bytes
|
|
188
|
+
[0xD0 | @channel, @pressure]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def deconstruct_keys(keys)
|
|
192
|
+
{pressure: @pressure, channel: @channel}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
class PitchBend < Base
|
|
197
|
+
MIN = 0
|
|
198
|
+
CENTER = 8192
|
|
199
|
+
MAX = 16_383
|
|
200
|
+
SIGNED_MIN = -8192
|
|
201
|
+
SIGNED_MAX = 8191
|
|
202
|
+
|
|
203
|
+
attr_reader :value
|
|
204
|
+
|
|
205
|
+
def initialize(value:, channel: 0, timestamp: nil)
|
|
206
|
+
validate_channel!(channel)
|
|
207
|
+
unless value.is_a?(Integer) && value.between?(MIN, MAX)
|
|
208
|
+
raise InvalidMessageError, "Pitch bend value must be between #{MIN} and #{MAX}, got #{value.inspect}"
|
|
209
|
+
end
|
|
210
|
+
@value = value
|
|
211
|
+
@channel = channel
|
|
212
|
+
super(timestamp: timestamp)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def to_bytes
|
|
216
|
+
[0xE0 | @channel, @value & 0x7F, (@value >> 7) & 0x7F]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def deconstruct_keys(keys)
|
|
220
|
+
{value: @value, channel: @channel}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def signed_value
|
|
224
|
+
@value - CENTER
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def self.from_signed(value, channel: 0, timestamp: nil)
|
|
228
|
+
unless value.is_a?(Integer) && value.between?(SIGNED_MIN, SIGNED_MAX)
|
|
229
|
+
raise InvalidMessageError,
|
|
230
|
+
"Signed pitch bend value must be between #{SIGNED_MIN} and #{SIGNED_MAX}, got #{value.inspect}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
new(value: value + CENTER, channel: channel, timestamp: timestamp)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Webmidi
|
|
4
|
+
module Message
|
|
5
|
+
module Parser
|
|
6
|
+
CHANNEL_LENGTHS = {
|
|
7
|
+
0x80 => 3,
|
|
8
|
+
0x90 => 3,
|
|
9
|
+
0xA0 => 3,
|
|
10
|
+
0xB0 => 3,
|
|
11
|
+
0xC0 => 2,
|
|
12
|
+
0xD0 => 2,
|
|
13
|
+
0xE0 => 3
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
SYSTEM_LENGTHS = {
|
|
17
|
+
0xF1 => 2,
|
|
18
|
+
0xF2 => 3,
|
|
19
|
+
0xF3 => 2,
|
|
20
|
+
0xF6 => 1,
|
|
21
|
+
0xF8 => 1,
|
|
22
|
+
0xFA => 1,
|
|
23
|
+
0xFB => 1,
|
|
24
|
+
0xFC => 1,
|
|
25
|
+
0xFE => 1,
|
|
26
|
+
0xFF => 1
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
REAL_TIME_STATUSES = [0xF8, 0xFA, 0xFB, 0xFC, 0xFE, 0xFF].freeze
|
|
30
|
+
INVALID_SYSTEM_STATUSES = [0xF4, 0xF5, 0xF7, 0xF9, 0xFD].freeze
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
def parse_single(bytes, normalize_note_on_zero: true)
|
|
35
|
+
bytes = validate_bytes!(bytes)
|
|
36
|
+
raise InvalidMessageError, "Empty message" if bytes.empty?
|
|
37
|
+
|
|
38
|
+
status = bytes[0]
|
|
39
|
+
validate_status!(status)
|
|
40
|
+
|
|
41
|
+
if status == 0xF0
|
|
42
|
+
return parse_sysex(bytes)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
validate_exact_length!(bytes, message_length(status))
|
|
46
|
+
validate_data_bytes!(bytes[1..])
|
|
47
|
+
|
|
48
|
+
case status & 0xF0
|
|
49
|
+
when 0x80
|
|
50
|
+
parse_note_off(bytes)
|
|
51
|
+
when 0x90
|
|
52
|
+
parse_note_on(bytes, normalize_note_on_zero: normalize_note_on_zero)
|
|
53
|
+
when 0xA0
|
|
54
|
+
parse_polyphonic_pressure(bytes)
|
|
55
|
+
when 0xB0
|
|
56
|
+
parse_control_change(bytes)
|
|
57
|
+
when 0xC0
|
|
58
|
+
parse_program_change(bytes)
|
|
59
|
+
when 0xD0
|
|
60
|
+
parse_channel_pressure(bytes)
|
|
61
|
+
when 0xE0
|
|
62
|
+
parse_pitch_bend(bytes)
|
|
63
|
+
when 0xF0
|
|
64
|
+
parse_system(bytes)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_many(bytes, normalize_note_on_zero: true)
|
|
69
|
+
parse_stream(bytes, running_status: false, normalize_note_on_zero: normalize_note_on_zero)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse_stream(bytes, running_status: true, normalize_note_on_zero: true)
|
|
73
|
+
bytes = validate_bytes!(bytes)
|
|
74
|
+
messages = []
|
|
75
|
+
pending = []
|
|
76
|
+
needed = nil
|
|
77
|
+
last_channel_status = nil
|
|
78
|
+
|
|
79
|
+
bytes.each do |byte|
|
|
80
|
+
if real_time_status?(byte)
|
|
81
|
+
messages << parse_single([byte], normalize_note_on_zero: normalize_note_on_zero)
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if pending.empty?
|
|
86
|
+
if byte < 0x80
|
|
87
|
+
raise InvalidMessageError, "Data byte #{format_byte(byte)} without status" unless running_status && last_channel_status
|
|
88
|
+
|
|
89
|
+
pending = [last_channel_status, byte]
|
|
90
|
+
needed = message_length(last_channel_status)
|
|
91
|
+
else
|
|
92
|
+
validate_status!(byte)
|
|
93
|
+
pending = [byte]
|
|
94
|
+
needed = (byte == 0xF0) ? :sysex : message_length(byte)
|
|
95
|
+
last_channel_status = channel_status?(byte) ? byte : nil
|
|
96
|
+
end
|
|
97
|
+
elsif needed == :sysex
|
|
98
|
+
validate_sysex_data_or_end!(byte)
|
|
99
|
+
pending << byte
|
|
100
|
+
else
|
|
101
|
+
raise InvalidMessageError, "Unexpected status byte #{format_byte(byte)} inside message" if byte >= 0x80
|
|
102
|
+
|
|
103
|
+
pending << byte
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
next unless message_complete?(pending, needed)
|
|
107
|
+
|
|
108
|
+
messages << parse_single(pending, normalize_note_on_zero: normalize_note_on_zero)
|
|
109
|
+
pending = []
|
|
110
|
+
needed = nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
raise_incomplete!(pending, needed) unless pending.empty?
|
|
114
|
+
|
|
115
|
+
messages
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def parse_note_off(bytes)
|
|
119
|
+
Channel::NoteOff.new(
|
|
120
|
+
note: bytes[1],
|
|
121
|
+
velocity: bytes[2],
|
|
122
|
+
channel: bytes[0] & 0x0F
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_note_on(bytes, normalize_note_on_zero: true)
|
|
127
|
+
if normalize_note_on_zero && bytes[2].zero?
|
|
128
|
+
Channel::NoteOff.new(
|
|
129
|
+
note: bytes[1],
|
|
130
|
+
velocity: 0,
|
|
131
|
+
channel: bytes[0] & 0x0F
|
|
132
|
+
)
|
|
133
|
+
else
|
|
134
|
+
Channel::NoteOn.new(
|
|
135
|
+
note: bytes[1],
|
|
136
|
+
velocity: bytes[2],
|
|
137
|
+
channel: bytes[0] & 0x0F
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def parse_polyphonic_pressure(bytes)
|
|
143
|
+
Channel::PolyphonicPressure.new(
|
|
144
|
+
note: bytes[1],
|
|
145
|
+
pressure: bytes[2],
|
|
146
|
+
channel: bytes[0] & 0x0F
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def parse_control_change(bytes)
|
|
151
|
+
Channel::ControlChange.new(
|
|
152
|
+
cc: bytes[1],
|
|
153
|
+
value: bytes[2],
|
|
154
|
+
channel: bytes[0] & 0x0F
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_program_change(bytes)
|
|
159
|
+
Channel::ProgramChange.new(
|
|
160
|
+
program: bytes[1],
|
|
161
|
+
channel: bytes[0] & 0x0F
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def parse_channel_pressure(bytes)
|
|
166
|
+
Channel::ChannelPressure.new(
|
|
167
|
+
pressure: bytes[1],
|
|
168
|
+
channel: bytes[0] & 0x0F
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def parse_pitch_bend(bytes)
|
|
173
|
+
value = bytes[1] | (bytes[2] << 7)
|
|
174
|
+
Channel::PitchBend.new(
|
|
175
|
+
value: value,
|
|
176
|
+
channel: bytes[0] & 0x0F
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def parse_system(bytes)
|
|
181
|
+
case bytes[0]
|
|
182
|
+
when 0xF1
|
|
183
|
+
System::TimeCode.new(type: (bytes[1] >> 4) & 0x07, value: bytes[1] & 0x0F)
|
|
184
|
+
when 0xF2
|
|
185
|
+
System::SongPosition.new(position: bytes[1] | (bytes[2] << 7))
|
|
186
|
+
when 0xF3
|
|
187
|
+
System::SongSelect.new(song: bytes[1])
|
|
188
|
+
when 0xF6
|
|
189
|
+
System::TuneRequest.new
|
|
190
|
+
when 0xF8
|
|
191
|
+
System::Clock.new
|
|
192
|
+
when 0xFA
|
|
193
|
+
System::Start.new
|
|
194
|
+
when 0xFB
|
|
195
|
+
System::Continue.new
|
|
196
|
+
when 0xFC
|
|
197
|
+
System::Stop.new
|
|
198
|
+
when 0xFE
|
|
199
|
+
System::ActiveSensing.new
|
|
200
|
+
when 0xFF
|
|
201
|
+
System::SystemReset.new
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def parse_sysex(bytes)
|
|
206
|
+
raise InvalidMessageError, "SysEx message too short" if bytes.length < 2
|
|
207
|
+
raise InvalidMessageError, "SysEx message must end with 0xF7" unless bytes.last == 0xF7
|
|
208
|
+
|
|
209
|
+
bytes[1...-1].each { |byte| validate_sysex_data!(byte) }
|
|
210
|
+
System::SysEx.new(data: bytes[1..-2])
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def message_length(status)
|
|
214
|
+
high = status & 0xF0
|
|
215
|
+
CHANNEL_LENGTHS[high] || SYSTEM_LENGTHS[status] || invalid_status!(status)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def validate_bytes!(bytes)
|
|
219
|
+
unless bytes.respond_to?(:each)
|
|
220
|
+
raise InvalidMessageError, "MIDI bytes must be enumerable, got #{bytes.class}"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
bytes.to_a.each_with_index do |byte, index|
|
|
224
|
+
next if byte.is_a?(Integer) && byte.between?(0, 255)
|
|
225
|
+
|
|
226
|
+
raise InvalidMessageError, "Byte at index #{index} must be between 0 and 255, got #{byte.inspect}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def validate_status!(status)
|
|
231
|
+
raise InvalidMessageError, "Invalid status byte: #{format_byte(status)}" if status < 0x80
|
|
232
|
+
|
|
233
|
+
invalid_status!(status) if INVALID_SYSTEM_STATUSES.include?(status)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def invalid_status!(status)
|
|
237
|
+
if INVALID_SYSTEM_STATUSES.include?(status)
|
|
238
|
+
raise InvalidMessageError, "Invalid system message status: #{format_byte(status)}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
raise InvalidMessageError, "Invalid status byte: #{format_byte(status)}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def validate_exact_length!(bytes, expected)
|
|
245
|
+
return if bytes.length == expected
|
|
246
|
+
|
|
247
|
+
detail = (bytes.length < expected) ? "Expected" : "Expected exactly"
|
|
248
|
+
raise InvalidMessageError,
|
|
249
|
+
"#{detail} #{expected} bytes for #{format_byte(bytes[0])}, got #{bytes.length}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def validate_sysex_data_or_end!(byte)
|
|
253
|
+
return if byte == 0xF7
|
|
254
|
+
|
|
255
|
+
validate_sysex_data!(byte)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def validate_sysex_data!(byte)
|
|
259
|
+
return if byte.between?(0, 127)
|
|
260
|
+
|
|
261
|
+
raise InvalidMessageError, "SysEx data byte must be between 0 and 127, got #{format_byte(byte)}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def validate_data_bytes!(bytes)
|
|
265
|
+
bytes.each_with_index do |byte, index|
|
|
266
|
+
next if byte.between?(0, 127)
|
|
267
|
+
|
|
268
|
+
raise InvalidMessageError, "Data byte at index #{index + 1} must be between 0 and 127, got #{format_byte(byte)}"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def message_complete?(pending, needed)
|
|
273
|
+
return pending.last == 0xF7 if needed == :sysex
|
|
274
|
+
|
|
275
|
+
pending.length == needed
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def raise_incomplete!(pending, needed)
|
|
279
|
+
if needed == :sysex
|
|
280
|
+
raise InvalidMessageError, "SysEx message must end with 0xF7"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
raise InvalidMessageError,
|
|
284
|
+
"Expected #{needed} bytes for #{format_byte(pending[0])}, got #{pending.length}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def real_time_status?(byte)
|
|
288
|
+
REAL_TIME_STATUSES.include?(byte)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def channel_status?(byte)
|
|
292
|
+
(byte & 0xF0).between?(0x80, 0xE0)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def format_byte(byte)
|
|
296
|
+
format("0x%02X", byte)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
private_class_method :parse_note_off, :parse_note_on, :parse_polyphonic_pressure,
|
|
300
|
+
:parse_control_change, :parse_program_change, :parse_channel_pressure,
|
|
301
|
+
:parse_pitch_bend, :parse_system, :parse_sysex, :message_length,
|
|
302
|
+
:validate_bytes!, :validate_status!, :invalid_status!,
|
|
303
|
+
:validate_exact_length!, :validate_sysex_data_or_end!,
|
|
304
|
+
:validate_sysex_data!, :validate_data_bytes!, :message_complete?, :raise_incomplete!,
|
|
305
|
+
:real_time_status?, :channel_status?, :format_byte
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Webmidi
|
|
4
|
+
module Message
|
|
5
|
+
module System
|
|
6
|
+
class Base < Message::Base
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class SysEx < Base
|
|
10
|
+
attr_reader :data
|
|
11
|
+
|
|
12
|
+
def initialize(data:, timestamp: nil)
|
|
13
|
+
unless data.respond_to?(:each)
|
|
14
|
+
raise InvalidMessageError, "SysEx data must be enumerable, got #{data.class}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
data.each_with_index do |byte, i|
|
|
18
|
+
unless byte.is_a?(Integer) && byte.between?(0, 127)
|
|
19
|
+
raise InvalidMessageError, "SysEx data byte at index #{i} must be between 0 and 127, got #{byte.inspect}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
@data = data.dup.freeze
|
|
23
|
+
super(timestamp: timestamp)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_bytes
|
|
27
|
+
[0xF0, *@data, 0xF7]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deconstruct_keys(keys)
|
|
31
|
+
{data: @data}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def chunks(max_data_bytes:)
|
|
35
|
+
self.class.split(self, max_data_bytes: max_data_bytes)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.split(data, max_data_bytes:)
|
|
39
|
+
unless max_data_bytes.is_a?(Integer) && max_data_bytes.positive?
|
|
40
|
+
raise InvalidMessageError, "max_data_bytes must be a positive integer, got #{max_data_bytes.inspect}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
bytes = data.is_a?(self) ? data.data : data
|
|
44
|
+
bytes.each_slice(max_data_bytes).map { |slice| new(data: slice) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.join(messages)
|
|
48
|
+
data = messages.flat_map do |message|
|
|
49
|
+
message.is_a?(self) ? message.data : message
|
|
50
|
+
end
|
|
51
|
+
new(data: data)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class TimeCode < Base
|
|
56
|
+
attr_reader :type, :value
|
|
57
|
+
|
|
58
|
+
def initialize(type:, value:, timestamp: nil)
|
|
59
|
+
unless type.is_a?(Integer) && type.between?(0, 7)
|
|
60
|
+
raise InvalidMessageError, "TimeCode type must be between 0 and 7, got #{type.inspect}"
|
|
61
|
+
end
|
|
62
|
+
unless value.is_a?(Integer) && value.between?(0, 15)
|
|
63
|
+
raise InvalidMessageError, "TimeCode value must be between 0 and 15, got #{value.inspect}"
|
|
64
|
+
end
|
|
65
|
+
@type = type
|
|
66
|
+
@value = value
|
|
67
|
+
super(timestamp: timestamp)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_bytes
|
|
71
|
+
[0xF1, (@type << 4) | @value]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def deconstruct_keys(keys)
|
|
75
|
+
{type: @type, value: @value}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class SongPosition < Base
|
|
80
|
+
attr_reader :position
|
|
81
|
+
|
|
82
|
+
def initialize(position:, timestamp: nil)
|
|
83
|
+
unless position.is_a?(Integer) && position.between?(0, 16383)
|
|
84
|
+
raise InvalidMessageError, "Song position must be between 0 and 16383, got #{position.inspect}"
|
|
85
|
+
end
|
|
86
|
+
@position = position
|
|
87
|
+
super(timestamp: timestamp)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_bytes
|
|
91
|
+
[0xF2, @position & 0x7F, (@position >> 7) & 0x7F]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def deconstruct_keys(keys)
|
|
95
|
+
{position: @position}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class SongSelect < Base
|
|
100
|
+
attr_reader :song
|
|
101
|
+
|
|
102
|
+
def initialize(song:, timestamp: nil)
|
|
103
|
+
unless song.is_a?(Integer) && song.between?(0, 127)
|
|
104
|
+
raise InvalidMessageError, "Song number must be between 0 and 127, got #{song.inspect}"
|
|
105
|
+
end
|
|
106
|
+
@song = song
|
|
107
|
+
super(timestamp: timestamp)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_bytes
|
|
111
|
+
[0xF3, @song]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def deconstruct_keys(keys)
|
|
115
|
+
{song: @song}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class TuneRequest < Base
|
|
120
|
+
def to_bytes
|
|
121
|
+
[0xF6]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class Clock < Base
|
|
126
|
+
def to_bytes
|
|
127
|
+
[0xF8]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class Start < Base
|
|
132
|
+
def to_bytes
|
|
133
|
+
[0xFA]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class Continue < Base
|
|
138
|
+
def to_bytes
|
|
139
|
+
[0xFB]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class Stop < Base
|
|
144
|
+
def to_bytes
|
|
145
|
+
[0xFC]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class ActiveSensing < Base
|
|
150
|
+
def to_bytes
|
|
151
|
+
[0xFE]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class SystemReset < Base
|
|
156
|
+
def to_bytes
|
|
157
|
+
[0xFF]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|