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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/Rakefile +48 -0
  6. data/lib/webmidi/access.rb +170 -0
  7. data/lib/webmidi/callback_subscription.rb +26 -0
  8. data/lib/webmidi/clock.rb +129 -0
  9. data/lib/webmidi/configuration.rb +43 -0
  10. data/lib/webmidi/error.rb +26 -0
  11. data/lib/webmidi/message/base.rb +64 -0
  12. data/lib/webmidi/message/channel.rb +238 -0
  13. data/lib/webmidi/message/parser.rb +308 -0
  14. data/lib/webmidi/message/system.rb +162 -0
  15. data/lib/webmidi/message/ump.rb +675 -0
  16. data/lib/webmidi/message.rb +154 -0
  17. data/lib/webmidi/middleware/base.rb +16 -0
  18. data/lib/webmidi/middleware/channel_map.rb +36 -0
  19. data/lib/webmidi/middleware/filter.rb +22 -0
  20. data/lib/webmidi/middleware/logger.rb +17 -0
  21. data/lib/webmidi/middleware/note_range_filter.rb +34 -0
  22. data/lib/webmidi/middleware/panic.rb +73 -0
  23. data/lib/webmidi/middleware/pipeline.rb +19 -0
  24. data/lib/webmidi/middleware/recorder.rb +123 -0
  25. data/lib/webmidi/middleware/split_by_channel.rb +66 -0
  26. data/lib/webmidi/middleware/stack.rb +55 -0
  27. data/lib/webmidi/middleware/timing_gate.rb +58 -0
  28. data/lib/webmidi/middleware/transpose.rb +30 -0
  29. data/lib/webmidi/middleware/velocity_clamp.rb +37 -0
  30. data/lib/webmidi/middleware/velocity_scale.rb +55 -0
  31. data/lib/webmidi/middleware.rb +21 -0
  32. data/lib/webmidi/music/chord.rb +90 -0
  33. data/lib/webmidi/music/note.rb +102 -0
  34. data/lib/webmidi/music/rhythm.rb +92 -0
  35. data/lib/webmidi/music/scale.rb +85 -0
  36. data/lib/webmidi/music.rb +24 -0
  37. data/lib/webmidi/network/apple_midi.rb +189 -0
  38. data/lib/webmidi/network/osc.rb +205 -0
  39. data/lib/webmidi/network/rtp.rb +410 -0
  40. data/lib/webmidi/network.rb +10 -0
  41. data/lib/webmidi/port/base.rb +89 -0
  42. data/lib/webmidi/port/input.rb +158 -0
  43. data/lib/webmidi/port/map.rb +65 -0
  44. data/lib/webmidi/port/output.rb +208 -0
  45. data/lib/webmidi/port.rb +11 -0
  46. data/lib/webmidi/smf/event.rb +206 -0
  47. data/lib/webmidi/smf/reader.rb +237 -0
  48. data/lib/webmidi/smf/sequence.rb +135 -0
  49. data/lib/webmidi/smf/tempo_map.rb +107 -0
  50. data/lib/webmidi/smf/track.rb +130 -0
  51. data/lib/webmidi/smf/writer.rb +121 -0
  52. data/lib/webmidi/smf.rb +13 -0
  53. data/lib/webmidi/transport/adapter.rb +46 -0
  54. data/lib/webmidi/transport/base.rb +59 -0
  55. data/lib/webmidi/transport/device_info.rb +7 -0
  56. data/lib/webmidi/transport/null.rb +81 -0
  57. data/lib/webmidi/transport/virtual.rb +184 -0
  58. data/lib/webmidi/transport.rb +80 -0
  59. data/lib/webmidi/version.rb +5 -0
  60. data/lib/webmidi/virtual/loopback.rb +45 -0
  61. data/lib/webmidi/virtual/port.rb +48 -0
  62. data/lib/webmidi/virtual.rb +9 -0
  63. data/lib/webmidi.rb +19 -0
  64. data/webmidi.gemspec +32 -0
  65. 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