midi-message 0.2.2
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.
- data/LICENSE +13 -0
- data/README.rdoc +100 -0
- data/TODO +0 -0
- data/lib/midi-message/channel_message.rb +179 -0
- data/lib/midi-message/constant.rb +61 -0
- data/lib/midi-message/context.rb +104 -0
- data/lib/midi-message/note_message.rb +70 -0
- data/lib/midi-message/parser.rb +48 -0
- data/lib/midi-message/process/filter.rb +58 -0
- data/lib/midi-message/process/limit.rb +31 -0
- data/lib/midi-message/process/processor.rb +30 -0
- data/lib/midi-message/process/transpose.rb +30 -0
- data/lib/midi-message/short_message.rb +119 -0
- data/lib/midi-message/system_exclusive.rb +202 -0
- data/lib/midi-message/system_message.rb +47 -0
- data/lib/midi-message/type_conversion.rb +47 -0
- data/lib/midi-message.rb +37 -0
- data/lib/midi.yml +337 -0
- data/test/helper.rb +11 -0
- data/test/test_constants.rb +48 -0
- data/test/test_context.rb +86 -0
- data/test/test_filter.rb +95 -0
- data/test/test_limit.rb +39 -0
- data/test/test_mutability.rb +28 -0
- data/test/test_parser.rb +72 -0
- data/test/test_processor.rb +19 -0
- data/test/test_short_message.rb +123 -0
- data/test/test_system_exclusive.rb +100 -0
- data/test/test_transpose.rb +39 -0
- metadata +74 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module MIDIMessage
|
4
|
+
|
5
|
+
module Process
|
6
|
+
|
7
|
+
class Transpose
|
8
|
+
|
9
|
+
include Processor
|
10
|
+
|
11
|
+
attr_reader :factor, :property
|
12
|
+
|
13
|
+
def initialize(message, prop, factor, options = {})
|
14
|
+
@factor = factor
|
15
|
+
@message = message
|
16
|
+
@property = prop
|
17
|
+
initialize_processor(message)
|
18
|
+
end
|
19
|
+
|
20
|
+
def process
|
21
|
+
val = @message.send(@property)
|
22
|
+
@message.send("#{@property}=", val + @factor)
|
23
|
+
@message
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
|
4
|
+
module MIDIMessage
|
5
|
+
|
6
|
+
# common behavior amongst all Message types
|
7
|
+
module ShortMessage
|
8
|
+
|
9
|
+
attr_reader :name,
|
10
|
+
:status,
|
11
|
+
:verbose_name
|
12
|
+
|
13
|
+
def initialize_short_message(status_nibble_1, status_nibble_2)
|
14
|
+
@status = [status_nibble_1, status_nibble_2]
|
15
|
+
populate_using_const
|
16
|
+
end
|
17
|
+
|
18
|
+
# byte array representation of the object eg [0x90, 0x40, 0x40] for NoteOn(0x40, 0x40)
|
19
|
+
def to_a
|
20
|
+
data = @data.nil? ? [] : [@data[0], @data[1]]
|
21
|
+
[(@status[0] << 4) + @status[1], *data].compact
|
22
|
+
end
|
23
|
+
alias_method :to_byte_a, :to_a
|
24
|
+
alias_method :to_byte_array, :to_a
|
25
|
+
alias_method :to_bytes, :to_a
|
26
|
+
|
27
|
+
# string representation of the object's bytes eg "904040" for NoteOn(0x40, 0x40)
|
28
|
+
def to_hex_s
|
29
|
+
TypeConversion.numeric_byte_array_to_hex_string(to_a)
|
30
|
+
end
|
31
|
+
alias_method :to_bytestr, :to_hex_s
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def self.included(base)
|
36
|
+
base.extend(ClassMethods)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update
|
40
|
+
populate_using_const
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# this will populate message metadata with information gathered from midi.yml
|
46
|
+
def populate_using_const
|
47
|
+
const_group_name = self.class.display_name
|
48
|
+
group_name_alias = self.class.constants
|
49
|
+
prop = self.class.map_constants_to
|
50
|
+
val = self.send(prop) unless prop.nil?
|
51
|
+
val ||= @status[1] # default property to use for constants
|
52
|
+
group = ConstantGroup[group_name_alias] || ConstantGroup[const_group_name]
|
53
|
+
unless group.nil?
|
54
|
+
const = group.find_by_value(val)
|
55
|
+
unless const.nil?
|
56
|
+
@const = const
|
57
|
+
@name = @const.nil? ? const.key : @const.key
|
58
|
+
@verbose_name = "#{self.class.display_name}: #{@name}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module ClassMethods
|
64
|
+
|
65
|
+
attr_reader :display_name, :constants, :map_constants_to
|
66
|
+
|
67
|
+
def get_constant(name)
|
68
|
+
key = @constants || @display_name
|
69
|
+
unless key.nil?
|
70
|
+
group = ConstantGroup[key]
|
71
|
+
group.find(name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# this returns a builder for the class, preloaded with the selected const
|
76
|
+
def [](const_name)
|
77
|
+
const = get_constant(const_name)
|
78
|
+
MessageBuilder.new(self, const) unless const.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
def use_display_name(name)
|
82
|
+
@display_name = name
|
83
|
+
end
|
84
|
+
|
85
|
+
def use_constants(name, options = {})
|
86
|
+
@map_constants_to = options[:for]
|
87
|
+
@constants = name
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
class MessageBuilder
|
95
|
+
|
96
|
+
def initialize(klass, const)
|
97
|
+
@klass = klass
|
98
|
+
@const = const
|
99
|
+
end
|
100
|
+
|
101
|
+
def new(*a)
|
102
|
+
a.last.kind_of?(Hash) ? a.last[:const] = @const : a.push(:const => @const)
|
103
|
+
@klass.new(*a)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
# shortcuts for dealing with message status
|
109
|
+
module Status
|
110
|
+
|
111
|
+
# this returns the value of the Status constant with the name status_name
|
112
|
+
def self.[](status_name)
|
113
|
+
const = Constant.find("Status", status_name)
|
114
|
+
const.value unless const.nil?
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
|
4
|
+
module MIDIMessage
|
5
|
+
|
6
|
+
# MIDI System-Exclusive Messages (SysEx)
|
7
|
+
module SystemExclusive
|
8
|
+
|
9
|
+
# basic SysEx data that a message class will contain
|
10
|
+
module Base
|
11
|
+
|
12
|
+
attr_accessor :node
|
13
|
+
attr_reader :address,
|
14
|
+
:checksum
|
15
|
+
|
16
|
+
StartByte = 0xF0
|
17
|
+
EndByte = 0xF7
|
18
|
+
|
19
|
+
# an array of message parts. multiple byte parts will be represented as an array of bytes
|
20
|
+
def to_a
|
21
|
+
# this may need to be cached when properties are updated
|
22
|
+
# might be worth benchmarking
|
23
|
+
[
|
24
|
+
self.class::StartByte,
|
25
|
+
@node.manufacturer_id,
|
26
|
+
@node.device_id, # (@device_id || @node.device_id) ?? dunno
|
27
|
+
@node.model_id,
|
28
|
+
type_byte,
|
29
|
+
[address].flatten,
|
30
|
+
[value].flatten,
|
31
|
+
checksum,
|
32
|
+
self.class::EndByte
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
# a flat array of message bytes
|
37
|
+
def to_numeric_byte_array
|
38
|
+
to_a.flatten
|
39
|
+
end
|
40
|
+
alias_method :to_numeric_bytes, :to_numeric_byte_array
|
41
|
+
alias_method :to_byte_array, :to_numeric_byte_array
|
42
|
+
alias_method :to_bytes, :to_numeric_byte_array
|
43
|
+
alias_method :to_byte_a, :to_numeric_byte_array
|
44
|
+
|
45
|
+
# string representation of the object's bytes
|
46
|
+
def to_hex_s
|
47
|
+
to_bytes.map { |b| s = b.to_s(16); s.length.eql?(1) ? "0#{s}" : s }.join.upcase
|
48
|
+
end
|
49
|
+
alias_method :to_bytestr, :to_hex_s
|
50
|
+
|
51
|
+
def name
|
52
|
+
"System Exclusive"
|
53
|
+
end
|
54
|
+
alias_method :verbose_name, :name
|
55
|
+
|
56
|
+
def type_byte
|
57
|
+
self.class::TypeByte
|
58
|
+
end
|
59
|
+
|
60
|
+
# alternate method from
|
61
|
+
# http://www.2writers.com/eddie/TutSysEx.htm
|
62
|
+
def checksum
|
63
|
+
sum = (address + [value].flatten).inject { |a, b| a + b }
|
64
|
+
(128 - sum.divmod(128)[1])
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def initialize_sysex(address, options = {})
|
70
|
+
@node = options[:node]
|
71
|
+
@checksum = options[:checksum]
|
72
|
+
@address = address
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# A SysEx command message
|
78
|
+
# a command message is identified by having a status byte equal to 0x12
|
79
|
+
#
|
80
|
+
class Command
|
81
|
+
|
82
|
+
include Base
|
83
|
+
|
84
|
+
attr_reader :data
|
85
|
+
alias_method :value, :data
|
86
|
+
#alias_method :value=, :data=
|
87
|
+
|
88
|
+
TypeByte = 0x12
|
89
|
+
|
90
|
+
def initialize(address, data, options = {})
|
91
|
+
# store as a byte if it's a single byte
|
92
|
+
@data = (data.kind_of?(Array) && data.length.eql?(1)) ? data[0] : data
|
93
|
+
initialize_sysex(address, options)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
# A SysEx request message
|
99
|
+
# A request message is identified by having a status byte equal to 0x11
|
100
|
+
#
|
101
|
+
class Request
|
102
|
+
|
103
|
+
include Base
|
104
|
+
|
105
|
+
attr_reader :size
|
106
|
+
|
107
|
+
alias_method :value, :size
|
108
|
+
|
109
|
+
TypeByte = 0x11
|
110
|
+
|
111
|
+
def initialize(address, size, options = {})
|
112
|
+
self.size = (size.kind_of?(Array) && size.length.eql?(1)) ? size[0] : size
|
113
|
+
initialize_sysex(address, options)
|
114
|
+
end
|
115
|
+
|
116
|
+
def size=(val)
|
117
|
+
# accepts a Numeric or Array but
|
118
|
+
# must always store value as an array of three bytes
|
119
|
+
size = []
|
120
|
+
if val.kind_of?(Array) && val.size <= 3
|
121
|
+
size = val
|
122
|
+
elsif val.kind_of?(Numeric) && (((val + 1) / 247) <= 2)
|
123
|
+
size = []
|
124
|
+
div, mod = *val.divmod(247)
|
125
|
+
size << mod unless mod.zero?
|
126
|
+
div.times { size << 247 }
|
127
|
+
end
|
128
|
+
(3 - size.size).times { size.unshift 0 }
|
129
|
+
@size = size
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# The SystemExclusive::Node represents a destination for a message. For example a hardware
|
136
|
+
# synthesizer or sampler
|
137
|
+
#
|
138
|
+
class Node
|
139
|
+
|
140
|
+
attr_accessor :device_id
|
141
|
+
attr_reader :manufacturer_id, :model_id
|
142
|
+
|
143
|
+
def initialize(manufacturer, options = {})
|
144
|
+
@device_id = options[:device_id]
|
145
|
+
@model_id = options[:model_id]
|
146
|
+
@manufacturer_id = manufacturer.kind_of?(Numeric) ? manufacturer : Constant.find("Manufacturer", manufacturer).value
|
147
|
+
end
|
148
|
+
|
149
|
+
# this message takes a prototype message, copies it, and returns the copy with its node set
|
150
|
+
# to this node
|
151
|
+
def new_message_from(prototype_message)
|
152
|
+
copy = prototype_message.clone
|
153
|
+
copy.node = self
|
154
|
+
copy
|
155
|
+
end
|
156
|
+
|
157
|
+
# create a new Command message associated with this node
|
158
|
+
def command(*a)
|
159
|
+
command = Command.new(*a)
|
160
|
+
command.node = self
|
161
|
+
command
|
162
|
+
end
|
163
|
+
|
164
|
+
# create a new Request message associated with this node
|
165
|
+
def request(*a)
|
166
|
+
request = Request.new(*a)
|
167
|
+
request.node = self
|
168
|
+
request
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
# convert raw MIDI data to SysEx message objects
|
174
|
+
def self.new(*bytes)
|
175
|
+
|
176
|
+
start_status = bytes.shift
|
177
|
+
end_status = bytes.pop
|
178
|
+
|
179
|
+
return nil unless start_status.eql?(0xF0) && end_status.eql?(0xF7)
|
180
|
+
|
181
|
+
fixed_length_message_part = bytes.slice!(0,7)
|
182
|
+
|
183
|
+
manufacturer_id = fixed_length_message_part[0]
|
184
|
+
device_id = fixed_length_message_part[1]
|
185
|
+
model_id = fixed_length_message_part[2]
|
186
|
+
|
187
|
+
msg_class = case fixed_length_message_part[3]
|
188
|
+
when 0x11 then Request
|
189
|
+
when 0x12 then Command
|
190
|
+
end
|
191
|
+
|
192
|
+
address = fixed_length_message_part.slice(4,3)
|
193
|
+
checksum = bytes.slice!((bytes.length - 1), 1)
|
194
|
+
value = bytes
|
195
|
+
|
196
|
+
node = Node.new(manufacturer_id, :model_id => model_id, :device_id => device_id)
|
197
|
+
msg_class.new(address, value, :checksum => checksum, :node => node)
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
|
4
|
+
module MIDIMessage
|
5
|
+
|
6
|
+
#
|
7
|
+
# MIDI System-Common message
|
8
|
+
#
|
9
|
+
class SystemCommon
|
10
|
+
|
11
|
+
include ShortMessage
|
12
|
+
use_display_name 'System Common'
|
13
|
+
|
14
|
+
attr_reader :data
|
15
|
+
|
16
|
+
def initialize(*a)
|
17
|
+
options = a.last.kind_of?(Hash) ? a.pop : {}
|
18
|
+
@const = options[:const]
|
19
|
+
@data = [a[1], a[2]]
|
20
|
+
second_nibble = @const.nil? ? a[0] : @const.value
|
21
|
+
initialize_short_message(0xF, second_nibble)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# MIDI System-Realtime message
|
28
|
+
#
|
29
|
+
class SystemRealtime
|
30
|
+
|
31
|
+
include ShortMessage
|
32
|
+
use_display_name 'System Realtime'
|
33
|
+
|
34
|
+
def initialize(*a)
|
35
|
+
options = a.last.kind_of?(Hash) ? a.pop : {}
|
36
|
+
@const = options[:const]
|
37
|
+
id = @const.nil? ? a[0] : @const.value
|
38
|
+
initialize_short_message(0xF, id)
|
39
|
+
end
|
40
|
+
|
41
|
+
def id
|
42
|
+
@status[1]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
module MIDIMessage
|
4
|
+
|
5
|
+
# this is a helper for converting nibbles and bytes
|
6
|
+
module TypeConversion
|
7
|
+
|
8
|
+
def self.hex_chars_to_numeric_byte_array(nibbles)
|
9
|
+
nibbles = nibbles.dup
|
10
|
+
# get rid of last nibble if there's an odd number
|
11
|
+
# it will be processed later anyway
|
12
|
+
nibbles.slice!(nibbles.length-2, 1) if nibbles.length.odd?
|
13
|
+
bytes = []
|
14
|
+
while !(nibs = nibbles.slice!(0,2)).empty?
|
15
|
+
byte = (nibs[0].hex << 4) + nibs[1].hex
|
16
|
+
bytes << byte
|
17
|
+
end
|
18
|
+
bytes
|
19
|
+
end
|
20
|
+
|
21
|
+
# convert byte str to byte array
|
22
|
+
def self.hex_string_to_numeric_byte_array(str)
|
23
|
+
str = str.dup
|
24
|
+
bytes = []
|
25
|
+
until str.eql?("")
|
26
|
+
bytes << str.slice!(0, 2).hex
|
27
|
+
end
|
28
|
+
bytes
|
29
|
+
end
|
30
|
+
|
31
|
+
# converts a string of hex digits to bytes
|
32
|
+
def self.hex_str_to_hex_chars(str)
|
33
|
+
str.split(//)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.numeric_byte_array_to_hex_string(bytes)
|
37
|
+
bytes.map { |b| s = b.to_s(16); s.length.eql?(1) ? "0#{s}" : s }.join.upcase
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.numeric_byte_to_hex_chars(num)
|
41
|
+
[((num & 0xF0) >> 4), (num & 0x0F)].map { |n| n.to_s(16) }
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/lib/midi-message.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# MIDI Messages in Ruby
|
4
|
+
# (c)2011 Ari Russo and licensed under the Apache 2.0 License
|
5
|
+
#
|
6
|
+
module MIDIMessage
|
7
|
+
|
8
|
+
module Process
|
9
|
+
end
|
10
|
+
|
11
|
+
VERSION = "0.2.2"
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'yaml'
|
16
|
+
|
17
|
+
# messages
|
18
|
+
|
19
|
+
require 'midi-message/short_message'
|
20
|
+
require 'midi-message/channel_message'
|
21
|
+
require 'midi-message/constant'
|
22
|
+
require 'midi-message/context'
|
23
|
+
require 'midi-message/note_message'
|
24
|
+
require 'midi-message/parser'
|
25
|
+
require 'midi-message/system_message'
|
26
|
+
require 'midi-message/system_exclusive'
|
27
|
+
require 'midi-message/type_conversion'
|
28
|
+
|
29
|
+
# message processors
|
30
|
+
|
31
|
+
# modules
|
32
|
+
require "midi-message/process/processor"
|
33
|
+
|
34
|
+
# classes
|
35
|
+
require "midi-message/process/filter"
|
36
|
+
require "midi-message/process/limit"
|
37
|
+
require "midi-message/process/transpose"
|