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,40 @@
1
+ module MIDIEvents
2
+
3
+ # Common Note Message Behavior
4
+ module NoteMessage
5
+
6
+ def self.included(base)
7
+ base.send(:include, ChannelMessage)
8
+ end
9
+
10
+ # The octave number of the note
11
+ # @return [Fixnum]
12
+ def octave
13
+ (note / 12) - 1
14
+ end
15
+ alias_method :oct, :octave
16
+
17
+ # Set the octave number of the note
18
+ # @param [Fixnum] value
19
+ # @return [NoteMessage] self
20
+ def octave=(value)
21
+ self.note = ((value + 1) * 12) + abs_note
22
+ self
23
+ end
24
+ alias_method :oct=, :octave=
25
+
26
+ # How many half-steps is this note above the closest C
27
+ # @return [Fixnum]
28
+ def abs_note
29
+ note - ((note / 12) * 12)
30
+ end
31
+
32
+ # The name of the note without its octave e.g. F#
33
+ # @return [String]
34
+ def note_name
35
+ name.split(/-?\d\z/).first unless name.nil?
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,244 @@
1
+ module MIDIEvents
2
+
3
+ # MIDI System-Exclusive Messages (SysEx)
4
+ module SystemExclusive
5
+ include MIDIEvents # this enables ..kind_of?(MIDIMessage)
6
+
7
+ def self.included(base)
8
+ base.send(:include, InstanceMethods)
9
+ end
10
+
11
+ # Common SysEx data that a message class will contain
12
+ module InstanceMethods
13
+ attr_accessor :node
14
+ attr_reader :address, :checksum
15
+
16
+ # an array of message parts. multiple byte parts will be represented as an array of bytes
17
+ def to_a(options = {})
18
+ omit = options[:omit] || []
19
+ node = @node.to_a(options) unless @node.nil? || omit.include?(:node)
20
+ # this may need to be cached when properties are updated
21
+ # might be worth benchmarking
22
+ [
23
+ start_byte,
24
+ node,
25
+ (type_byte unless omit.include?(:type)),
26
+ [address].compact.flatten,
27
+ [value].compact.flatten,
28
+ (checksum unless omit.include?(:checksum)),
29
+ end_byte
30
+ ].compact
31
+ end
32
+
33
+ # a flat array of message bytes
34
+ def to_numeric_byte_array(options = {})
35
+ to_a(options).flatten
36
+ end
37
+ alias_method :to_numeric_bytes, :to_numeric_byte_array
38
+ alias_method :to_byte_array, :to_numeric_byte_array
39
+ alias_method :to_bytes, :to_numeric_byte_array
40
+ alias_method :to_byte_a, :to_numeric_byte_array
41
+
42
+ # string representation of the object's bytes
43
+ def to_hex_s
44
+ strings = to_bytes.map do |byte|
45
+ string = byte.to_s(16)
46
+ string = "0#{string}" if string.length == 1
47
+ string
48
+ end
49
+ strings.join.upcase
50
+ end
51
+ alias_method :to_bytestr, :to_hex_s
52
+
53
+ def name
54
+ SystemExclusive::DISPLAY_NAME
55
+ end
56
+ alias_method :verbose_name, :name
57
+
58
+ def start_byte
59
+ SystemExclusive::DELIMITER[:start]
60
+ end
61
+
62
+ def end_byte
63
+ SystemExclusive::DELIMITER[:finish]
64
+ end
65
+
66
+ def type_byte
67
+ self.class::TYPE
68
+ end
69
+
70
+ # alternate method from
71
+ # http://www.2writers.com/eddie/TutSysEx.htm
72
+ def checksum
73
+ sum = (address + [value].flatten).inject(&:+)
74
+ mod = sum.divmod(128)[1]
75
+ 128 - mod
76
+ end
77
+
78
+ private
79
+
80
+ def initialize_sysex(address, options = {})
81
+ @node = options[:node]
82
+ @checksum = options[:checksum]
83
+ @address = address
84
+ end
85
+
86
+ end
87
+
88
+ # A SysEx message with no implied type
89
+ #
90
+ class Message
91
+ include InstanceMethods
92
+
93
+ attr_accessor :data
94
+
95
+ def initialize(data, options = {})
96
+ @data = if data.kind_of?(Array) && data.length == 1
97
+ data.first
98
+ else
99
+ data
100
+ end
101
+ initialize_sysex(nil, options)
102
+ end
103
+
104
+ # an array of message parts. multiple byte parts will be represented as an array of bytes
105
+ def to_a(options = {})
106
+ omit = options[:omit] || []
107
+ node = @node.to_a(options) unless @node.nil? || omit.include?(:node)
108
+ # this may need to be cached when properties are updated
109
+ # might be worth benchmarking
110
+ [
111
+ start_byte,
112
+ node,
113
+ @data,
114
+ end_byte
115
+ ].compact
116
+ end
117
+
118
+ def ==(other_message)
119
+ self.class == other_message.class &&
120
+ to_a == other_message.to_a
121
+ end
122
+
123
+ end
124
+
125
+ #
126
+ # The SystemExclusive::Node represents a destination for a message. For example a hardware
127
+ # synthesizer or sampler
128
+ #
129
+ class Node
130
+
131
+ attr_accessor :device_id
132
+ attr_reader :manufacturer_id, :model_id
133
+
134
+ def initialize(manufacturer, options = {})
135
+ @device_id = options[:device_id]
136
+ @model_id = options[:model_id]
137
+ @manufacturer_id = get_manufacturer_id(manufacturer)
138
+ end
139
+
140
+ def to_a(options = {})
141
+ omit = options[:omit] || []
142
+ properties = [:manufacturer, :device, :model].map do |property|
143
+ unless omit.include?(property) || omit.include?("#{property.to_s}_id")
144
+ instance_variable_get("@#{property.to_s}_id")
145
+ end
146
+ end
147
+ properties.compact
148
+ end
149
+
150
+ # this message takes a prototype message, copies it, and returns the copy with its node set
151
+ # to this node
152
+ def new_message_from(prototype_message)
153
+ copy = prototype_message.clone
154
+ copy.node = self
155
+ copy
156
+ end
157
+
158
+ # create a new Command message associated with this node
159
+ def command(*a)
160
+ command = Command.new(*a)
161
+ command.node = self
162
+ command
163
+ end
164
+
165
+ # create a new Request message associated with this node
166
+ def request(*a)
167
+ request = Request.new(*a)
168
+ request.node = self
169
+ request
170
+ end
171
+
172
+ private
173
+
174
+ def get_manufacturer_id(manufacturer)
175
+ if manufacturer.kind_of?(Numeric)
176
+ manufacturer
177
+ else
178
+ const = Constant.find("Manufacturer", manufacturer)
179
+ const.value
180
+ end
181
+ end
182
+
183
+ end
184
+
185
+ module Builder
186
+
187
+ extend self
188
+
189
+ # Convert raw MIDI data to a SysEx message object
190
+ def build(*bytes)
191
+ if is_sysex?(bytes)
192
+
193
+ # if the 4th byte isn't status, we will just make this a Message object
194
+ # -- this may need some tweaking
195
+ message_class = get_message_class(bytes)
196
+
197
+ if message_class.nil?
198
+ Message.new(bytes)
199
+ else
200
+ build_typed_message(message_class, bytes)
201
+ end
202
+ end
203
+
204
+ end
205
+
206
+ private
207
+
208
+ def get_message_class(bytes)
209
+ [Request, Command].find { |klass| klass::TYPE == bytes[3] }
210
+ end
211
+
212
+ def is_sysex?(bytes)
213
+ bytes.shift == SystemExclusive::DELIMITER[:start] &&
214
+ bytes.pop == SystemExclusive::DELIMITER[:finish]
215
+ end
216
+
217
+ # Build a SysEx message object of the given type using the given bytes
218
+ def build_typed_message(message_class, bytes)
219
+ bytes = bytes.dup
220
+ fixed_length_message_part = bytes.slice!(0,7)
221
+
222
+ manufacturer_id = fixed_length_message_part[0]
223
+ device_id = fixed_length_message_part[1]
224
+ model_id = fixed_length_message_part[2]
225
+
226
+ address = fixed_length_message_part.slice(4,3)
227
+ checksum = bytes.slice!((bytes.length - 1), 1)
228
+ value = bytes
229
+
230
+ node = Node.new(manufacturer_id, :model_id => model_id, :device_id => device_id)
231
+ message_class.new(address, value, :checksum => checksum, :node => node)
232
+ end
233
+
234
+ end
235
+
236
+ # Convert raw MIDI data to a SysEx message object
237
+ # Shortcut to Builder.build
238
+ def self.new(*bytes)
239
+ Builder.build(*bytes)
240
+ end
241
+
242
+ end
243
+
244
+ end
@@ -0,0 +1,19 @@
1
+ module MIDIEvents
2
+ # Common MIDI system message behavior
3
+ module SystemMessage
4
+ STATUS = 0xF
5
+
6
+ def self.included(base)
7
+ base.send(:include, Message)
8
+ end
9
+
10
+ # In the case of something like SystemCommon.new(0xF2, 0x00, 0x08), the first nibble F is redundant because
11
+ # all system messages start with 0xF and it can be assumed.
12
+ # However, the this method looks to see if this has occurred and strips the redundancy
13
+ # @param [Fixnum] byte The byte to strip of a redundant 0xF
14
+ # @return [Fixnum] The remaining nibble
15
+ def strip_redundant_nibble(byte)
16
+ byte > STATUS ? (byte & 0x0F) : byte
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,79 @@
1
+ module MIDIEvents
2
+
3
+ # Helper for converting nibbles and bytes
4
+ module TypeConversion
5
+
6
+ extend self
7
+
8
+ # Convert an array of hex nibbles to an array of numeric bytes
9
+ # eg ["9", "0", "4", "0"] to [0x90, 0x40]
10
+ #
11
+ # @param [Array<String>] An array of hex nibbles eg ["9", "0", "4", "0"]
12
+ # @return [Array<Fixnum] An array of numeric bytes eg [0x90, 0x40]
13
+ def hex_chars_to_numeric_byte_array(nibbles)
14
+ nibbles = nibbles.dup # Don't mess with the input
15
+ # get rid of last nibble if there's an odd number
16
+ # it will be processed later anyway
17
+ nibbles.slice!(nibbles.length-2, 1) if nibbles.length.odd?
18
+ bytes = []
19
+ while !(nibs = nibbles.slice!(0,2)).empty?
20
+ byte = (nibs[0].hex << 4) + nibs[1].hex
21
+ bytes << byte
22
+ end
23
+ bytes
24
+ end
25
+
26
+ # Convert byte string to an array of numeric bytes
27
+ # eg. "904040" to [0x90, 0x40, 0x40]
28
+ # @param [String] string A string representing hex digits eg "904040"
29
+ # @return [Array<Fixnum>] An array of numeric bytes eg [0x90, 0x40, 0x40]
30
+ def hex_string_to_numeric_byte_array(string)
31
+ string = string.dup
32
+ bytes = []
33
+ until string.length == 0
34
+ bytes << string.slice!(0, 2).hex
35
+ end
36
+ bytes
37
+ end
38
+
39
+ # Convert a string of hex digits to an array of nibbles
40
+ # eg. "904040" to ["9", "0", "4", "0", "4", "0"]
41
+ # @param [String] string A string representing hex digits eg "904040"
42
+ # @return [Array<String>] An array of hex nibble chars eg ["9", "0", "4", "0", "4", "0"]
43
+ def hex_str_to_hex_chars(string)
44
+ string.split(//)
45
+ end
46
+
47
+ # Convert an array of numeric bytes to a string of hex digits
48
+ # eg. [0x90, 0x40, 0x40] to "904040"
49
+ # @param [Array<Fixnum>] bytes An array of numeric bytes eg [0x90, 0x40, 0x40]
50
+ # @return [String] A string representing hex digits eg "904040"
51
+ def numeric_byte_array_to_hex_string(bytes)
52
+ string_bytes = bytes.map do |byte|
53
+ string = byte.to_s(16)
54
+ string = "0#{string}" if string.length == 1
55
+ string
56
+ end
57
+ string_bytes.join.upcase
58
+ end
59
+
60
+ # Convert a numeric byte to hex chars
61
+ # eg 0x90 to ["9", "0"]
62
+ # @param [Fixnum] num A numeric byte eg 0x90
63
+ # @return [Array<String>] An array of hex nibble chars eg ["9", "0"]
64
+ def numeric_byte_to_hex_chars(num)
65
+ nibbles = numeric_byte_to_nibbles(num)
66
+ nibbles.map { |n| n.to_s(16) }
67
+ end
68
+
69
+ # Convert a numeric byte to nibbles
70
+ # eg 0x90 to [0x9, 0x0]
71
+ # @param [Fixnum] num A numeric byte eg 0x90
72
+ # @return [Array<Fixnum>] An array of nibbles eg [0x9, 0x0]
73
+ def numeric_byte_to_nibbles(num)
74
+ [((num & 0xF0) >> 4), (num & 0x0F)]
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,27 @@
1
+ #
2
+ # Ruby MIDI message objects
3
+ #
4
+ # (c)2021 Javier Sánchez Yeste for the modifications, licensed under LGPL 3.0 License
5
+ # (c)2011-2015 Ari Russo for original MIDI Message library, licensed under Apache 2.0 License
6
+ #
7
+
8
+ # Libs
9
+ require 'forwardable'
10
+ require 'yaml'
11
+
12
+ # Modules
13
+ require 'midi-events/constant'
14
+ require 'midi-events/message'
15
+ require 'midi-events/channel_message'
16
+ require 'midi-events/note_message'
17
+ require 'midi-events/system_exclusive'
18
+ require 'midi-events/system_message'
19
+ require 'midi-events/type_conversion'
20
+
21
+ # Classes
22
+ require 'midi-events/context'
23
+ require 'midi-events/messages'
24
+
25
+ module MIDIEvents
26
+ VERSION = '0.5.0'.freeze
27
+ end