midi-message 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|