midi-message 0.4.8 → 0.4.9
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 +4 -4
- data/lib/midi-message.rb +1 -1
- data/lib/midi-message/channel_message.rb +76 -53
- data/lib/midi-message/constant.rb +34 -7
- data/lib/midi-message/context.rb +11 -1
- data/lib/midi-message/message.rb +11 -3
- data/lib/midi-message/messages.rb +45 -15
- data/lib/midi-message/parser.rb +65 -22
- data/lib/midi-message/system_exclusive.rb +42 -22
- data/lib/midi-message/system_message.rb +3 -1
- data/test/constant_test.rb +81 -3
- data/test/context_test.rb +86 -82
- data/test/message_test.rb +28 -122
- data/test/messages_test.rb +139 -0
- data/test/parser_test.rb +1 -1
- data/test/system_exclusive_test.rb +245 -98
- data/test/system_message_test.rb +95 -55
- metadata +3 -3
- data/test/mutability_test.rb +0 -23
data/lib/midi-message/parser.rb
CHANGED
@@ -1,46 +1,89 @@
|
|
1
1
|
module MIDIMessage
|
2
2
|
|
3
|
-
#
|
4
|
-
#
|
3
|
+
# Simple message parsing
|
4
|
+
# For more advanced parsing check out {nibbler}[http://github.com/arirusso/nibbler]
|
5
5
|
class Parser
|
6
6
|
|
7
|
+
MESSAGE_TYPE = [
|
8
|
+
NoteOff,
|
9
|
+
NoteOn,
|
10
|
+
PolyphonicAftertouch,
|
11
|
+
ControlChange,
|
12
|
+
ProgramChange,
|
13
|
+
ChannelAftertouch,
|
14
|
+
PitchBend,
|
15
|
+
SystemMessage
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
SYSTEM_MESSAGE_TYPE = [
|
19
|
+
SystemExclusive,
|
20
|
+
SystemCommon,
|
21
|
+
SystemRealtime
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
# Can take either a hex string eg Parser.new("904040")
|
25
|
+
# or bytes eg Parser.new(0x90, 0x40, 0x40)
|
26
|
+
# or an array of bytes eg Parser.new([0x90, 0x40, 0x40])
|
27
|
+
# @param [Array<Fixnum>, *Fixnum, String] args
|
28
|
+
# @return [MIDIMessage]
|
29
|
+
def self.parse(*args)
|
30
|
+
parser = new(*args)
|
31
|
+
parser.parse
|
32
|
+
end
|
33
|
+
|
7
34
|
# Can take either a hex string eg Parser.new("904040")
|
8
35
|
# or bytes eg Parser.new(0x90, 0x40, 0x40)
|
9
36
|
# or an array of bytes eg Parser.new([0x90, 0x40, 0x40])
|
37
|
+
# @param [Array<Fixnum>, *Fixnum, String] args
|
38
|
+
# @return [MIDIMessage]
|
10
39
|
def initialize(*args)
|
11
40
|
@data = case args.first
|
12
41
|
when Array then args.first
|
13
|
-
when Numeric then args
|
42
|
+
when Numeric then args
|
14
43
|
when String then TypeConversion.hex_string_to_numeric_byte_array(args.first)
|
15
44
|
end
|
16
45
|
end
|
17
46
|
|
18
47
|
# Parse the data and return a message
|
48
|
+
# @return [MIDIMessage]
|
19
49
|
def parse
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
when 0xB then ControlChange.new(second_nibble, @data[1], @data[2])
|
27
|
-
when 0xC then ProgramChange.new(second_nibble, @data[1])
|
28
|
-
when 0xD then ChannelAftertouch.new(second_nibble, @data[1])
|
29
|
-
when 0xE then PitchBend.new(second_nibble, @data[1], @data[2])
|
30
|
-
when 0xF then case second_nibble
|
31
|
-
when 0x0 then SystemExclusive.new(*@data)
|
32
|
-
when 0x1..0x6 then SystemCommon.new(second_nibble, @data[1], @data[2])
|
33
|
-
when 0x8..0xF then SystemRealtime.new(second_nibble)
|
34
|
-
else nil
|
35
|
-
end
|
36
|
-
else nil
|
50
|
+
nibbles = get_nibbles
|
51
|
+
klass = MESSAGE_TYPE.find { |type| type::STATUS == nibbles[0] }
|
52
|
+
if klass == SystemMessage
|
53
|
+
build_system_message(nibbles[1])
|
54
|
+
else
|
55
|
+
klass.new(nibbles[1], *@data.drop(1))
|
37
56
|
end
|
38
|
-
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def get_nibbles
|
62
|
+
[
|
63
|
+
((@data.first & 0xF0) >> 4),
|
64
|
+
(@data.first & 0x0F)
|
65
|
+
]
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_system_message(id)
|
69
|
+
klass = SYSTEM_MESSAGE_TYPE.find do |type|
|
70
|
+
id == type::ID ||
|
71
|
+
(type::ID.kind_of?(Range) && type::ID.include?(id))
|
72
|
+
end
|
73
|
+
if klass == SystemExclusive
|
74
|
+
SystemExclusive.new(*@data)
|
75
|
+
else
|
76
|
+
klass.new(id, *@data.drop(1))
|
77
|
+
end
|
78
|
+
end
|
39
79
|
|
40
80
|
end
|
41
81
|
|
82
|
+
# Shortcut to Parser.parse
|
83
|
+
# @param [Array<Fixnum>, *Fixnum, String] args
|
84
|
+
# @return [MIDIMessage]
|
42
85
|
def self.parse(*args)
|
43
|
-
Parser.
|
86
|
+
Parser.parse(*args)
|
44
87
|
end
|
45
88
|
|
46
89
|
end
|
@@ -4,7 +4,7 @@ module MIDIMessage
|
|
4
4
|
module SystemExclusive
|
5
5
|
|
6
6
|
include MIDIMessage # this enables ..kind_of?(MIDIMessage)
|
7
|
-
|
7
|
+
|
8
8
|
def self.included(base)
|
9
9
|
base.send(:include, InstanceMethods)
|
10
10
|
end
|
@@ -15,9 +15,6 @@ module MIDIMessage
|
|
15
15
|
attr_accessor :node
|
16
16
|
attr_reader :address, :checksum
|
17
17
|
|
18
|
-
StartByte = 0xF0
|
19
|
-
EndByte = 0xF7
|
20
|
-
|
21
18
|
# an array of message parts. multiple byte parts will be represented as an array of bytes
|
22
19
|
def to_a(options = {})
|
23
20
|
omit = options[:omit] || []
|
@@ -56,20 +53,20 @@ module MIDIMessage
|
|
56
53
|
alias_method :to_bytestr, :to_hex_s
|
57
54
|
|
58
55
|
def name
|
59
|
-
|
56
|
+
SystemExclusive::DISPLAY_NAME
|
60
57
|
end
|
61
58
|
alias_method :verbose_name, :name
|
62
59
|
|
63
60
|
def start_byte
|
64
|
-
|
61
|
+
SystemExclusive::DELIMITER[:start]
|
65
62
|
end
|
66
63
|
|
67
64
|
def end_byte
|
68
|
-
|
65
|
+
SystemExclusive::DELIMITER[:finish]
|
69
66
|
end
|
70
67
|
|
71
68
|
def type_byte
|
72
|
-
self.class::
|
69
|
+
self.class::TYPE
|
73
70
|
end
|
74
71
|
|
75
72
|
# alternate method from
|
@@ -183,25 +180,41 @@ module MIDIMessage
|
|
183
180
|
|
184
181
|
end
|
185
182
|
|
186
|
-
|
187
|
-
def self.new(*bytes)
|
183
|
+
module Builder
|
188
184
|
|
189
|
-
|
190
|
-
end_status = bytes.pop
|
185
|
+
extend self
|
191
186
|
|
192
|
-
|
187
|
+
# Convert raw MIDI data to a SysEx message object
|
188
|
+
def build(*bytes)
|
189
|
+
if is_sysex?(bytes)
|
193
190
|
|
194
|
-
|
191
|
+
# if the 4th byte isn't status, we will just make this a Message object
|
192
|
+
# -- this may need some tweaking
|
193
|
+
message_class = get_message_class(bytes)
|
195
194
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
else
|
202
|
-
return Message.new(bytes)
|
195
|
+
if message_class.nil?
|
196
|
+
Message.new(bytes)
|
197
|
+
else
|
198
|
+
build_typed_message(message_class, bytes)
|
199
|
+
end
|
203
200
|
end
|
204
201
|
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
def get_message_class(bytes)
|
207
|
+
[Request, Command].find { |klass| klass::TYPE == bytes[3] }
|
208
|
+
end
|
209
|
+
|
210
|
+
def is_sysex?(bytes)
|
211
|
+
bytes.shift == SystemExclusive::DELIMITER[:start] &&
|
212
|
+
bytes.pop == SystemExclusive::DELIMITER[:finish]
|
213
|
+
end
|
214
|
+
|
215
|
+
# Build a SysEx message object of the given type using the given bytes
|
216
|
+
def build_typed_message(message_class, bytes)
|
217
|
+
bytes = bytes.dup
|
205
218
|
fixed_length_message_part = bytes.slice!(0,7)
|
206
219
|
|
207
220
|
manufacturer_id = fixed_length_message_part[0]
|
@@ -213,8 +226,15 @@ module MIDIMessage
|
|
213
226
|
value = bytes
|
214
227
|
|
215
228
|
node = Node.new(manufacturer_id, :model_id => model_id, :device_id => device_id)
|
216
|
-
|
229
|
+
message_class.new(address, value, :checksum => checksum, :node => node)
|
217
230
|
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
# Convert raw MIDI data to a SysEx message object
|
235
|
+
# Shortcut to Builder.build
|
236
|
+
def self.new(*bytes)
|
237
|
+
Builder.build(*bytes)
|
218
238
|
end
|
219
239
|
|
220
240
|
end
|
@@ -3,6 +3,8 @@ module MIDIMessage
|
|
3
3
|
# Common MIDI system message behavior
|
4
4
|
module SystemMessage
|
5
5
|
|
6
|
+
STATUS = 0xF
|
7
|
+
|
6
8
|
def self.included(base)
|
7
9
|
base.send(:include, Message)
|
8
10
|
end
|
@@ -13,7 +15,7 @@ module MIDIMessage
|
|
13
15
|
# @param [Fixnum] byte The byte to strip of a redundant 0xF
|
14
16
|
# @return [Fixnum] The remaining nibble
|
15
17
|
def strip_redundant_nibble(byte)
|
16
|
-
byte >
|
18
|
+
byte > STATUS ? (byte & 0x0F) : byte
|
17
19
|
end
|
18
20
|
|
19
21
|
end
|
data/test/constant_test.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require "helper"
|
2
2
|
|
3
|
-
class ConstantTest < Minitest::Test
|
3
|
+
class MIDIMessage::ConstantTest < Minitest::Test
|
4
4
|
|
5
5
|
context "Constant" do
|
6
6
|
|
@@ -33,6 +33,30 @@ class ConstantTest < Minitest::Test
|
|
33
33
|
|
34
34
|
end
|
35
35
|
|
36
|
+
context "Name" do
|
37
|
+
|
38
|
+
context ".underscore" do
|
39
|
+
|
40
|
+
should "convert string" do
|
41
|
+
@result = MIDIMessage::Constant::Name.underscore("Control Change")
|
42
|
+
refute_nil @result
|
43
|
+
assert_equal "control_change", @result
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
context ".match?" do
|
49
|
+
|
50
|
+
should "match string" do
|
51
|
+
assert MIDIMessage::Constant::Name.match?("Control Change", :control_change)
|
52
|
+
assert MIDIMessage::Constant::Name.match?("Note", :note)
|
53
|
+
assert MIDIMessage::Constant::Name.match?("System Common", :system_common)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
36
60
|
context "Group" do
|
37
61
|
|
38
62
|
context "#find" do
|
@@ -68,11 +92,65 @@ class ConstantTest < Minitest::Test
|
|
68
92
|
|
69
93
|
end
|
70
94
|
|
95
|
+
context "MessageBuilder" do
|
96
|
+
|
97
|
+
context "#new" do
|
98
|
+
|
99
|
+
context "note on" do
|
100
|
+
|
101
|
+
setup do
|
102
|
+
@group = MIDIMessage::Constant::Group.find(:note)
|
103
|
+
@map = @group.find("C3")
|
104
|
+
@builder = MIDIMessage::Constant::MessageBuilder.new(MIDIMessage::NoteOn, @map)
|
105
|
+
end
|
106
|
+
|
107
|
+
should "build correct note" do
|
108
|
+
@note = @builder.new
|
109
|
+
refute_nil @note
|
110
|
+
assert_equal "C3", @note.name
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
context "cc" do
|
116
|
+
|
117
|
+
setup do
|
118
|
+
@group = MIDIMessage::Constant::Group.find(:control_change)
|
119
|
+
@map = @group.find("Modulation Wheel")
|
120
|
+
@builder = MIDIMessage::Constant::MessageBuilder.new(MIDIMessage::ControlChange, @map)
|
121
|
+
end
|
122
|
+
|
123
|
+
should "build correct cc" do
|
124
|
+
@cc = @builder.new
|
125
|
+
refute_nil @cc
|
126
|
+
assert_equal "Modulation Wheel", @cc.name
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
context "Status" do
|
136
|
+
|
137
|
+
context ".find" do
|
138
|
+
|
139
|
+
should "find status" do
|
140
|
+
assert_equal 0x8, MIDIMessage::Constant::Status.find("Note Off")
|
141
|
+
assert_equal 0x9, MIDIMessage::Constant::Status.find("Note On")
|
142
|
+
assert_equal 0xB, MIDIMessage::Constant::Status["Control Change"]
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
71
149
|
context "Loader" do
|
72
150
|
|
73
151
|
context "DSL" do
|
74
152
|
|
75
|
-
context "
|
153
|
+
context ".find" do
|
76
154
|
|
77
155
|
context "note on" do
|
78
156
|
|
@@ -108,7 +186,7 @@ class ConstantTest < Minitest::Test
|
|
108
186
|
context "cc" do
|
109
187
|
|
110
188
|
setup do
|
111
|
-
@message = MIDIMessage::ControlChange
|
189
|
+
@message = MIDIMessage::ControlChange.find("Modulation Wheel").new(2, 0x20)
|
112
190
|
end
|
113
191
|
|
114
192
|
should "create message object" do
|
data/test/context_test.rb
CHANGED
@@ -1,129 +1,133 @@
|
|
1
1
|
require "helper"
|
2
2
|
|
3
|
-
class ContextTest < Minitest::Test
|
3
|
+
class MIDIMessage::ContextTest < Minitest::Test
|
4
4
|
|
5
5
|
context "Context" do
|
6
6
|
|
7
|
-
context "
|
7
|
+
context ".with" do
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
context "note off" do
|
10
|
+
|
11
|
+
setup do
|
12
|
+
@message = MIDIMessage.with(:channel => 0, :velocity => 64) do
|
13
|
+
note_off(55)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
should "create message object" do
|
18
|
+
assert_equal(0, @message.channel)
|
19
|
+
assert_equal(55, @message.note)
|
20
|
+
assert_equal(64, @message.velocity)
|
21
|
+
assert_equal([128, 55, 64], @message.to_a)
|
22
|
+
assert_equal("803740", @message.to_bytestr)
|
12
23
|
end
|
13
|
-
end
|
14
24
|
|
15
|
-
should "create message object" do
|
16
|
-
assert_equal(0, @message.channel)
|
17
|
-
assert_equal(55, @message.note)
|
18
|
-
assert_equal(64, @message.velocity)
|
19
|
-
assert_equal([128, 55, 64], @message.to_a)
|
20
|
-
assert_equal("803740", @message.to_bytestr)
|
21
25
|
end
|
22
26
|
|
23
|
-
|
27
|
+
context "note on" do
|
24
28
|
|
25
|
-
|
29
|
+
setup do
|
30
|
+
@message = MIDIMessage.with(:channel => 0, :velocity => 64) do
|
31
|
+
note_on(55)
|
32
|
+
end
|
33
|
+
end
|
26
34
|
|
27
|
-
|
28
|
-
|
29
|
-
|
35
|
+
should "create message object" do
|
36
|
+
assert_equal(0, @message.channel)
|
37
|
+
assert_equal(55, @message.note)
|
38
|
+
assert_equal(64, @message.velocity)
|
39
|
+
assert_equal([144, 55, 64], @message.to_a)
|
40
|
+
assert_equal("903740", @message.to_bytestr)
|
30
41
|
end
|
31
|
-
end
|
32
42
|
|
33
|
-
should "create message object" do
|
34
|
-
assert_equal(0, @message.channel)
|
35
|
-
assert_equal(55, @message.note)
|
36
|
-
assert_equal(64, @message.velocity)
|
37
|
-
assert_equal([144, 55, 64], @message.to_a)
|
38
|
-
assert_equal("903740", @message.to_bytestr)
|
39
43
|
end
|
40
44
|
|
41
|
-
|
45
|
+
context "cc" do
|
42
46
|
|
43
|
-
|
47
|
+
setup do
|
48
|
+
@message = MIDIMessage::Context.with(:channel => 2) do
|
49
|
+
control_change(0x20, 0x30)
|
50
|
+
end
|
51
|
+
end
|
44
52
|
|
45
|
-
|
46
|
-
|
47
|
-
|
53
|
+
should "create message object" do
|
54
|
+
assert_equal(@message.channel, 2)
|
55
|
+
assert_equal(0x20, @message.index)
|
56
|
+
assert_equal(0x30, @message.value)
|
57
|
+
assert_equal([0xB2, 0x20, 0x30], @message.to_a)
|
58
|
+
assert_equal("B22030", @message.to_bytestr)
|
48
59
|
end
|
49
|
-
end
|
50
60
|
|
51
|
-
should "create message object" do
|
52
|
-
assert_equal(@message.channel, 2)
|
53
|
-
assert_equal(0x20, @message.index)
|
54
|
-
assert_equal(0x30, @message.value)
|
55
|
-
assert_equal([0xB2, 0x20, 0x30], @message.to_a)
|
56
|
-
assert_equal("B22030", @message.to_bytestr)
|
57
61
|
end
|
58
62
|
|
59
|
-
|
63
|
+
context "polyphonic aftertouch" do
|
60
64
|
|
61
|
-
|
65
|
+
setup do
|
66
|
+
@message = MIDIMessage::Context.with(:channel => 1) do
|
67
|
+
polyphonic_aftertouch(0x40, 0x40)
|
68
|
+
end
|
69
|
+
end
|
62
70
|
|
63
|
-
|
64
|
-
|
65
|
-
|
71
|
+
should "create message object" do
|
72
|
+
assert_equal(1, @message.channel)
|
73
|
+
assert_equal(0x40, @message.note)
|
74
|
+
assert_equal(0x40, @message.value)
|
75
|
+
assert_equal([0xA1, 0x40, 0x40], @message.to_a)
|
76
|
+
assert_equal("A14040", @message.to_bytestr)
|
66
77
|
end
|
67
|
-
end
|
68
78
|
|
69
|
-
should "create message object" do
|
70
|
-
assert_equal(1, @message.channel)
|
71
|
-
assert_equal(0x40, @message.note)
|
72
|
-
assert_equal(0x40, @message.value)
|
73
|
-
assert_equal([0xA1, 0x40, 0x40], @message.to_a)
|
74
|
-
assert_equal("A14040", @message.to_bytestr)
|
75
79
|
end
|
76
80
|
|
77
|
-
|
81
|
+
context "program change" do
|
78
82
|
|
79
|
-
|
83
|
+
setup do
|
84
|
+
@message = MIDIMessage.with(:channel => 3) do
|
85
|
+
program_change(0x40)
|
86
|
+
end
|
87
|
+
end
|
80
88
|
|
81
|
-
|
82
|
-
|
83
|
-
|
89
|
+
should "create message object" do
|
90
|
+
assert_equal(3, @message.channel)
|
91
|
+
assert_equal(0x40, @message.program)
|
92
|
+
assert_equal([0xC3, 0x40], @message.to_a)
|
93
|
+
assert_equal("C340", @message.to_bytestr)
|
84
94
|
end
|
85
|
-
end
|
86
95
|
|
87
|
-
should "create message object" do
|
88
|
-
assert_equal(3, @message.channel)
|
89
|
-
assert_equal(0x40, @message.program)
|
90
|
-
assert_equal([0xC3, 0x40], @message.to_a)
|
91
|
-
assert_equal("C340", @message.to_bytestr)
|
92
96
|
end
|
93
97
|
|
94
|
-
|
98
|
+
context "channel aftertouch" do
|
95
99
|
|
96
|
-
|
100
|
+
setup do
|
101
|
+
@message = MIDIMessage.with(:channel => 3) do
|
102
|
+
channel_aftertouch(0x50)
|
103
|
+
end
|
104
|
+
end
|
97
105
|
|
98
|
-
|
99
|
-
|
100
|
-
|
106
|
+
should "create message object" do
|
107
|
+
assert_equal(3, @message.channel)
|
108
|
+
assert_equal(0x50, @message.value)
|
109
|
+
assert_equal([0xD3, 0x50], @message.to_a)
|
110
|
+
assert_equal("D350", @message.to_bytestr)
|
101
111
|
end
|
102
|
-
end
|
103
112
|
|
104
|
-
should "create message object" do
|
105
|
-
assert_equal(3, @message.channel)
|
106
|
-
assert_equal(0x50, @message.value)
|
107
|
-
assert_equal([0xD3, 0x50], @message.to_a)
|
108
|
-
assert_equal("D350", @message.to_bytestr)
|
109
113
|
end
|
110
114
|
|
111
|
-
|
115
|
+
context "pitch bend" do
|
112
116
|
|
113
|
-
|
117
|
+
setup do
|
118
|
+
@message = MIDIMessage.with(:channel => 0) do
|
119
|
+
pitch_bend(0x50, 0xA0)
|
120
|
+
end
|
121
|
+
end
|
114
122
|
|
115
|
-
|
116
|
-
|
117
|
-
|
123
|
+
should "create message object" do
|
124
|
+
assert_equal(0, @message.channel)
|
125
|
+
assert_equal(0x50, @message.low)
|
126
|
+
assert_equal(0xA0, @message.high)
|
127
|
+
assert_equal([0xE0, 0x50, 0xA0], @message.to_a)
|
128
|
+
assert_equal("E050A0", @message.to_bytestr)
|
118
129
|
end
|
119
|
-
end
|
120
130
|
|
121
|
-
should "create message object" do
|
122
|
-
assert_equal(0, @message.channel)
|
123
|
-
assert_equal(0x50, @message.low)
|
124
|
-
assert_equal(0xA0, @message.high)
|
125
|
-
assert_equal([0xE0, 0x50, 0xA0], @message.to_a)
|
126
|
-
assert_equal("E050A0", @message.to_bytestr)
|
127
131
|
end
|
128
132
|
|
129
133
|
end
|