midi-events 0.6.0 → 0.7.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 +4 -4
- data/.gitignore +2 -2
- data/.version +6 -0
- data/.yardopts +6 -0
- data/LICENSE +159 -668
- data/README.md +49 -14
- data/examples/short_messages.rb +0 -7
- data/lib/midi-events/constant.rb +168 -41
- data/lib/midi-events/context.rb +36 -7
- data/lib/midi-events/type_conversion.rb +66 -27
- data/lib/midi-events/version.rb +47 -0
- data/lib/midi-events.rb +1 -10
- data/midi-events.gemspec +11 -8
- metadata +50 -2
data/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# MIDI Events
|
|
2
2
|
|
|
3
|
+
[](https://www.ruby-lang.org/)
|
|
4
|
+
[](https://www.gnu.org/licenses/lgpl-3.0.html)
|
|
5
|
+
|
|
3
6
|
**Ruby MIDI Events Objects**
|
|
4
7
|
|
|
5
8
|
This library is part of a suite of Ruby libraries for MIDI:
|
|
@@ -61,6 +64,49 @@ Those expressions all evaluate to the same object:
|
|
|
61
64
|
@verbose_name="Note On: E4">
|
|
62
65
|
```
|
|
63
66
|
|
|
67
|
+
#### Raw Channel Messages
|
|
68
|
+
|
|
69
|
+
You can also create raw channel messages directly from nibbles and bytes:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
MIDIEvents::ChannelMessage.new(0x9, 0x0, 0x40, 0x40)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Mutable Properties
|
|
76
|
+
|
|
77
|
+
Some message properties can be modified after creation:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
msg = MIDIEvents::NoteOn["E4"].new(0, 100)
|
|
81
|
+
msg.note += 5 # Transpose up 5 semitones
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### System Realtime Messages
|
|
85
|
+
|
|
86
|
+
System Realtime messages are used for synchronization:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
MIDIEvents::SystemRealtime["Start"].new
|
|
90
|
+
MIDIEvents::SystemRealtime["Stop"].new
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Building Melodies
|
|
94
|
+
|
|
95
|
+
You can construct sequences of notes programmatically:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
channel = 0
|
|
99
|
+
notes = [36, 40, 43] # C E G
|
|
100
|
+
octaves = 2
|
|
101
|
+
velocity = 100
|
|
102
|
+
|
|
103
|
+
melody = []
|
|
104
|
+
|
|
105
|
+
(0..((octaves-1)*12)).step(12) do |oct|
|
|
106
|
+
notes.each { |note| melody << MIDIEvents::NoteOn.new(channel, note + oct, velocity) }
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
64
110
|
#### SysEx Messages
|
|
65
111
|
|
|
66
112
|
As with any kind of message, you can begin with raw data:
|
|
@@ -101,16 +147,15 @@ One way or another, you will wind up with a pair of objects like this:
|
|
|
101
147
|
|
|
102
148
|
## Documentation
|
|
103
149
|
|
|
104
|
-
*
|
|
150
|
+
* [rdoc](http://rubydoc.info/github/javier-sy/midi-events)
|
|
105
151
|
|
|
106
152
|
## Differences between [MIDI Events](https://github.com/javier-sy/midi-events) library and [MIDI Message](https://github.com/arirusso/midi-message) library
|
|
107
153
|
|
|
108
154
|
[MIDI Events](https://github.com/javier-sy/midi-events) is mostly a clone of [MIDI Message](https://github.com/arirusso/midi-message) with some modifications:
|
|
155
|
+
|
|
109
156
|
* Renamed gem to midi-events instead of midi-message
|
|
110
157
|
* Renamed module to MIDIEvents instead of MIDIMessage
|
|
111
158
|
* Removed parsing features (in favour of the more complete parser [MIDI Parser](https://github.com/javier-sy/midi-parser))
|
|
112
|
-
* TODO: update tests to use rspec instead of rake
|
|
113
|
-
* TODO: migrate to (or confirm it's working ok on) Ruby 3.0 and Ruby 3.1
|
|
114
159
|
|
|
115
160
|
## Then, why does exist this library if it is mostly a clone of another library?
|
|
116
161
|
|
|
@@ -144,16 +189,6 @@ I've decided to publish my own renamed version of the modified dependencies beca
|
|
|
144
189
|
|
|
145
190
|
All in all I have decided to publish a suite of libraries optimized for MusaDSL use case that also can be used by other people in their projects.
|
|
146
191
|
|
|
147
|
-
| Function | Library | Based on Ari Russo's| Difference |
|
|
148
|
-
| --- | --- | --- | --- |
|
|
149
|
-
| MIDI Events representation | [MIDI Events](https://github.com/javier-sy/midi-events) | [MIDI Message](https://github.com/arirusso/midi-message) | removed parsing, small improvements |
|
|
150
|
-
| MIDI Data parsing | [MIDI Parser](https://github.com/javier-sy/midi-parser) | [Nibbler](https://github.com/arirusso/nibbler) | removed process history information, minor optimizations |
|
|
151
|
-
| MIDI communication with Instruments and Control Surfaces | [MIDI Communications](https://github.com/javier-sy/midi-communications) | [unimidi](https://github.com/arirusso/unimidi) | use of [MIDI Communications MacOS Layer](https://github.com/javier-sy/midi-communications-macos, removed process history information, removed buffering, removed command line script)
|
|
152
|
-
| Low level MIDI interface to MacOS | [MIDI Communications MacOS Layer](https://github.com/javier-sy/midi-communications-macos) | [ffi-coremidi](https://github.com/arirusso/ffi-coremidi) | removed buffering and process history information, locking behaviour when waiting midi events, improved midi devices name detection, minor optimizations |
|
|
153
|
-
| Low level MIDI interface to Linux | **TO DO** | | |
|
|
154
|
-
| Low level MIDI interface to JRuby | **TO DO** | | |
|
|
155
|
-
| Low level MIDI interface to Windows | **TO DO** | | |
|
|
156
|
-
|
|
157
192
|
## Author
|
|
158
193
|
|
|
159
194
|
* [Javier Sánchez Yeste](https://github.com/javier-sy)
|
|
@@ -164,7 +199,7 @@ Thanks to [Ari Russo](http://github.com/arirusso) for his ruby library [MIDI Mes
|
|
|
164
199
|
|
|
165
200
|
## License
|
|
166
201
|
|
|
167
|
-
[MIDI Events](https://github.com/javier-sy/midi-events) Copyright (c) 2021 [Javier Sánchez Yeste](https://yeste.studio), licensed under LGPL 3.0 License
|
|
202
|
+
[MIDI Events](https://github.com/javier-sy/midi-events) Copyright (c) 2021-2025 [Javier Sánchez Yeste](https://yeste.studio), licensed under LGPL 3.0 License
|
|
168
203
|
|
|
169
204
|
[MIDI Message](https://github.com/arirusso/midi-message) Copyright (c) 2011-2015 [Ari Russo](http://arirusso.com), licensed under Apache License 2.0 (see the file LICENSE.midi-message)
|
|
170
205
|
|
data/examples/short_messages.rb
CHANGED
|
@@ -9,19 +9,12 @@ require "midi-events"
|
|
|
9
9
|
|
|
10
10
|
# Here are examples of different ways to construct messages, going from low to high-level
|
|
11
11
|
|
|
12
|
-
pp MIDIEvents.parse(0x90, 0x40, 0x40)
|
|
13
|
-
|
|
14
12
|
channel_msg = MIDIEvents::ChannelMessage.new(0x9, 0x0, 0x40, 0x40)
|
|
15
13
|
|
|
16
14
|
pp channel_msg
|
|
17
15
|
|
|
18
|
-
# this will return a NoteOn object with the properties of channel_msg
|
|
19
|
-
pp channel_msg.to_type
|
|
20
|
-
|
|
21
16
|
pp MIDIEvents::ChannelMessage.new(MIDIEvents::Constant::Status["Note On"], 0x0, 0x40, 0x40)
|
|
22
17
|
|
|
23
|
-
pp MIDIEvents::ChannelMessage.new(MIDIEvents::Constant::Status["Note On"], 0x0, 0x40, 0x40).to_type
|
|
24
|
-
|
|
25
18
|
pp MIDIEvents::NoteOn.new(0, 64, 64) # or NoteOn.new(0x0, 0x64, 0x64)
|
|
26
19
|
|
|
27
20
|
# some message properties are mutable
|
data/lib/midi-events/constant.rb
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
module MIDIEvents
|
|
2
|
-
#
|
|
3
|
-
#
|
|
2
|
+
# MIDI constant lookups and mappings
|
|
3
|
+
#
|
|
4
|
+
# Provides a flexible system for referring to MIDI messages by their human-readable names
|
|
5
|
+
# instead of numeric values. For example, "C4" for MIDI note 60, or "Bank Select" for
|
|
6
|
+
# MIDI control change 0.
|
|
7
|
+
#
|
|
8
|
+
# Constants are loaded from a YAML dictionary (midi.yml) and organized into groups
|
|
9
|
+
# (Notes, Control Changes, Status bytes, etc.).
|
|
10
|
+
#
|
|
11
|
+
# @example Looking up note values
|
|
12
|
+
# MIDIEvents::Constant.find('Note', 'C4')
|
|
13
|
+
# # => #<MIDIEvents::Constant::Map @key="C4", @value=60>
|
|
14
|
+
#
|
|
15
|
+
# @example Getting constant value directly
|
|
16
|
+
# MIDIEvents::Constant.value('Note', 'E4')
|
|
17
|
+
# # => 64
|
|
18
|
+
#
|
|
19
|
+
# @example Using constants in message creation
|
|
20
|
+
# MIDIEvents::NoteOn["C4"].new(0, 100)
|
|
21
|
+
# # Creates a NoteOn message for C4 (MIDI note 60)
|
|
22
|
+
#
|
|
23
|
+
# @api public
|
|
4
24
|
module Constant
|
|
5
25
|
# Get a Mapping object for the specified constant
|
|
6
26
|
# @param [Symbol, String] group_name
|
|
@@ -20,46 +40,88 @@ module MIDIEvents
|
|
|
20
40
|
map.value
|
|
21
41
|
end
|
|
22
42
|
|
|
43
|
+
# Name manipulation utilities for constant lookups
|
|
44
|
+
#
|
|
45
|
+
# Provides methods to normalize and compare constant names in a case-insensitive
|
|
46
|
+
# manner, supporting both "Control Change" and "control_change" formats.
|
|
47
|
+
#
|
|
48
|
+
# @api private
|
|
23
49
|
module Name
|
|
24
50
|
extend self
|
|
25
51
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# @
|
|
52
|
+
# Convert a name to underscore format
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# MIDIEvents::Constant::Name.underscore("Control Change")
|
|
56
|
+
# # => "control_change"
|
|
57
|
+
#
|
|
58
|
+
# @param [Symbol, String] string The name to convert
|
|
59
|
+
# @return [String] The underscored version
|
|
29
60
|
def underscore(string)
|
|
30
61
|
string.to_s.downcase.gsub(/(\ )+/, '_')
|
|
31
62
|
end
|
|
32
63
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# @
|
|
64
|
+
# Check if two names match (case-insensitive, supports underscored or spaced)
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# MIDIEvents::Constant::Name.match?("Control Change", "control_change")
|
|
68
|
+
# # => true
|
|
69
|
+
#
|
|
70
|
+
# @param [Symbol, String] key First name to compare
|
|
71
|
+
# @param [Symbol, String] other Second name to compare
|
|
72
|
+
# @return [Boolean] True if names match
|
|
36
73
|
def match?(key, other)
|
|
37
74
|
match_key = key.to_s.downcase
|
|
38
75
|
[match_key, Name.underscore(match_key)].include?(other.to_s.downcase)
|
|
39
76
|
end
|
|
40
77
|
end
|
|
41
78
|
|
|
42
|
-
# MIDI
|
|
79
|
+
# Container for a group of related MIDI constants
|
|
80
|
+
#
|
|
81
|
+
# Groups organize constants by category (e.g., "Note", "Control Change", "Status").
|
|
82
|
+
# Each group contains multiple Map objects that pair constant names with their values.
|
|
83
|
+
#
|
|
84
|
+
# @example Accessing a constant group
|
|
85
|
+
# group = MIDIEvents::Constant::Group['Note']
|
|
86
|
+
# group.find('C4') # => Map object for C4
|
|
87
|
+
#
|
|
88
|
+
# @api public
|
|
43
89
|
class Group
|
|
44
|
-
|
|
90
|
+
# @return [Array<MIDIEvents::Constant::Map>] The constants in this group
|
|
91
|
+
attr_reader :constants
|
|
45
92
|
|
|
46
|
-
# @
|
|
47
|
-
|
|
93
|
+
# @return [String] The group's key/name
|
|
94
|
+
attr_reader :key
|
|
95
|
+
|
|
96
|
+
# Create a new constant group
|
|
97
|
+
#
|
|
98
|
+
# @param [String] key The group identifier
|
|
99
|
+
# @param [Hash] constants Hash of constant names to values
|
|
48
100
|
def initialize(key, constants)
|
|
49
101
|
@key = key
|
|
50
102
|
@constants = constants.map { |k, v| Constant::Map.new(k, v) }
|
|
51
103
|
end
|
|
52
104
|
|
|
53
|
-
# Find a constant by its name
|
|
54
|
-
#
|
|
55
|
-
# @
|
|
105
|
+
# Find a constant in this group by its name
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# group = MIDIEvents::Constant::Group['Note']
|
|
109
|
+
# group.find('E4') # => Map for E4 (value 64)
|
|
110
|
+
#
|
|
111
|
+
# @param [String, Symbol] name The constant name to find
|
|
112
|
+
# @return [MIDIEvents::Constant::Map, nil] The matching constant or nil
|
|
56
113
|
def find(name)
|
|
57
114
|
@constants.find { |const| Name.match?(const.key, name) }
|
|
58
115
|
end
|
|
59
116
|
|
|
60
|
-
# Find a constant by its value
|
|
61
|
-
#
|
|
62
|
-
# @
|
|
117
|
+
# Find a constant in this group by its value (reverse lookup)
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# group = MIDIEvents::Constant::Group['Note']
|
|
121
|
+
# group.find_by_value(64) # => Map for E4
|
|
122
|
+
#
|
|
123
|
+
# @param [Object] value The numeric value to find
|
|
124
|
+
# @return [MIDIEvents::Constant::Map, nil] The matching constant or nil
|
|
63
125
|
def find_by_value(value)
|
|
64
126
|
@constants.find { |const| Name.match?(const.value, value) }
|
|
65
127
|
end
|
|
@@ -113,28 +175,61 @@ module MIDIEvents
|
|
|
113
175
|
|
|
114
176
|
end
|
|
115
177
|
|
|
116
|
-
#
|
|
178
|
+
# A single constant mapping (name to value pair)
|
|
179
|
+
#
|
|
180
|
+
# Represents an individual MIDI constant, pairing a human-readable name
|
|
181
|
+
# with its numeric MIDI value.
|
|
182
|
+
#
|
|
183
|
+
# @example
|
|
184
|
+
# map = MIDIEvents::Constant::Map.new("C4", 60)
|
|
185
|
+
# map.key # => "C4"
|
|
186
|
+
# map.value # => 60
|
|
187
|
+
#
|
|
188
|
+
# @api public
|
|
117
189
|
class Map
|
|
118
|
-
|
|
190
|
+
# @return [String] The human-readable name of the constant
|
|
191
|
+
attr_reader :key
|
|
192
|
+
|
|
193
|
+
# @return [Object] The numeric MIDI value
|
|
194
|
+
attr_reader :value
|
|
119
195
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
196
|
+
# Create a new constant mapping
|
|
197
|
+
#
|
|
198
|
+
# @param [String] key The constant name (e.g., "C4", "Note On")
|
|
199
|
+
# @param [Object] value The constant value (e.g., 60, 0x9)
|
|
122
200
|
def initialize(key, value)
|
|
123
201
|
@key = key
|
|
124
202
|
@value = value
|
|
125
203
|
end
|
|
126
204
|
end
|
|
127
205
|
|
|
206
|
+
# Helper class for building messages with pre-bound constants
|
|
207
|
+
#
|
|
208
|
+
# This class is returned when you call a message class's bracket method
|
|
209
|
+
# with a constant name (e.g., NoteOn["C4"]). It stores the constant
|
|
210
|
+
# and message class so that when you call #new, it creates the message
|
|
211
|
+
# with the constant value already filled in.
|
|
212
|
+
#
|
|
213
|
+
# @example
|
|
214
|
+
# builder = MIDIEvents::NoteOn["C4"]
|
|
215
|
+
# note = builder.new(0, 100) # channel 0, velocity 100
|
|
216
|
+
# # The note value (60 for C4) is automatically filled in
|
|
217
|
+
#
|
|
218
|
+
# @api private
|
|
128
219
|
class MessageBuilder
|
|
129
|
-
#
|
|
130
|
-
#
|
|
220
|
+
# Create a new message builder
|
|
221
|
+
#
|
|
222
|
+
# @param [Class] klass The message class to build (e.g., NoteOn)
|
|
223
|
+
# @param [MIDIEvents::Constant::Map] const The constant to build with
|
|
131
224
|
def initialize(klass, const)
|
|
132
225
|
@klass = klass
|
|
133
226
|
@const = const
|
|
134
227
|
end
|
|
135
228
|
|
|
136
|
-
#
|
|
137
|
-
#
|
|
229
|
+
# Create a message instance with the bound constant
|
|
230
|
+
#
|
|
231
|
+
# @param [Array] args The remaining arguments for the message constructor
|
|
232
|
+
# @return [MIDIEvents::Message] The constructed message
|
|
138
233
|
def new(*args)
|
|
139
234
|
args = args.dup
|
|
140
235
|
args.last.is_a?(Hash) ? args.last[:const] = @const : args.push(const: @const)
|
|
@@ -142,13 +237,23 @@ module MIDIEvents
|
|
|
142
237
|
end
|
|
143
238
|
end
|
|
144
239
|
|
|
145
|
-
# Shortcuts for dealing with
|
|
240
|
+
# Shortcuts for dealing with MIDI status bytes
|
|
241
|
+
#
|
|
242
|
+
# Provides quick lookup of status byte values by their human-readable names
|
|
243
|
+
# (e.g., "Note On" => 0x9).
|
|
244
|
+
#
|
|
245
|
+
# @example
|
|
246
|
+
# MIDIEvents::Constant::Status['Note On'] # => 0x9
|
|
247
|
+
# MIDIEvents::Constant::Status['Control Change'] # => 0xB
|
|
248
|
+
#
|
|
249
|
+
# @api public
|
|
146
250
|
module Status
|
|
147
251
|
extend self
|
|
148
252
|
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
# @
|
|
253
|
+
# Find a status byte value by its name
|
|
254
|
+
#
|
|
255
|
+
# @param [String, Symbol] status_name The name of the status (e.g., "Note On")
|
|
256
|
+
# @return [Integer, nil] The status nibble value or nil if not found
|
|
152
257
|
def find(status_name)
|
|
153
258
|
const = Constant.find('Status', status_name)
|
|
154
259
|
const&.value
|
|
@@ -156,21 +261,31 @@ module MIDIEvents
|
|
|
156
261
|
alias [] find
|
|
157
262
|
end
|
|
158
263
|
|
|
159
|
-
#
|
|
264
|
+
# Internal system for loading constants into message objects
|
|
265
|
+
#
|
|
266
|
+
# Handles the automatic population of message metadata (names, verbose names)
|
|
267
|
+
# based on constant definitions in midi.yml.
|
|
268
|
+
#
|
|
269
|
+
# @api private
|
|
160
270
|
module Loader
|
|
161
271
|
extend self
|
|
162
272
|
|
|
163
|
-
# Get the index
|
|
164
|
-
#
|
|
165
|
-
# @
|
|
273
|
+
# Get the property index for a constant in a message
|
|
274
|
+
#
|
|
275
|
+
# @param [MIDIEvents::Message] message The message to inspect
|
|
276
|
+
# @return [Integer] The index of the constant property
|
|
166
277
|
def get_index(message)
|
|
167
278
|
key = message.class.constant_property
|
|
168
279
|
message.class.properties.index(key) || 0
|
|
169
280
|
end
|
|
170
281
|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
282
|
+
# Populate message metadata using constant information from midi.yml
|
|
283
|
+
#
|
|
284
|
+
# Looks up the constant for a message and returns metadata including
|
|
285
|
+
# the constant object, name, and verbose name.
|
|
286
|
+
#
|
|
287
|
+
# @param [MIDIEvents::Message] message The message to populate
|
|
288
|
+
# @return [Hash, nil] Hash with :const, :name, :verbose_name keys, or nil
|
|
174
289
|
def get_info(message)
|
|
175
290
|
const_group_name = message.class.display_name
|
|
176
291
|
group_name_alias = message.class.constant_name
|
|
@@ -189,7 +304,12 @@ module MIDIEvents
|
|
|
189
304
|
end
|
|
190
305
|
end
|
|
191
306
|
|
|
192
|
-
# DSL
|
|
307
|
+
# DSL class methods for message classes to work with constants
|
|
308
|
+
#
|
|
309
|
+
# These methods are extended into message classes to provide constant lookup
|
|
310
|
+
# functionality (e.g., NoteOn["C4"]).
|
|
311
|
+
#
|
|
312
|
+
# @api private
|
|
193
313
|
module DSL
|
|
194
314
|
# Find a constant value in this class's group for the passed in key
|
|
195
315
|
# @param [String] name The constant key
|
|
@@ -228,9 +348,16 @@ module MIDIEvents
|
|
|
228
348
|
Constant::Status[display_name]
|
|
229
349
|
end
|
|
230
350
|
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
351
|
+
# Find a constant and return a MessageBuilder bound to it
|
|
352
|
+
#
|
|
353
|
+
# This enables the bracket syntax: NoteOn["C4"].new(channel, velocity)
|
|
354
|
+
#
|
|
355
|
+
# @example
|
|
356
|
+
# builder = MIDIEvents::NoteOn["C4"]
|
|
357
|
+
# note = builder.new(0, 100) # Creates NoteOn for C4 on channel 0
|
|
358
|
+
#
|
|
359
|
+
# @param [String, Symbol] const_name The constant name to look up
|
|
360
|
+
# @return [MIDIEvents::Constant::MessageBuilder, nil] Builder or nil if not found
|
|
234
361
|
def find(const_name)
|
|
235
362
|
const = get_constant(const_name.to_s)
|
|
236
363
|
MessageBuilder.new(self, const) unless const.nil?
|
data/lib/midi-events/context.rb
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
1
1
|
module MIDIEvents
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# DSL for creating MIDI messages with shared context
|
|
4
|
+
#
|
|
5
|
+
# Provides a convenient way to create multiple MIDI messages that share common
|
|
6
|
+
# parameters (like channel and velocity) without repeating them for each message.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic context usage
|
|
9
|
+
# MIDIEvents.with(channel: 0, velocity: 100) do
|
|
10
|
+
# note_on("C4") # Creates NoteOn with channel 0, velocity 100
|
|
11
|
+
# note_off("C4") # Creates NoteOff with channel 0, velocity 100
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Override context parameters
|
|
15
|
+
# MIDIEvents.with(channel: 0, velocity: 100) do
|
|
16
|
+
# note_on("C4") # Uses context: channel 0, velocity 100
|
|
17
|
+
# note_on("E4", velocity: 127) # Overrides velocity to 127
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Control changes in context
|
|
21
|
+
# MIDIEvents.with(channel: 0) do
|
|
22
|
+
# control_change("Modulation Wheel", 64)
|
|
23
|
+
# program_change("Acoustic Grand Piano")
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
4
27
|
class Context
|
|
5
28
|
|
|
6
|
-
|
|
29
|
+
# @return [Integer, nil] The MIDI channel (0-15) for messages created in this context
|
|
30
|
+
attr_accessor :channel
|
|
7
31
|
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#
|
|
12
|
-
#
|
|
32
|
+
# @return [Integer, nil] The velocity (0-127) for note messages created in this context
|
|
33
|
+
attr_accessor :velocity
|
|
34
|
+
|
|
35
|
+
# Create and execute a context with the given parameters
|
|
36
|
+
#
|
|
37
|
+
# @param [Hash] options Context parameters
|
|
38
|
+
# @param [Proc] block The block to execute within this context
|
|
39
|
+
# @option options [Integer] :channel MIDI channel (0-15)
|
|
40
|
+
# @option options [Integer] :velocity Note velocity (0-127)
|
|
41
|
+
# @return [Object] The result of evaluating the block
|
|
13
42
|
def self.with(options = {}, &block)
|
|
14
43
|
new(options, &block).instance_eval(&block)
|
|
15
44
|
end
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
module MIDIEvents
|
|
2
|
-
|
|
3
|
-
#
|
|
2
|
+
|
|
3
|
+
# Utilities for converting between different MIDI data representations
|
|
4
|
+
#
|
|
5
|
+
# Provides methods to convert between hex strings, nibbles, and numeric byte arrays.
|
|
6
|
+
# Useful for working with MIDI data in different formats.
|
|
7
|
+
#
|
|
8
|
+
# @example Converting hex string to bytes
|
|
9
|
+
# MIDIEvents::TypeConversion.hex_string_to_numeric_byte_array("904040")
|
|
10
|
+
# # => [0x90, 0x40, 0x40]
|
|
11
|
+
#
|
|
12
|
+
# @example Converting bytes to hex string
|
|
13
|
+
# MIDIEvents::TypeConversion.numeric_byte_array_to_hex_string([0x90, 0x40, 0x40])
|
|
14
|
+
# # => "904040"
|
|
15
|
+
#
|
|
16
|
+
# @api public
|
|
4
17
|
module TypeConversion
|
|
5
|
-
|
|
18
|
+
|
|
6
19
|
extend self
|
|
7
20
|
|
|
8
|
-
# Convert an array of hex nibbles to
|
|
9
|
-
#
|
|
21
|
+
# Convert an array of hex character nibbles to numeric bytes
|
|
22
|
+
#
|
|
23
|
+
# Pairs nibbles together to form bytes. If there's an odd number of nibbles,
|
|
24
|
+
# the second-to-last nibble is removed.
|
|
10
25
|
#
|
|
11
|
-
# @
|
|
12
|
-
#
|
|
26
|
+
# @example
|
|
27
|
+
# hex_chars_to_numeric_byte_array(["9", "0", "4", "0"])
|
|
28
|
+
# # => [0x90, 0x40]
|
|
29
|
+
#
|
|
30
|
+
# @param [Array<String>] nibbles Array of hex character strings (e.g., ["9", "0", "4", "0"])
|
|
31
|
+
# @return [Array<Integer>] Array of numeric bytes (e.g., [0x90, 0x40])
|
|
13
32
|
def hex_chars_to_numeric_byte_array(nibbles)
|
|
14
33
|
nibbles = nibbles.dup # Don't mess with the input
|
|
15
34
|
# get rid of last nibble if there's an odd number
|
|
@@ -23,10 +42,14 @@ module MIDIEvents
|
|
|
23
42
|
bytes
|
|
24
43
|
end
|
|
25
44
|
|
|
26
|
-
# Convert
|
|
27
|
-
#
|
|
28
|
-
# @
|
|
29
|
-
#
|
|
45
|
+
# Convert a hex string to an array of numeric bytes
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# hex_string_to_numeric_byte_array("904040")
|
|
49
|
+
# # => [0x90, 0x40, 0x40]
|
|
50
|
+
#
|
|
51
|
+
# @param [String] string A string of hex digits (e.g., "904040")
|
|
52
|
+
# @return [Array<Integer>] An array of numeric bytes (e.g., [0x90, 0x40, 0x40])
|
|
30
53
|
def hex_string_to_numeric_byte_array(string)
|
|
31
54
|
string = string.dup
|
|
32
55
|
bytes = []
|
|
@@ -36,18 +59,26 @@ module MIDIEvents
|
|
|
36
59
|
bytes
|
|
37
60
|
end
|
|
38
61
|
|
|
39
|
-
# Convert a
|
|
40
|
-
#
|
|
41
|
-
# @
|
|
42
|
-
#
|
|
62
|
+
# Convert a hex string to an array of character nibbles
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# hex_str_to_hex_chars("904040")
|
|
66
|
+
# # => ["9", "0", "4", "0", "4", "0"]
|
|
67
|
+
#
|
|
68
|
+
# @param [String] string A string of hex digits (e.g., "904040")
|
|
69
|
+
# @return [Array<String>] An array of individual hex character nibbles
|
|
43
70
|
def hex_str_to_hex_chars(string)
|
|
44
71
|
string.split(//)
|
|
45
72
|
end
|
|
46
73
|
|
|
47
|
-
# Convert an array of numeric bytes to
|
|
48
|
-
#
|
|
49
|
-
# @
|
|
50
|
-
#
|
|
74
|
+
# Convert an array of numeric bytes to an uppercase hex string
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# numeric_byte_array_to_hex_string([0x90, 0x40, 0x40])
|
|
78
|
+
# # => "904040"
|
|
79
|
+
#
|
|
80
|
+
# @param [Array<Integer>] bytes An array of numeric bytes (e.g., [0x90, 0x40, 0x40])
|
|
81
|
+
# @return [String] An uppercase hex string (e.g., "904040")
|
|
51
82
|
def numeric_byte_array_to_hex_string(bytes)
|
|
52
83
|
string_bytes = bytes.map do |byte|
|
|
53
84
|
string = byte.to_s(16)
|
|
@@ -57,19 +88,27 @@ module MIDIEvents
|
|
|
57
88
|
string_bytes.join.upcase
|
|
58
89
|
end
|
|
59
90
|
|
|
60
|
-
# Convert a numeric byte to hex
|
|
61
|
-
#
|
|
62
|
-
# @
|
|
63
|
-
#
|
|
91
|
+
# Convert a numeric byte to hex character nibbles
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# numeric_byte_to_hex_chars(0x90)
|
|
95
|
+
# # => ["9", "0"]
|
|
96
|
+
#
|
|
97
|
+
# @param [Integer] num A numeric byte (e.g., 0x90)
|
|
98
|
+
# @return [Array<String>] An array of two hex character nibbles (e.g., ["9", "0"])
|
|
64
99
|
def numeric_byte_to_hex_chars(num)
|
|
65
100
|
nibbles = numeric_byte_to_nibbles(num)
|
|
66
101
|
nibbles.map { |n| n.to_s(16) }
|
|
67
102
|
end
|
|
68
103
|
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# @
|
|
72
|
-
#
|
|
104
|
+
# Split a numeric byte into its high and low nibbles
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# numeric_byte_to_nibbles(0x90)
|
|
108
|
+
# # => [0x9, 0x0]
|
|
109
|
+
#
|
|
110
|
+
# @param [Integer] num A numeric byte (e.g., 0x90)
|
|
111
|
+
# @return [Array<Integer>] An array of two nibbles [high, low] (e.g., [0x9, 0x0])
|
|
73
112
|
def numeric_byte_to_nibbles(num)
|
|
74
113
|
[((num & 0xF0) >> 4), (num & 0x0F)]
|
|
75
114
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Ruby MIDI Events library - object-oriented representation of MIDI messages
|
|
2
|
+
#
|
|
3
|
+
# This library provides a comprehensive set of classes and modules for working with MIDI events
|
|
4
|
+
# in Ruby. It offers an intuitive API for creating and manipulating various MIDI message types
|
|
5
|
+
# including channel messages (notes, control changes, program changes), system messages, and
|
|
6
|
+
# system exclusive (SysEx) messages.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic note creation
|
|
9
|
+
# require 'midi-events'
|
|
10
|
+
#
|
|
11
|
+
# # Create a middle C note-on message on channel 0 with velocity 64
|
|
12
|
+
# note = MIDIEvents::NoteOn.new(0, 64, 64)
|
|
13
|
+
# # => #<MIDIEvents::NoteOn @channel=0, @note=64, @velocity=64>
|
|
14
|
+
#
|
|
15
|
+
# @example Using note names
|
|
16
|
+
# # Create note using named constant
|
|
17
|
+
# note = MIDIEvents::NoteOn["E4"].new(0, 100)
|
|
18
|
+
# # => #<MIDIEvents::NoteOn @channel=0, @note=64, @velocity=100, @name="E4">
|
|
19
|
+
#
|
|
20
|
+
# @example Using context for common parameters
|
|
21
|
+
# # Set channel and velocity as context
|
|
22
|
+
# MIDIEvents.with(channel: 0, velocity: 100) do
|
|
23
|
+
# note_on("E4") # Creates note-on with channel 0, velocity 100
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Working with control changes
|
|
27
|
+
# # Create modulation wheel control change
|
|
28
|
+
# cc = MIDIEvents::ControlChange["Modulation Wheel"].new(0, 64)
|
|
29
|
+
#
|
|
30
|
+
# @example System exclusive messages
|
|
31
|
+
# # Create a SysEx node representing a device
|
|
32
|
+
# synth = MIDIEvents::SystemExclusive::Node.new(0x41, model_id: 0x42, device_id: 0x10)
|
|
33
|
+
#
|
|
34
|
+
# # Send a command to the device
|
|
35
|
+
# command = synth.command([0x40, 0x7F, 0x00], 0x00)
|
|
36
|
+
#
|
|
37
|
+
# @see MIDIEvents::Context For DSL-style message creation
|
|
38
|
+
# @see MIDIEvents::Constant For MIDI constant lookups
|
|
39
|
+
#
|
|
40
|
+
# @author (c)2021 Javier Sánchez Yeste for the modifications, licensed under LGPL 3.0 License
|
|
41
|
+
# @author (c)2011-2015 Ari Russo for original MIDI Message library, licensed under Apache 2.0 License
|
|
42
|
+
#
|
|
43
|
+
# @note This library is part of the MusaDSL ecosystem
|
|
44
|
+
# @note Based on Ari Russo's MIDI Message library with performance optimizations
|
|
45
|
+
module MIDIEvents
|
|
46
|
+
VERSION = '0.7.0'.freeze
|
|
47
|
+
end
|
data/lib/midi-events.rb
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
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
1
|
# Libs
|
|
9
2
|
require 'forwardable'
|
|
10
3
|
require 'yaml'
|
|
@@ -22,6 +15,4 @@ require 'midi-events/type_conversion'
|
|
|
22
15
|
require 'midi-events/context'
|
|
23
16
|
require 'midi-events/messages'
|
|
24
17
|
|
|
25
|
-
|
|
26
|
-
VERSION = '0.5.1'.freeze
|
|
27
|
-
end
|
|
18
|
+
require_relative 'midi-events/version'
|