midi-message 0.4.4 → 0.4.5
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 +4 -4
- data/LICENSE +1 -1
- data/README.md +16 -16
- data/lib/midi-message.rb +5 -5
- data/lib/midi-message/channel_message.rb +55 -33
- data/lib/midi-message/constant.rb +201 -45
- data/lib/midi-message/message.rb +36 -195
- data/lib/midi-message/messages.rb +211 -0
- data/lib/midi-message/system_message.rb +2 -2
- data/test/constants_test.rb +25 -30
- data/test/context_test.rb +2 -6
- data/test/helper.rb +3 -8
- data/test/{short_message_test.rb → message_test.rb} +44 -49
- data/test/mutability_test.rb +8 -13
- data/test/parser_test.rb +39 -44
- data/test/system_exclusive_test.rb +54 -59
- data/test/system_message_test.rb +21 -27
- metadata +85 -5
- data/lib/midi-message/short_message.rb +0 -137
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33e3e855ef83585302fda5a2b7f332254b089c9f
|
4
|
+
data.tar.gz: d130122274fa395bd8939da501ac735c75f9b1ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 78bd93128dae08a2f2367028f9e6fd38442919777d17b7445b8a8a16e8310f39f291a904a5ebd394bac90245abdb681c28d580e20badf7ef44274219d5959a55
|
7
|
+
data.tar.gz: b96b8747c8582d448a991f37f9fe34f7b9f66af4644892bd449267e083eefb9ceb8a1b302d7d74ff4af1870bc64b868614736dd8691e887399fd918bfef8b21a
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,7 @@ Ruby MIDI message objects
|
|
9
9
|
* Flexible API to accommodate various sources and destinations of MIDI data
|
10
10
|
* Simple approach to System Exclusive data and devices
|
11
11
|
* [YAML dictionary of MIDI constants](https://github.com/arirusso/midi-message/blob/master/lib/midi.yml)
|
12
|
-
|
12
|
+
|
13
13
|
## Install
|
14
14
|
|
15
15
|
`gem install midi-message`
|
@@ -23,16 +23,16 @@ Or if you're using Bundler, add this to your Gemfile
|
|
23
23
|
```ruby
|
24
24
|
require "midi-message"
|
25
25
|
```
|
26
|
-
|
26
|
+
|
27
27
|
#### Basic Messages
|
28
28
|
|
29
29
|
There are a few ways to create a new MIDI message. Here are some examples
|
30
|
-
|
31
|
-
```ruby
|
30
|
+
|
31
|
+
```ruby
|
32
32
|
MIDIMessage::NoteOn.new(0, 64, 64)
|
33
|
-
|
33
|
+
|
34
34
|
MIDIMessage::NoteOn["E4"].new(0, 100)
|
35
|
-
|
35
|
+
|
36
36
|
MIDIMessage.with(:channel => 0, :velocity => 100) { note_on("E4") }
|
37
37
|
```
|
38
38
|
|
@@ -52,27 +52,27 @@ Those expressions all evaluate to the same object
|
|
52
52
|
#### SysEx Messages
|
53
53
|
|
54
54
|
As with any kind of message, you can begin with raw data
|
55
|
-
|
55
|
+
|
56
56
|
```ruby
|
57
57
|
MIDIMessage::SystemExclusive.new(0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7)
|
58
58
|
```
|
59
|
-
|
59
|
+
|
60
60
|
Or in a more object oriented way
|
61
61
|
|
62
62
|
```ruby
|
63
63
|
synth = SystemExclusive::Node.new(0x41, :model_id => 0x42, :device_id => 0x10)
|
64
|
-
|
64
|
+
|
65
65
|
SystemExclusive::Command.new([0x40, 0x7F, 0x00], 0x00, :node => synth)
|
66
66
|
```
|
67
67
|
|
68
68
|
A Node represents a device that you're sending a message to (eg. your Yamaha DX7 is a Node). Sysex messages can either be a Command or Request
|
69
|
-
|
69
|
+
|
70
70
|
You can use the Node to instantiate a message
|
71
71
|
|
72
72
|
```ruby
|
73
73
|
synth.command([0x40, 0x7F, 0x00], 0x00)
|
74
74
|
```
|
75
|
-
|
75
|
+
|
76
76
|
One way or another, you will wind up with a pair of objects like this
|
77
77
|
|
78
78
|
```ruby
|
@@ -86,18 +86,18 @@ One way or another, you will wind up with a pair of objects like this
|
|
86
86
|
@manufacturer_id=65,
|
87
87
|
@model_id=66>>
|
88
88
|
```
|
89
|
-
|
89
|
+
|
90
90
|
#### Parsing
|
91
91
|
|
92
92
|
The parse method will take any valid message data and return the object representation
|
93
93
|
|
94
94
|
```ruby
|
95
95
|
MIDIMessage.parse(0x90, 0x40, 0x40)
|
96
|
-
|
96
|
+
|
97
97
|
#<MIDIMessage::NoteOn:0x9c1c240 ..>
|
98
|
-
|
98
|
+
|
99
99
|
MIDIMessage.parse(0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7)
|
100
|
-
|
100
|
+
|
101
101
|
#<MIDIMessage::SystemExclusive::Command:0x9c1e57c ..>
|
102
102
|
```
|
103
103
|
|
@@ -115,4 +115,4 @@ Check out [nibbler](http://github.com/arirusso/nibbler) for more advanced parsin
|
|
115
115
|
|
116
116
|
Apache 2.0, See the file LICENSE
|
117
117
|
|
118
|
-
Copyright (c) 2011-
|
118
|
+
Copyright (c) 2011-2015 Ari Russo
|
data/lib/midi-message.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#
|
2
2
|
# Ruby MIDI message objects
|
3
3
|
#
|
4
|
-
# (c)2011-
|
4
|
+
# (c)2011-2015 Ari Russo
|
5
5
|
# Apache 2.0 License
|
6
6
|
#
|
7
7
|
|
@@ -10,7 +10,8 @@ require "forwardable"
|
|
10
10
|
require "yaml"
|
11
11
|
|
12
12
|
# Modules
|
13
|
-
require "midi-message/
|
13
|
+
require "midi-message/constant"
|
14
|
+
require "midi-message/message"
|
14
15
|
require "midi-message/channel_message"
|
15
16
|
require "midi-message/note_message"
|
16
17
|
require "midi-message/system_exclusive"
|
@@ -18,13 +19,12 @@ require "midi-message/system_message"
|
|
18
19
|
require "midi-message/type_conversion"
|
19
20
|
|
20
21
|
# Classes
|
21
|
-
require "midi-message/constant"
|
22
22
|
require "midi-message/context"
|
23
|
-
require "midi-message/
|
23
|
+
require "midi-message/messages"
|
24
24
|
require "midi-message/parser"
|
25
25
|
|
26
26
|
module MIDIMessage
|
27
27
|
|
28
|
-
VERSION = "0.4.
|
28
|
+
VERSION = "0.4.5"
|
29
29
|
|
30
30
|
end
|
@@ -4,6 +4,7 @@ module MIDIMessage
|
|
4
4
|
module ChannelMessage
|
5
5
|
|
6
6
|
include MIDIMessage # this enables ..kind_of?(MIDIMessage)
|
7
|
+
|
7
8
|
attr_reader :data, :name
|
8
9
|
|
9
10
|
# Shortcut to RawChannelMessage.new
|
@@ -19,44 +20,71 @@ module MIDIMessage
|
|
19
20
|
def initialize(*data)
|
20
21
|
data = data.dup
|
21
22
|
options = data.last.kind_of?(Hash) ? data.pop : {}
|
22
|
-
|
23
|
-
initialize_channel_message(self.class.type_for_status, *
|
23
|
+
add_constant_value(options[:const], data) unless options[:const].nil?
|
24
|
+
initialize_channel_message(self.class.type_for_status, *data)
|
24
25
|
end
|
25
26
|
|
27
|
+
# Decorates the object with the particular properties for its type
|
28
|
+
# @return [Boolean]
|
26
29
|
def initialize_properties
|
27
|
-
props = [
|
28
|
-
{ :name => :status, :index => 1 },
|
29
|
-
{ :name => :data, :index => 0 },
|
30
|
-
{ :name => :data, :index => 1 }
|
31
|
-
]
|
32
30
|
properties = self.class.properties
|
33
|
-
unless properties.nil?
|
34
|
-
properties.each_with_index do |prop,i|
|
35
|
-
self.class.send(:attr_reader, prop)
|
36
|
-
self.class.send(:define_method, "#{prop}=") do |val|
|
37
|
-
send(:instance_variable_set, "@#{prop.to_s}", val)
|
38
|
-
send(props[i][:name])[props[i][:index]] = val
|
39
|
-
update
|
40
|
-
return self
|
41
|
-
end
|
42
|
-
instance_variable_set("@#{prop}", send(props[i][:name])[props[i][:index]])
|
43
|
-
end
|
44
|
-
end
|
31
|
+
add_properties(properties) unless properties.nil?
|
45
32
|
end
|
46
33
|
|
47
|
-
|
34
|
+
private
|
48
35
|
|
49
36
|
def self.included(base)
|
50
|
-
base.send(:include,
|
37
|
+
base.send(:include, ::MIDIMessage::Message)
|
51
38
|
base.send(:extend, ClassMethods)
|
52
39
|
end
|
53
40
|
|
54
|
-
|
41
|
+
def add_constant_value(constant, data)
|
42
|
+
index = Constant::Loader.get_index(self)
|
43
|
+
data.insert(index, constant.value)
|
44
|
+
end
|
55
45
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
46
|
+
# @param [Array<Symbol>] properties
|
47
|
+
# @return [Boolean]
|
48
|
+
def add_properties(properties)
|
49
|
+
has_properties = false
|
50
|
+
schema = [
|
51
|
+
{ :name => :status, :index => 1 }, # second status nibble
|
52
|
+
{ :name => :data, :index => 0 }, # first data byte
|
53
|
+
{ :name => :data, :index => 1 } # second data byte
|
54
|
+
]
|
55
|
+
properties.each_with_index do |property, i|
|
56
|
+
property_schema = schema[i]
|
57
|
+
container = send(property_schema[:name])
|
58
|
+
index = property_schema[:index]
|
59
|
+
define_getter(property, container, index)
|
60
|
+
define_setter(property, container, index)
|
61
|
+
has_properties = true
|
62
|
+
end
|
63
|
+
has_properties
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param [Symbol, String] property
|
67
|
+
# @param [Hash] container
|
68
|
+
# @param [Fixnum] index
|
69
|
+
# @return [Boolean]
|
70
|
+
def define_getter(property, container, index)
|
71
|
+
self.class.send(:attr_reader, property)
|
72
|
+
instance_variable_set("@#{property.to_s}", container[index])
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param [Symbol, String] property
|
77
|
+
# @param [Hash] container
|
78
|
+
# @param [Fixnum] index
|
79
|
+
# @return [Boolean]
|
80
|
+
def define_setter(property, container, index)
|
81
|
+
self.class.send(:define_method, "#{property.to_s}=") do |value|
|
82
|
+
send(:instance_variable_set, "@#{property.to_s}", value)
|
83
|
+
container[index] = value
|
84
|
+
update
|
85
|
+
return self
|
86
|
+
end
|
87
|
+
true
|
60
88
|
end
|
61
89
|
|
62
90
|
def initialize_channel_message(status_nibble_1, status_nibble_2, data_byte_1, data_byte_2 = 0)
|
@@ -64,18 +92,12 @@ module MIDIMessage
|
|
64
92
|
@data = [data_byte_1]
|
65
93
|
@data[1] = data_byte_2 if self.class.second_data_byte?
|
66
94
|
initialize_properties
|
67
|
-
|
95
|
+
initialize_message(status_nibble_1, status_nibble_2)
|
68
96
|
end
|
69
97
|
|
70
98
|
# For defining Channel Message class types
|
71
99
|
module ClassMethods
|
72
100
|
|
73
|
-
# Get the status nibble for this particular message type
|
74
|
-
# @return [Fixnum] The status nibble
|
75
|
-
def type_for_status
|
76
|
-
Status[display_name]
|
77
|
-
end
|
78
|
-
|
79
101
|
def properties
|
80
102
|
const_get("DATA") if const_defined?("DATA")
|
81
103
|
end
|
@@ -1,58 +1,214 @@
|
|
1
1
|
module MIDIMessage
|
2
2
|
|
3
|
-
# MIDI
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@constants = constants.map { |k, v| Constant.new(k, v) }
|
11
|
-
end
|
12
|
-
|
13
|
-
def find(name)
|
14
|
-
@constants.find { |const| const.key.to_s.downcase == name.to_s.downcase }
|
15
|
-
end
|
16
|
-
alias_method :[], :find
|
17
|
-
|
18
|
-
def find_by_value(value)
|
19
|
-
@constants.find { |const| const.value.to_s.downcase == value.to_s.downcase }
|
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
|
+
def self.find(group_name, const_name)
|
8
|
+
group = Group[group_name]
|
9
|
+
group.find(const_name)
|
20
10
|
end
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
11
|
+
|
12
|
+
# MIDI Constant container
|
13
|
+
class Group
|
14
|
+
|
15
|
+
attr_reader :key, :value
|
16
|
+
|
17
|
+
def initialize(key, constants)
|
18
|
+
@key = key
|
19
|
+
@constants = constants.map { |k, v| Constant::Map.new(k, v) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Find a constant by its name
|
23
|
+
# @param [String, Symbol] name
|
24
|
+
# @return [Constant::Map]
|
25
|
+
def find(name)
|
26
|
+
@constants.find { |const| const.key.to_s.downcase == name.to_s.downcase }
|
27
|
+
end
|
28
|
+
alias_method :[], :find
|
29
|
+
|
30
|
+
# Find a constant by its value
|
31
|
+
# @param [Object] value
|
32
|
+
# @return [Constant::Map]
|
33
|
+
def find_by_value(value)
|
34
|
+
@constants.find { |const| const.value.to_s.downcase == value.to_s.downcase }
|
35
|
+
end
|
36
|
+
|
37
|
+
# All constant groups
|
38
|
+
# @return [Array<ConstantGroup>]
|
39
|
+
def self.all
|
40
|
+
ensure_initialized
|
41
|
+
@groups
|
42
|
+
end
|
43
|
+
|
44
|
+
# Find a constant group by its key
|
45
|
+
# @param [String, Symbol] key
|
46
|
+
# @return [ConstantGroup]
|
47
|
+
def self.[](key)
|
48
|
+
ensure_initialized
|
49
|
+
@groups.find { |g| g.key.to_s.downcase == key.to_s.downcase }
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Lazy initialize
|
55
|
+
# @return [Boolean]
|
56
|
+
def self.ensure_initialized
|
57
|
+
populate_dictionary | populate_groups
|
58
|
+
end
|
59
|
+
|
60
|
+
# Populate the dictionary of constants
|
61
|
+
# @return [Boolean]
|
62
|
+
def self.populate_dictionary
|
63
|
+
if @dict.nil?
|
64
|
+
@dict = YAML.load_file(File.expand_path('../../midi.yml', __FILE__))
|
65
|
+
true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Populate the constant groups using the dictionary
|
70
|
+
# @return [Boolean]
|
71
|
+
def self.populate_groups
|
72
|
+
if @groups.nil? && !@dict.nil?
|
73
|
+
@groups = @dict.map { |k, v| new(k, v) }
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
25
78
|
end
|
26
79
|
|
27
|
-
|
28
|
-
|
29
|
-
|
80
|
+
# The mapping of a constant key to its value eg "Note On" => 0x9
|
81
|
+
class Map
|
82
|
+
|
83
|
+
attr_reader :key, :value
|
84
|
+
|
85
|
+
# @param [String] key
|
86
|
+
# @param [Object] value
|
87
|
+
def initialize(key, value)
|
88
|
+
@key = key
|
89
|
+
@value = value
|
90
|
+
end
|
91
|
+
|
30
92
|
end
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
93
|
+
|
94
|
+
class MessageBuilder
|
95
|
+
|
96
|
+
# @param [MIDIMessage] klass The message class to build
|
97
|
+
# @param [String] const The constant to build the message with
|
98
|
+
def initialize(klass, const)
|
99
|
+
@klass = klass
|
100
|
+
@const = const
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param [*Object] args
|
104
|
+
# @return [Message]
|
105
|
+
def new(*args)
|
106
|
+
args = args.dup
|
107
|
+
args.last.kind_of?(Hash) ? args.last[:const] = @const : args.push(:const => @const)
|
108
|
+
@klass.new(*args)
|
109
|
+
end
|
110
|
+
|
38
111
|
end
|
39
112
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
113
|
+
# Shortcuts for dealing with message status
|
114
|
+
module Status
|
115
|
+
|
116
|
+
# The value of the Status constant with the name status_name
|
117
|
+
# @param [String] status_name The key to use to look up a constant value
|
118
|
+
# @return [String] The constant value that was looked up
|
119
|
+
def self.[](status_name)
|
120
|
+
const = Constant.find("Status", status_name)
|
121
|
+
const.value unless const.nil?
|
122
|
+
end
|
123
|
+
|
49
124
|
end
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
125
|
+
|
126
|
+
# Loading constants from the spec file into messages
|
127
|
+
module Loader
|
128
|
+
|
129
|
+
extend self
|
130
|
+
|
131
|
+
# Get the index of the constant from the given message's type
|
132
|
+
# @param [Message] message
|
133
|
+
# @return [Fixnum]
|
134
|
+
def get_index(message)
|
135
|
+
key = message.class.constant_property
|
136
|
+
message.class.properties.index(key) || 0
|
137
|
+
end
|
138
|
+
|
139
|
+
# Used to populate message metadata with information gathered from midi.yml
|
140
|
+
# @param [Message] message
|
141
|
+
# @return [Hash, nil]
|
142
|
+
def get_info(message)
|
143
|
+
const_group_name = message.class.display_name
|
144
|
+
group_name_alias = message.class.constant_name
|
145
|
+
property = message.class.constant_property
|
146
|
+
value = message.send(property) unless property.nil?
|
147
|
+
value ||= message.status[1] # default property to use for constants
|
148
|
+
group = Constant::Group[group_name_alias] || Constant::Group[const_group_name]
|
149
|
+
unless group.nil?
|
150
|
+
unless (const = group.find_by_value(value)).nil?
|
151
|
+
{
|
152
|
+
:const => const,
|
153
|
+
:name => const.key,
|
154
|
+
:verbose_name => "#{message.class.display_name}: #{@name}"
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# DSL type class methods for loading constants into messages
|
161
|
+
module DSL
|
162
|
+
|
163
|
+
# Find a constant value in this class's group for the passed in key
|
164
|
+
# @param [String] name The constant key
|
165
|
+
# @return [String] The constant value
|
166
|
+
def get_constant(name)
|
167
|
+
key = constant_name || display_name
|
168
|
+
unless key.nil?
|
169
|
+
group = Group[key]
|
170
|
+
group.find(name)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# @return [String]
|
175
|
+
def display_name
|
176
|
+
const_get("DISPLAY_NAME") if const_defined?("DISPLAY_NAME")
|
177
|
+
end
|
178
|
+
|
179
|
+
# @return [Hash]
|
180
|
+
def constant_map
|
181
|
+
const_get("CONSTANT") if const_defined?("CONSTANT")
|
182
|
+
end
|
183
|
+
|
184
|
+
# @return [String]
|
185
|
+
def constant_name
|
186
|
+
constant_map.keys.first unless constant_map.nil?
|
187
|
+
end
|
188
|
+
|
189
|
+
# @return [Symbol]
|
190
|
+
def constant_property
|
191
|
+
constant_map[constant_name] unless constant_map.nil?
|
192
|
+
end
|
193
|
+
|
194
|
+
# Get the status nibble for this particular message type
|
195
|
+
# @return [Fixnum] The status nibble
|
196
|
+
def type_for_status
|
197
|
+
Constant::Status[display_name]
|
198
|
+
end
|
199
|
+
|
200
|
+
# This returns a MessageBuilder for the class, preloaded with the selected const
|
201
|
+
# @param [String, Symbol] const_name The constant key to use to build the message
|
202
|
+
# @return [MIDIMessage::MessageBuilder] A MessageBuilder object for the passed in constant
|
203
|
+
def [](const_name)
|
204
|
+
const = get_constant(const_name.to_s)
|
205
|
+
MessageBuilder.new(self, const) unless const.nil?
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
54
210
|
end
|
55
|
-
|
211
|
+
|
56
212
|
end
|
57
213
|
|
58
214
|
end
|