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.
@@ -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