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,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
|
data/lib/midi-events.rb
ADDED
@@ -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
|