midi-events 0.5.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/.gitignore +10 -0
- data/Gemfile +8 -0
- data/LICENSE +674 -0
- data/LICENSE.midi-message +13 -0
- data/README.md +170 -0
- data/Rakefile +10 -0
- data/examples/constants.rb +37 -0
- data/examples/context.rb +25 -0
- data/examples/melody.rb +27 -0
- data/examples/short_messages.rb +33 -0
- data/examples/sysex.rb +56 -0
- data/lib/midi-events/channel_message.rb +152 -0
- data/lib/midi-events/constant.rb +260 -0
- data/lib/midi-events/context.rb +161 -0
- data/lib/midi-events/message.rb +62 -0
- data/lib/midi-events/messages.rb +215 -0
- data/lib/midi-events/note_message.rb +40 -0
- data/lib/midi-events/system_exclusive.rb +244 -0
- data/lib/midi-events/system_message.rb +19 -0
- data/lib/midi-events/type_conversion.rb +79 -0
- data/lib/midi-events.rb +27 -0
- data/lib/midi.yml +338 -0
- data/midi-events.gemspec +22 -0
- metadata +67 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
module MIDIEvents
|
2
|
+
|
3
|
+
# Refer to a MIDI message by its usage
|
4
|
+
# eg *C4* for MIDI note *60* or *Bank Select* for MIDI control change *0*
|
5
|
+
module Constant
|
6
|
+
|
7
|
+
# Get a Mapping object for the specified constant
|
8
|
+
# @param [Symbol, String] group_name
|
9
|
+
# @param [String] const_name
|
10
|
+
# @return [MIDIEvents::Constant::Map, nil]
|
11
|
+
def self.find(group_name, const_name)
|
12
|
+
group = Group[group_name]
|
13
|
+
group.find(const_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get the value of the specified constant
|
17
|
+
# @param [Symbol, String] group_name
|
18
|
+
# @param [String] const_name
|
19
|
+
# @return [Object]
|
20
|
+
def self.value(group_name, const_name)
|
21
|
+
map = find(group_name, const_name)
|
22
|
+
map.value
|
23
|
+
end
|
24
|
+
|
25
|
+
module Name
|
26
|
+
|
27
|
+
extend self
|
28
|
+
|
29
|
+
# eg "Control Change" -> "control_change"
|
30
|
+
# @param [Symbol, String] string
|
31
|
+
# @return [String]
|
32
|
+
def underscore(string)
|
33
|
+
string.to_s.downcase.gsub(/(\ )+/, "_")
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [Symbol, String] key
|
37
|
+
# @param [Symbol, String] other
|
38
|
+
# @return [Boolean]
|
39
|
+
def match?(key, other)
|
40
|
+
match_key = key.to_s.downcase
|
41
|
+
[match_key, Name.underscore(match_key)].include?(other.to_s.downcase)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
# MIDI Constant container
|
47
|
+
class Group
|
48
|
+
|
49
|
+
attr_reader :constants, :key
|
50
|
+
|
51
|
+
# @param [String] key
|
52
|
+
# @param [Hash] constants
|
53
|
+
def initialize(key, constants)
|
54
|
+
@key = key
|
55
|
+
@constants = constants.map { |k, v| Constant::Map.new(k, v) }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Find a constant by its name
|
59
|
+
# @param [String, Symbol] name
|
60
|
+
# @return [Constant::Map]
|
61
|
+
def find(name)
|
62
|
+
@constants.find { |const| Name.match?(const.key, name) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Find a constant by its value
|
66
|
+
# @param [Object] value
|
67
|
+
# @return [Constant::Map]
|
68
|
+
def find_by_value(value)
|
69
|
+
@constants.find { |const| Name.match?(const.value, value) }
|
70
|
+
end
|
71
|
+
|
72
|
+
class << self
|
73
|
+
|
74
|
+
# All constant groups
|
75
|
+
# @return [Array<ConstantGroup>]
|
76
|
+
def all
|
77
|
+
ensure_initialized
|
78
|
+
@groups
|
79
|
+
end
|
80
|
+
|
81
|
+
# Find a constant group by its key
|
82
|
+
# @param [String, Symbol] key
|
83
|
+
# @return [ConstantGroup]
|
84
|
+
def find(key)
|
85
|
+
ensure_initialized
|
86
|
+
@groups.find { |group| Name.match?(group.key, key) }
|
87
|
+
end
|
88
|
+
alias_method :[], :find
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Lazy initialize
|
93
|
+
# @return [Boolean]
|
94
|
+
def ensure_initialized
|
95
|
+
populate_dictionary | populate_groups
|
96
|
+
end
|
97
|
+
|
98
|
+
# Populate the dictionary of constants
|
99
|
+
# @return [Boolean]
|
100
|
+
def populate_dictionary
|
101
|
+
if @dict.nil?
|
102
|
+
file = File.expand_path('../../midi.yml', __FILE__)
|
103
|
+
@dict = YAML.load_file(file)
|
104
|
+
@dict.freeze
|
105
|
+
true
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Populate the constant groups using the dictionary
|
110
|
+
# @return [Boolean]
|
111
|
+
def populate_groups
|
112
|
+
if @groups.nil? && !@dict.nil?
|
113
|
+
@groups = @dict.map { |k, v| new(k, v) }
|
114
|
+
true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
# The mapping of a constant key to its value eg "Note On" => 0x9
|
123
|
+
class Map
|
124
|
+
|
125
|
+
attr_reader :key, :value
|
126
|
+
|
127
|
+
# @param [String] key
|
128
|
+
# @param [Object] value
|
129
|
+
def initialize(key, value)
|
130
|
+
@key = key
|
131
|
+
@value = value
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
class MessageBuilder
|
137
|
+
|
138
|
+
# @param [MIDIEvents] klass The message class to build
|
139
|
+
# @param [MIDIEvents::Constant::Map] const The constant to build the message with
|
140
|
+
def initialize(klass, const)
|
141
|
+
@klass = klass
|
142
|
+
@const = const
|
143
|
+
end
|
144
|
+
|
145
|
+
# @param [*Object] args
|
146
|
+
# @return [Message]
|
147
|
+
def new(*args)
|
148
|
+
args = args.dup
|
149
|
+
args.last.kind_of?(Hash) ? args.last[:const] = @const : args.push(:const => @const)
|
150
|
+
@klass.new(*args)
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
# Shortcuts for dealing with message status
|
156
|
+
module Status
|
157
|
+
|
158
|
+
extend self
|
159
|
+
|
160
|
+
# The value of the Status constant with the name status_name
|
161
|
+
# @param [String] status_name The key to use to look up a constant value
|
162
|
+
# @return [String] The constant value that was looked up
|
163
|
+
def find(status_name)
|
164
|
+
const = Constant.find("Status", status_name)
|
165
|
+
const.value unless const.nil?
|
166
|
+
end
|
167
|
+
alias_method :[], :find
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
# Loading constants from the spec file into messages
|
172
|
+
module Loader
|
173
|
+
|
174
|
+
extend self
|
175
|
+
|
176
|
+
# Get the index of the constant from the given message's type
|
177
|
+
# @param [Message] message
|
178
|
+
# @return [Fixnum]
|
179
|
+
def get_index(message)
|
180
|
+
key = message.class.constant_property
|
181
|
+
message.class.properties.index(key) || 0
|
182
|
+
end
|
183
|
+
|
184
|
+
# Used to populate message metadata with information gathered from midi.yml
|
185
|
+
# @param [Message] message
|
186
|
+
# @return [Hash, nil]
|
187
|
+
def get_info(message)
|
188
|
+
const_group_name = message.class.display_name
|
189
|
+
group_name_alias = message.class.constant_name
|
190
|
+
property = message.class.constant_property
|
191
|
+
value = message.send(property) unless property.nil?
|
192
|
+
value ||= message.status[1] # default property to use for constants
|
193
|
+
group = Constant::Group[group_name_alias] || Constant::Group[const_group_name]
|
194
|
+
unless group.nil?
|
195
|
+
unless (const = group.find_by_value(value)).nil?
|
196
|
+
{
|
197
|
+
:const => const,
|
198
|
+
:name => const.key,
|
199
|
+
:verbose_name => "#{message.class.display_name}: #{const.key}"
|
200
|
+
}
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# DSL type class methods for loading constants into messages
|
206
|
+
module DSL
|
207
|
+
|
208
|
+
# Find a constant value in this class's group for the passed in key
|
209
|
+
# @param [String] name The constant key
|
210
|
+
# @return [String] The constant value
|
211
|
+
def get_constant(name)
|
212
|
+
key = constant_name || display_name
|
213
|
+
unless key.nil?
|
214
|
+
group = Group[key]
|
215
|
+
group.find(name)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# @return [String]
|
220
|
+
def display_name
|
221
|
+
const_get("DISPLAY_NAME") if const_defined?("DISPLAY_NAME")
|
222
|
+
end
|
223
|
+
|
224
|
+
# @return [Hash]
|
225
|
+
def constant_map
|
226
|
+
const_get("CONSTANT") if const_defined?("CONSTANT")
|
227
|
+
end
|
228
|
+
|
229
|
+
# @return [String]
|
230
|
+
def constant_name
|
231
|
+
constant_map.keys.first unless constant_map.nil?
|
232
|
+
end
|
233
|
+
|
234
|
+
# @return [Symbol]
|
235
|
+
def constant_property
|
236
|
+
constant_map[constant_name] unless constant_map.nil?
|
237
|
+
end
|
238
|
+
|
239
|
+
# Get the status nibble for this particular message type
|
240
|
+
# @return [Fixnum] The status nibble
|
241
|
+
def type_for_status
|
242
|
+
Constant::Status[display_name]
|
243
|
+
end
|
244
|
+
|
245
|
+
# This returns a MessageBuilder for the class, preloaded with the selected const
|
246
|
+
# @param [String, Symbol] const_name The constant key to use to build the message
|
247
|
+
# @return [MIDIEvents::MessageBuilder] A MessageBuilder object for the passed in constant
|
248
|
+
def find(const_name)
|
249
|
+
const = get_constant(const_name.to_s)
|
250
|
+
MessageBuilder.new(self, const) unless const.nil?
|
251
|
+
end
|
252
|
+
alias_method :[], :find
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module MIDIEvents
|
2
|
+
|
3
|
+
# A DSL for instantiating message objects
|
4
|
+
class Context
|
5
|
+
|
6
|
+
attr_accessor :channel, :velocity
|
7
|
+
|
8
|
+
# Open a context with the given options
|
9
|
+
# @param [Hash] options
|
10
|
+
# @param [Proc] block
|
11
|
+
# @option options [Fixnum] :channel
|
12
|
+
# @option options [Fixnum] :velocity
|
13
|
+
def self.with(options = {}, &block)
|
14
|
+
new(options, &block).instance_eval(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Hash] options
|
18
|
+
# @option options [Fixnum] :channel
|
19
|
+
# @option options [Fixnum] :velocity
|
20
|
+
def initialize(options = {})
|
21
|
+
@channel = options[:channel]
|
22
|
+
@velocity = options[:velocity]
|
23
|
+
end
|
24
|
+
|
25
|
+
# A note off message
|
26
|
+
# @param [Fixnum, String] note
|
27
|
+
# @param [Hash] options
|
28
|
+
# @option options [Fixnum] :channel
|
29
|
+
# @option options [Fixnum] :velocity
|
30
|
+
def note_off(note, options = {})
|
31
|
+
channel = options[:channel] || @channel
|
32
|
+
velocity = options[:velocity] || @velocity
|
33
|
+
raise 'note_off requires both channel and velocity' if channel.nil? || velocity.nil?
|
34
|
+
|
35
|
+
if note.is_a?(String)
|
36
|
+
NoteOff[note].new(channel, velocity, options)
|
37
|
+
else
|
38
|
+
NoteOff.new(channel, note, velocity, options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
alias_method :NoteOff, :note_off
|
42
|
+
|
43
|
+
# A note on message
|
44
|
+
# @param [Fixnum, String] note
|
45
|
+
# @param [Hash] options
|
46
|
+
# @option options [Fixnum] :channel
|
47
|
+
# @option options [Fixnum] :velocity
|
48
|
+
def note_on(note, options = {})
|
49
|
+
channel = options[:channel] || @channel
|
50
|
+
velocity = options[:velocity] || @velocity
|
51
|
+
raise 'note_on requires both channel and velocity' if channel.nil? || velocity.nil?
|
52
|
+
|
53
|
+
if note.is_a?(String)
|
54
|
+
NoteOn[note].new(channel, velocity, options)
|
55
|
+
else
|
56
|
+
NoteOn.new(channel, note, velocity, options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
alias_method :NoteOn, :note_on
|
60
|
+
|
61
|
+
# A program change message
|
62
|
+
# @param [Fixnum, String] program
|
63
|
+
# @param [Hash] options
|
64
|
+
# @option options [Fixnum] :channel
|
65
|
+
def program_change(program, options = {})
|
66
|
+
channel = options[:channel] || @channel
|
67
|
+
raise 'program_change requires channel' if channel.nil?
|
68
|
+
|
69
|
+
if program.is_a?(String)
|
70
|
+
ProgramChange[program].new(channel, options)
|
71
|
+
else
|
72
|
+
ProgramChange.new(channel, program, options)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
alias_method :ProgramChange, :program_change
|
76
|
+
|
77
|
+
# A control change message
|
78
|
+
# @param [Fixnum, String] index
|
79
|
+
# @param [Fixnum] value
|
80
|
+
# @param [Hash] options
|
81
|
+
# @option options [Fixnum] :channel
|
82
|
+
# @option options [Fixnum] :velocity
|
83
|
+
def control_change(index, value, options = {})
|
84
|
+
channel = options[:channel] || @channel
|
85
|
+
raise 'control_change requires channel' if channel.nil?
|
86
|
+
|
87
|
+
if index.is_a?(String)
|
88
|
+
ControlChange[index].new(channel, value, options)
|
89
|
+
else
|
90
|
+
ControlChange.new(channel, index, value, options)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
alias_method :ControlChange, :control_change
|
94
|
+
alias_method :Controller, :control_change
|
95
|
+
alias_method :controller, :control_change
|
96
|
+
|
97
|
+
# A poly pressure message
|
98
|
+
# @param [Fixnum, String] note
|
99
|
+
# @param [Fixnum] value
|
100
|
+
# @param [Hash] options
|
101
|
+
# @option options [Fixnum] :channel
|
102
|
+
def polyphonic_aftertouch(note, value, options = {})
|
103
|
+
channel = options[:channel] || @channel
|
104
|
+
raise 'channel_aftertouch requires a channel' if channel.nil?
|
105
|
+
|
106
|
+
if note.is_a?(String)
|
107
|
+
PolyphonicAftertouch[note].new(channel, value, options)
|
108
|
+
else
|
109
|
+
PolyphonicAftertouch.new(channel, note, value, options)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
alias_method :PolyphonicAftertouch, :polyphonic_aftertouch
|
114
|
+
alias_method :PolyAftertouch, :polyphonic_aftertouch
|
115
|
+
alias_method :PolyphonicPressure, :polyphonic_aftertouch
|
116
|
+
alias_method :PolyPressure, :polyphonic_aftertouch
|
117
|
+
alias_method :poly_aftertouch, :polyphonic_aftertouch
|
118
|
+
alias_method :poly_pressure, :polyphonic_aftertouch
|
119
|
+
|
120
|
+
# A channel pressure message
|
121
|
+
# @param [Fixnum] value
|
122
|
+
# @param [Hash] options
|
123
|
+
# @option options [Fixnum] :channel
|
124
|
+
def channel_aftertouch(value, options = {})
|
125
|
+
channel = options[:channel] || @channel
|
126
|
+
raise 'channel_aftertouch requires a channel' if channel.nil?
|
127
|
+
|
128
|
+
ChannelAftertouch.new(channel, value, options)
|
129
|
+
end
|
130
|
+
alias_method :ChannelAftertouch, :channel_aftertouch
|
131
|
+
alias_method :ChannelPressure, :channel_aftertouch
|
132
|
+
alias_method :channel_pressure, :channel_aftertouch
|
133
|
+
|
134
|
+
# A poly pressure message
|
135
|
+
# @param [Fixnum] low
|
136
|
+
# @param [Fixnum] high
|
137
|
+
# @param [Hash] options
|
138
|
+
# @option options [Fixnum] :channel
|
139
|
+
def pitch_bend(low, high, options = {})
|
140
|
+
channel = options[:channel] || @channel
|
141
|
+
raise 'channel_aftertouch requires a channel' if channel.nil?
|
142
|
+
|
143
|
+
PitchBend.new(channel, low, high, options)
|
144
|
+
end
|
145
|
+
alias_method :PitchBend, :pitch_bend
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
# Shortcut to MIDIMessage::Context.with
|
150
|
+
# @param [Hash] options
|
151
|
+
# @param [Proc] block
|
152
|
+
# @option options [Fixnum] :channel
|
153
|
+
# @option options [Fixnum] :velocity
|
154
|
+
def self.with_context(options = {}, &block)
|
155
|
+
Context.with(options, &block)
|
156
|
+
end
|
157
|
+
class << self
|
158
|
+
alias_method :with, :with_context
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module MIDIEvents
|
2
|
+
# Common behavior amongst all Message types
|
3
|
+
module Message
|
4
|
+
# Initialize the message status
|
5
|
+
# @param [Fixnum] status_nibble_1 The first nibble of the status
|
6
|
+
# @param [Fixnum] status_nibble_2 The second nibble of the status
|
7
|
+
def initialize_message(status_nibble_1, status_nibble_2)
|
8
|
+
@status = [status_nibble_1, status_nibble_2]
|
9
|
+
populate_using_const
|
10
|
+
end
|
11
|
+
|
12
|
+
# Byte array representation of the message eg [0x90, 0x40, 0x40] for NoteOn(0x40, 0x40)
|
13
|
+
# @return [Array<Fixnum>] The array of bytes in the MIDI message
|
14
|
+
def to_a
|
15
|
+
data = [@data[0], @data[1]] unless @data.nil?
|
16
|
+
data ||= []
|
17
|
+
[status_as_byte, *data].compact
|
18
|
+
end
|
19
|
+
alias_method :to_byte_a, :to_a
|
20
|
+
alias_method :to_byte_array, :to_a
|
21
|
+
alias_method :to_bytes, :to_a
|
22
|
+
|
23
|
+
# String representation of the message's bytes eg "904040" for NoteOn(0x40, 0x40)
|
24
|
+
# @return [String] The bytes of the message as a string of hex bytes
|
25
|
+
def to_hex_s
|
26
|
+
TypeConversion.numeric_byte_array_to_hex_string(to_a)
|
27
|
+
end
|
28
|
+
alias_method :to_bytestr, :to_hex_s
|
29
|
+
|
30
|
+
def update
|
31
|
+
populate_using_const
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(other_message)
|
35
|
+
self.class == other_message.class &&
|
36
|
+
to_a == other_message.to_a
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Convert the status nibbles to a single byte
|
42
|
+
# Eg [0x9, 0xF] -> 0x9F
|
43
|
+
# @return [Fixnum]
|
44
|
+
def status_as_byte
|
45
|
+
(@status[0] << 4) + @status[1]
|
46
|
+
end
|
47
|
+
|
48
|
+
def populate_using_const
|
49
|
+
unless (info = Constant::Loader.get_info(self)).nil?
|
50
|
+
@const = info[:const]
|
51
|
+
@name = info[:name]
|
52
|
+
@verbose_name = info[:verbose_name]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.included(base)
|
57
|
+
base.send(:extend, Constant::Loader::DSL)
|
58
|
+
base.send(:include, MIDIEvents) # this enables ..kind_of?(MIDIMessage)
|
59
|
+
base.send(:attr_reader, :name, :status, :verbose_name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
module MIDIEvents
|
2
|
+
#
|
3
|
+
# MIDI Channel Aftertouch message
|
4
|
+
#
|
5
|
+
class ChannelAftertouch
|
6
|
+
include ChannelMessage
|
7
|
+
|
8
|
+
STATUS = 0xD
|
9
|
+
DATA = [:channel, :value].freeze
|
10
|
+
DISPLAY_NAME = 'Channel Aftertouch'.freeze
|
11
|
+
|
12
|
+
ChannelMessage::Accessors.decorate(self)
|
13
|
+
end
|
14
|
+
ChannelPressure = ChannelAftertouch
|
15
|
+
|
16
|
+
#
|
17
|
+
# MIDI Control Change message
|
18
|
+
#
|
19
|
+
class ControlChange
|
20
|
+
include ChannelMessage
|
21
|
+
|
22
|
+
STATUS = 0xB
|
23
|
+
DATA = [:channel, :index, :value].freeze
|
24
|
+
DISPLAY_NAME = 'Control Change'.freeze
|
25
|
+
CONSTANT = { 'Control Change' => :index }.freeze
|
26
|
+
|
27
|
+
ChannelMessage::Accessors.decorate(self)
|
28
|
+
end
|
29
|
+
Controller = ControlChange #shortcut
|
30
|
+
|
31
|
+
#
|
32
|
+
# MIDI Pitch Bend message
|
33
|
+
#
|
34
|
+
class PitchBend
|
35
|
+
include ChannelMessage
|
36
|
+
|
37
|
+
STATUS = 0xE
|
38
|
+
DATA = [:channel, :low, :high].freeze
|
39
|
+
DISPLAY_NAME = 'Pitch Bend'.freeze
|
40
|
+
|
41
|
+
ChannelMessage::Accessors.decorate(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# MIDI Polyphonic (note specific) Aftertouch message
|
46
|
+
#
|
47
|
+
class PolyphonicAftertouch
|
48
|
+
include ChannelMessage
|
49
|
+
|
50
|
+
STATUS = 0xA
|
51
|
+
DATA = [:channel, :note, :value].freeze
|
52
|
+
DISPLAY_NAME = 'Polyphonic Aftertouch'.freeze
|
53
|
+
CONSTANT = { 'Note' => :note }.freeze
|
54
|
+
|
55
|
+
ChannelMessage::Accessors.decorate(self)
|
56
|
+
end
|
57
|
+
PolyAftertouch = PolyphonicAftertouch
|
58
|
+
PolyPressure = PolyphonicAftertouch
|
59
|
+
PolyphonicPressure = PolyphonicAftertouch
|
60
|
+
|
61
|
+
#
|
62
|
+
# MIDI Program Change message
|
63
|
+
#
|
64
|
+
class ProgramChange
|
65
|
+
include ChannelMessage
|
66
|
+
|
67
|
+
STATUS = 0xC
|
68
|
+
DATA = [:channel, :program].freeze
|
69
|
+
DISPLAY_NAME = 'Program Change'.freeze
|
70
|
+
|
71
|
+
ChannelMessage::Accessors.decorate(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# MIDI Note-Off message
|
76
|
+
#
|
77
|
+
class NoteOff
|
78
|
+
include NoteMessage
|
79
|
+
|
80
|
+
STATUS = 0x8
|
81
|
+
DATA = [:channel, :note, :velocity].freeze
|
82
|
+
DISPLAY_NAME = 'Note Off'.freeze
|
83
|
+
CONSTANT = { 'Note' => :note }.freeze
|
84
|
+
|
85
|
+
ChannelMessage::Accessors.decorate(self)
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# MIDI Note-On message
|
90
|
+
#
|
91
|
+
class NoteOn
|
92
|
+
include NoteMessage
|
93
|
+
|
94
|
+
STATUS = 0x9
|
95
|
+
DATA = [:channel, :note, :velocity].freeze
|
96
|
+
DISPLAY_NAME = 'Note On'.freeze
|
97
|
+
CONSTANT = { 'Note' => :note }.freeze
|
98
|
+
|
99
|
+
ChannelMessage::Accessors.decorate(self)
|
100
|
+
|
101
|
+
# returns the NoteOff equivalent of this object
|
102
|
+
def to_note_off
|
103
|
+
NoteOff.new(channel, note, velocity)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# MIDI System-Common message
|
109
|
+
#
|
110
|
+
class SystemCommon
|
111
|
+
include SystemMessage
|
112
|
+
|
113
|
+
ID = (0x1..0x6).freeze
|
114
|
+
DISPLAY_NAME = 'System Common'.freeze
|
115
|
+
|
116
|
+
attr_reader :data
|
117
|
+
|
118
|
+
def initialize(*args)
|
119
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
120
|
+
@const = options[:const]
|
121
|
+
id = @const.nil? ? args.shift : @const.value
|
122
|
+
id = strip_redundant_nibble(id)
|
123
|
+
initialize_message(SystemMessage::STATUS, id)
|
124
|
+
@data = args.slice(0..1)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# MIDI System-Realtime message
|
130
|
+
#
|
131
|
+
class SystemRealtime
|
132
|
+
include SystemMessage
|
133
|
+
|
134
|
+
ID = (0x8..0xF).freeze
|
135
|
+
DISPLAY_NAME = 'System Realtime'.freeze
|
136
|
+
|
137
|
+
def initialize(*args)
|
138
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
139
|
+
@const = options[:const]
|
140
|
+
id = @const.nil? ? args.first : @const.value
|
141
|
+
id = strip_redundant_nibble(id)
|
142
|
+
initialize_message(SystemMessage::STATUS, id)
|
143
|
+
end
|
144
|
+
|
145
|
+
def id
|
146
|
+
@status[1]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
module SystemExclusive
|
151
|
+
ID = 0x0
|
152
|
+
DELIMITER = {
|
153
|
+
start: 0xF0,
|
154
|
+
finish: 0xF7
|
155
|
+
}.freeze
|
156
|
+
DISPLAY_NAME = 'System Exclusive'.freeze
|
157
|
+
|
158
|
+
# A SysEx command message
|
159
|
+
# A command message is identified by having a status byte equal to 0x12
|
160
|
+
class Command
|
161
|
+
include SystemExclusive
|
162
|
+
|
163
|
+
attr_accessor :data
|
164
|
+
alias_method :value, :data
|
165
|
+
|
166
|
+
TYPE = 0x12
|
167
|
+
|
168
|
+
def initialize(address, data, options = {})
|
169
|
+
# store as a byte if it's a single byte
|
170
|
+
@data = if data.is_a?(Array) && data.length == 1
|
171
|
+
data.first
|
172
|
+
else
|
173
|
+
data
|
174
|
+
end
|
175
|
+
initialize_sysex(address, options)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# A SysEx request message
|
180
|
+
# A request message is identified by having a status byte equal to 0x11
|
181
|
+
class Request
|
182
|
+
include SystemExclusive
|
183
|
+
|
184
|
+
attr_reader :size
|
185
|
+
alias_method :value, :size
|
186
|
+
|
187
|
+
TYPE = 0x11
|
188
|
+
|
189
|
+
def initialize(address, size, options = {})
|
190
|
+
self.size = if size.is_a?(Array) && size.count == 1
|
191
|
+
size.first
|
192
|
+
else
|
193
|
+
size
|
194
|
+
end
|
195
|
+
initialize_sysex(address, options)
|
196
|
+
end
|
197
|
+
|
198
|
+
def size=(value)
|
199
|
+
# accepts a Numeric or Array but
|
200
|
+
# must always store value as an array of three bytes
|
201
|
+
size = []
|
202
|
+
if value.is_a?(Array) && value.size <= 3
|
203
|
+
size = value
|
204
|
+
elsif value.is_a?(Numeric) && (value + 1) / 247 <= 2
|
205
|
+
size = []
|
206
|
+
div, mod = *value.divmod(247)
|
207
|
+
size << mod unless mod.zero?
|
208
|
+
div.times { size << 247 }
|
209
|
+
end
|
210
|
+
(3 - size.size).times { size.unshift 0 }
|
211
|
+
@size = size
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|