midi-parser 0.4.0 → 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 +4 -4
- data/.gitignore +2 -1
- data/.version +6 -0
- data/.yardopts +6 -0
- data/LICENSE +159 -668
- data/README.md +7 -15
- data/examples/usage.rb +2 -24
- data/lib/midi-parser/data_processor.rb +44 -18
- data/lib/midi-parser/message_builder.rb +36 -1
- data/lib/midi-parser/parser.rb +48 -16
- data/lib/midi-parser/session.rb +86 -11
- data/lib/midi-parser/type_conversion.rb +70 -27
- data/lib/midi-parser/version.rb +3 -0
- data/lib/midi-parser.rb +51 -4
- data/midi-parser.gemspec +12 -9
- metadata +52 -4
data/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# MIDI Parser
|
|
2
2
|
|
|
3
|
+
[](https://www.ruby-lang.org/)
|
|
4
|
+
[](https://www.gnu.org/licenses/lgpl-3.0.html)
|
|
5
|
+
|
|
3
6
|
**Ruby Parser for Raw MIDI Messages**
|
|
4
7
|
|
|
5
8
|
This library is part of a suite of Ruby libraries for MIDI:
|
|
@@ -92,7 +95,7 @@ Use running status:
|
|
|
92
95
|
|
|
93
96
|
```ruby
|
|
94
97
|
midi_parser.parse(0x40, 64)
|
|
95
|
-
=> #<
|
|
98
|
+
=> #<MIDIEvents::NoteOn:0x98c9818 ...>
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
Add an incomplete message:
|
|
@@ -116,10 +119,11 @@ MIDI Parser generates [midi-events](http://github.com/javier-sy/midi-events) obj
|
|
|
116
119
|
|
|
117
120
|
## Documentation
|
|
118
121
|
|
|
119
|
-
*
|
|
122
|
+
* [rdoc](http://rubydoc.info/github/javier-sy/midi-parser)
|
|
120
123
|
|
|
121
124
|
## Differences between [MIDI Parser](https://github.com/javier-sy/midi-parser) and [Nibbler](https://github.com/arirusso/nibbler)
|
|
122
125
|
[MIDI Parser](https://github.com/javier-sy/midi-parser) is mostly a clone of [Nibbler](https://github.com/arirusso/nibbler) with some modifications:
|
|
126
|
+
|
|
123
127
|
* Removed logging attributes (messages, rejected, processed) to reduce parsing overhead
|
|
124
128
|
* Updated dependencies versions
|
|
125
129
|
* Source updated to Ruby 2.7 code conventions (method keyword parameters instead of options = {}, hash keys as 'key:' instead of ':key =>', etc.)
|
|
@@ -128,8 +132,6 @@ MIDI Parser generates [midi-events](http://github.com/javier-sy/midi-events) obj
|
|
|
128
132
|
* Renamed module to MIDIParser instead of Nibbler
|
|
129
133
|
* Renamed gem to midi-parser instead of nibbler
|
|
130
134
|
* Minor docs fixing
|
|
131
|
-
* TODO: update tests to use rspec instead of rake
|
|
132
|
-
* TODO: migrate to (or confirm it's working ok on) Ruby 3.0 or Ruby 3.1
|
|
133
135
|
|
|
134
136
|
## Then, why does exist this library if it is mostly a clone of another library?
|
|
135
137
|
|
|
@@ -163,16 +165,6 @@ I've decided to publish my own renamed version of the modified dependencies beca
|
|
|
163
165
|
|
|
164
166
|
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.
|
|
165
167
|
|
|
166
|
-
| Function | Library | Based on Ari Russo's| Difference |
|
|
167
|
-
| --- | --- | --- | --- |
|
|
168
|
-
| MIDI Events representation | [MIDI Events](https://github.com/javier-sy/midi-events) | [MIDI Message](https://github.com/arirusso/midi-message) | removed parsing, small improvements |
|
|
169
|
-
| MIDI Data parsing | [MIDI Parser](https://github.com/javier-sy/midi-parser) | [Nibbler](https://github.com/arirusso/nibbler) | removed process history information, minor optimizations |
|
|
170
|
-
| 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)
|
|
171
|
-
| 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 |
|
|
172
|
-
| Low level MIDI interface to Linux | **TO DO** | | |
|
|
173
|
-
| Low level MIDI interface to JRuby | **TO DO** | | |
|
|
174
|
-
| Low level MIDI interface to Windows | **TO DO** | | |
|
|
175
|
-
|
|
176
168
|
## Author
|
|
177
169
|
|
|
178
170
|
* [Javier Sánchez Yeste](https://github.com/javier-sy)
|
|
@@ -183,6 +175,6 @@ Thanks to [Ari Russo](http://github.com/arirusso) for his ruby library [Nibbler]
|
|
|
183
175
|
|
|
184
176
|
## License
|
|
185
177
|
|
|
186
|
-
[MIDI Parser](https://github.com/javier-sy/midi-parser) Copyright (c) 2021 [Javier Sánchez Yeste](https://yeste.studio), licensed under LGPL 3.0 License
|
|
178
|
+
[MIDI Parser](https://github.com/javier-sy/midi-parser) Copyright (c) 2021-2025 [Javier Sánchez Yeste](https://yeste.studio), licensed under LGPL 3.0 License
|
|
187
179
|
|
|
188
180
|
[Nibbler](https://github.com/arirusso/nibbler) Copyright (c) 2011-2015 [Ari Russo](http://arirusso.com), licensed under Apache License 2.0 (see the file LICENSE.nibbler)
|
data/examples/usage.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
$:.unshift(File.join('..', 'lib'))
|
|
3
3
|
|
|
4
4
|
#
|
|
5
|
-
# Walk through different ways to use
|
|
5
|
+
# Walk through different ways to use MIDI Parser
|
|
6
6
|
#
|
|
7
7
|
|
|
8
8
|
require 'midi-parser'
|
|
@@ -46,26 +46,4 @@ pp 'See progress'
|
|
|
46
46
|
|
|
47
47
|
pp midi_parser.buffer # should give you an array of bits
|
|
48
48
|
|
|
49
|
-
pp midi_parser.buffer_s # should give you
|
|
50
|
-
|
|
51
|
-
pp 'Pass in a timestamp'
|
|
52
|
-
|
|
53
|
-
# note:
|
|
54
|
-
# once you pass in a timestamp for the first time, midi-parser.messages will then return
|
|
55
|
-
# an array of message/timestamp hashes
|
|
56
|
-
# if there was no timestamp for a particular message it will be nil
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
pp midi_parser.parse('904040', timestamp: Time.now.to_i)
|
|
60
|
-
|
|
61
|
-
pp 'Add callbacks'
|
|
62
|
-
|
|
63
|
-
# you can list any properties of the message to check against.
|
|
64
|
-
# if they are all true, the callback will fire
|
|
65
|
-
#
|
|
66
|
-
# if you wish to use "or" or any more advanced matching I would just process the message after it"s
|
|
67
|
-
# returned
|
|
68
|
-
#
|
|
69
|
-
midi_parser.when({ class: MIDIMessage::NoteOn }) { |msg| puts 'bark' }
|
|
70
|
-
pp midi_parser.parse('904040')
|
|
71
|
-
pp midi_parser.parse('804040')
|
|
49
|
+
pp midi_parser.buffer_s # should give you a hex string
|
|
@@ -1,28 +1,53 @@
|
|
|
1
1
|
module MIDIParser
|
|
2
|
-
#
|
|
2
|
+
# Normalizes various input formats into hex nibble strings for parsing.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# DataProcessor accepts bytes, hex strings, nibbles, and arrays, converting
|
|
5
|
+
# them into a consistent format (uppercase hex character strings) that can
|
|
6
|
+
# be processed by the {Parser}.
|
|
6
7
|
#
|
|
7
|
-
#
|
|
8
|
+
# This module handles the complexity of accepting multiple input formats,
|
|
9
|
+
# allowing users to mix and match formats in a single parse call.
|
|
8
10
|
#
|
|
11
|
+
# @note This outputs String objects rather than Integers because Ruby's
|
|
12
|
+
# 0x0 and 0x00 are the same Integer, which would make nibbles and bytes
|
|
13
|
+
# ambiguous.
|
|
14
|
+
#
|
|
15
|
+
# @example Processing bytes
|
|
16
|
+
# MIDIParser::DataProcessor.process([0x90, 0x40, 0x40])
|
|
17
|
+
# # => ["9", "0", "4", "0", "4", "0"]
|
|
18
|
+
#
|
|
19
|
+
# @example Processing hex string
|
|
20
|
+
# MIDIParser::DataProcessor.process(["904040"])
|
|
21
|
+
# # => ["9", "0", "4", "0", "4", "0"]
|
|
22
|
+
#
|
|
23
|
+
# @example Processing mixed input
|
|
24
|
+
# MIDIParser::DataProcessor.process(["9", "0", 0x40, 64])
|
|
25
|
+
# # => ["9", "0", "4", "0", "4", "0"]
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
9
28
|
module DataProcessor
|
|
10
29
|
extend self
|
|
11
30
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
31
|
+
# Converts various input formats to an array of hex nibble strings.
|
|
32
|
+
#
|
|
33
|
+
# Invalid input (non-hex characters, out-of-range bytes) is filtered out.
|
|
14
34
|
#
|
|
15
|
-
# @param [
|
|
16
|
-
# @return [Array<String>]
|
|
35
|
+
# @param args [Array<String, Integer, Array>] input data in various formats
|
|
36
|
+
# @return [Array<String>] array of uppercase hex nibble strings (e.g., ["9", "0", "4", "0"])
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# DataProcessor.process([0x90, "40", 0x40])
|
|
40
|
+
# # => ["9", "0", "4", "0", "4", "0"]
|
|
17
41
|
def process(*args)
|
|
18
42
|
args.map { |arg| convert(arg) }.flatten.compact.map(&:upcase)
|
|
19
43
|
end
|
|
20
44
|
|
|
21
45
|
private
|
|
22
46
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# @
|
|
47
|
+
# Converts a single value to hex character strings.
|
|
48
|
+
#
|
|
49
|
+
# @param value [Array, String, Integer] value to convert
|
|
50
|
+
# @return [Array<String>, nil] hex character strings, or nil if invalid
|
|
26
51
|
def convert(value)
|
|
27
52
|
case value
|
|
28
53
|
when Array then value.map { |arr| process(*arr) }.reduce(:+)
|
|
@@ -31,17 +56,18 @@ module MIDIParser
|
|
|
31
56
|
end
|
|
32
57
|
end
|
|
33
58
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# @param [Integer]
|
|
37
|
-
# @return [Integer, nil]
|
|
59
|
+
# Filters numeric values to valid MIDI byte range.
|
|
60
|
+
#
|
|
61
|
+
# @param num [Integer] byte value to filter
|
|
62
|
+
# @return [Integer, nil] the value if valid (0x00-0xFF), nil otherwise
|
|
38
63
|
def filter_numeric(num)
|
|
39
64
|
num if (0x00..0xFF).include?(num)
|
|
40
65
|
end
|
|
41
66
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# @
|
|
67
|
+
# Filters a string to only valid hex characters.
|
|
68
|
+
#
|
|
69
|
+
# @param string [String] input string
|
|
70
|
+
# @return [String] string with only hex characters (0-9, A-F)
|
|
45
71
|
def filter_string(string)
|
|
46
72
|
string.gsub(/[^0-9a-fA-F]/, '').upcase
|
|
47
73
|
end
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
module MIDIParser
|
|
2
|
+
# Factory for building MIDI message objects from parsed data.
|
|
3
|
+
#
|
|
4
|
+
# MessageBuilder maps MIDI status bytes to the appropriate MIDIEvents
|
|
5
|
+
# message classes and handles the construction of message objects.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
2
8
|
class MessageBuilder
|
|
9
|
+
# Channel message type mappings.
|
|
10
|
+
# Maps status nibble to message class and expected nibble count.
|
|
3
11
|
CHANNEL_MESSAGE = [
|
|
4
12
|
{
|
|
5
13
|
status: 0x8,
|
|
@@ -38,6 +46,8 @@ module MIDIParser
|
|
|
38
46
|
}
|
|
39
47
|
].freeze
|
|
40
48
|
|
|
49
|
+
# System message type mappings.
|
|
50
|
+
# Maps status nibble to message class and expected nibble count.
|
|
41
51
|
SYSTEM_MESSAGE = [
|
|
42
52
|
{
|
|
43
53
|
status: 0x1..0x6,
|
|
@@ -51,27 +61,52 @@ module MIDIParser
|
|
|
51
61
|
}
|
|
52
62
|
].freeze
|
|
53
63
|
|
|
54
|
-
|
|
64
|
+
# @return [Integer] number of nibbles expected for this message type
|
|
65
|
+
attr_reader :num_nibbles
|
|
55
66
|
|
|
67
|
+
# @return [String, nil] optional name for the message type
|
|
68
|
+
attr_reader :name
|
|
69
|
+
|
|
70
|
+
# @return [Class] the MIDIEvents class to instantiate
|
|
71
|
+
attr_reader :clazz
|
|
72
|
+
|
|
73
|
+
# Builds a System Exclusive message from raw data.
|
|
74
|
+
#
|
|
75
|
+
# @param message_data [Array<Integer>] SysEx message bytes
|
|
76
|
+
# @return [MIDIEvents::SystemExclusive] the constructed message
|
|
56
77
|
def self.build_system_exclusive(*message_data)
|
|
57
78
|
MIDIEvents::SystemExclusive.new(*message_data)
|
|
58
79
|
end
|
|
59
80
|
|
|
81
|
+
# Creates a MessageBuilder for a system message type.
|
|
82
|
+
#
|
|
83
|
+
# @param status [Integer] the second status nibble (0x1-0xF)
|
|
84
|
+
# @return [MessageBuilder] configured builder for the message type
|
|
60
85
|
def self.for_system_message(status)
|
|
61
86
|
type = SYSTEM_MESSAGE.find { |type| type[:status].cover?(status) }
|
|
62
87
|
new(type[:nibbles], type[:class])
|
|
63
88
|
end
|
|
64
89
|
|
|
90
|
+
# Creates a MessageBuilder for a channel message type.
|
|
91
|
+
#
|
|
92
|
+
# @param status [Integer] the first status nibble (0x8-0xE)
|
|
93
|
+
# @return [MessageBuilder] configured builder for the message type
|
|
65
94
|
def self.for_channel_message(status)
|
|
66
95
|
type = CHANNEL_MESSAGE.find { |type| type[:status] == status }
|
|
67
96
|
new(type[:nibbles], type[:class])
|
|
68
97
|
end
|
|
69
98
|
|
|
99
|
+
# @param num_nibbles [Integer] expected nibble count for this message type
|
|
100
|
+
# @param clazz [Class] MIDIEvents class to instantiate
|
|
70
101
|
def initialize(num_nibbles, clazz)
|
|
71
102
|
@num_nibbles = num_nibbles
|
|
72
103
|
@clazz = clazz
|
|
73
104
|
end
|
|
74
105
|
|
|
106
|
+
# Builds a MIDI message from the given data.
|
|
107
|
+
#
|
|
108
|
+
# @param message_data [Array<Integer>] message data bytes
|
|
109
|
+
# @return [MIDIEvents::ChannelMessage, MIDIEvents::SystemMessage] the constructed message
|
|
75
110
|
def build(*message_data)
|
|
76
111
|
@clazz.new(*message_data)
|
|
77
112
|
end
|
data/lib/midi-parser/parser.rb
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
module MIDIParser
|
|
2
|
+
# Low-level MIDI message parser.
|
|
3
|
+
#
|
|
4
|
+
# Parser handles the actual parsing logic, converting hex nibbles into
|
|
5
|
+
# MIDI message objects. It maintains an internal buffer and supports
|
|
6
|
+
# MIDI running status.
|
|
7
|
+
#
|
|
8
|
+
# This class is typically not used directly. Use {Session} instead for
|
|
9
|
+
# a higher-level interface.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
2
12
|
class Parser
|
|
13
|
+
# @return [Array<String>] the current buffer of hex nibble strings
|
|
3
14
|
attr_reader :buffer
|
|
4
15
|
|
|
16
|
+
# Creates a new parser with empty buffer and running status.
|
|
5
17
|
def initialize
|
|
6
18
|
@running_status = RunningStatus.new
|
|
7
19
|
@buffer = []
|
|
8
20
|
end
|
|
9
21
|
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
22
|
+
# Processes hex nibbles and returns any complete MIDI messages.
|
|
23
|
+
#
|
|
24
|
+
# Nibbles are accumulated in the buffer. When enough data is present
|
|
25
|
+
# to form a complete MIDI message, it is parsed and returned.
|
|
26
|
+
#
|
|
27
|
+
# @param nibbles [Array<String>] hex nibble strings (e.g., ["9", "0", "4", "0"])
|
|
28
|
+
# @return [Array<MIDIEvents::ChannelMessage, MIDIEvents::SystemMessage>]
|
|
29
|
+
# array of parsed messages
|
|
13
30
|
def process(nibbles)
|
|
14
31
|
messages = []
|
|
15
32
|
pointer = 0
|
|
@@ -33,9 +50,10 @@ module MIDIParser
|
|
|
33
50
|
messages
|
|
34
51
|
end
|
|
35
52
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# @
|
|
53
|
+
# Attempts to convert a fragment of nibbles to a MIDI message.
|
|
54
|
+
#
|
|
55
|
+
# @param fragment [Array<String>] hex nibble strings (e.g., ["9", "0", "4", "0"])
|
|
56
|
+
# @return [Hash, nil] hash with :message and :processed keys, or nil if incomplete
|
|
39
57
|
def nibbles_to_message(fragment)
|
|
40
58
|
if fragment.length >= 2
|
|
41
59
|
# convert the part of the fragment to start with to a numeric
|
|
@@ -76,15 +94,15 @@ module MIDIParser
|
|
|
76
94
|
@buffer[pointer, (@buffer.length - pointer)]
|
|
77
95
|
end
|
|
78
96
|
|
|
79
|
-
#
|
|
80
|
-
# to build a MIDI message
|
|
97
|
+
# Attempts to build a MIDI message from a fragment with enough nibbles.
|
|
81
98
|
#
|
|
82
|
-
# @param [
|
|
83
|
-
# @param [
|
|
84
|
-
# @param [Hash] options
|
|
85
|
-
# @option options [String] :status_nibble_2
|
|
86
|
-
# @option options [Boolean] :recursive
|
|
87
|
-
# @
|
|
99
|
+
# @param fragment [Array<String>] hex nibble strings
|
|
100
|
+
# @param message_builder [MessageBuilder] builder for the message type
|
|
101
|
+
# @param options [Hash] additional options
|
|
102
|
+
# @option options [String] :status_nibble_2 cached status nibble for running status
|
|
103
|
+
# @option options [Boolean] :recursive whether to try shorter lengths
|
|
104
|
+
# @option options [Integer] :offset nibble offset adjustment
|
|
105
|
+
# @return [Hash, nil] hash with :message and :processed keys, or nil
|
|
88
106
|
def lookahead(fragment, message_builder, options = {})
|
|
89
107
|
offset = options.fetch(:offset, 0)
|
|
90
108
|
num_nibbles = message_builder.num_nibbles + offset
|
|
@@ -127,6 +145,12 @@ module MIDIParser
|
|
|
127
145
|
end
|
|
128
146
|
end
|
|
129
147
|
|
|
148
|
+
# Manages MIDI running status state.
|
|
149
|
+
#
|
|
150
|
+
# Running status is a MIDI optimization where the status byte can be omitted
|
|
151
|
+
# if it's the same as the previous message.
|
|
152
|
+
#
|
|
153
|
+
# @api private
|
|
130
154
|
class RunningStatus
|
|
131
155
|
extend Forwardable
|
|
132
156
|
|
|
@@ -136,16 +160,24 @@ module MIDIParser
|
|
|
136
160
|
|
|
137
161
|
def_delegators :@state, :[]
|
|
138
162
|
|
|
163
|
+
# Clears the running status state.
|
|
164
|
+
# @return [nil]
|
|
139
165
|
def cancel
|
|
140
166
|
@state = nil
|
|
141
167
|
end
|
|
142
168
|
|
|
143
|
-
#
|
|
144
|
-
# @return [Boolean]
|
|
169
|
+
# Checks if running status is available.
|
|
170
|
+
# @return [Boolean] true if a previous status is cached
|
|
145
171
|
def possible?
|
|
146
172
|
!@state.nil?
|
|
147
173
|
end
|
|
148
174
|
|
|
175
|
+
# Stores the running status state.
|
|
176
|
+
#
|
|
177
|
+
# @param offset [Integer] nibble offset for running status
|
|
178
|
+
# @param message_builder [MessageBuilder] builder for message type
|
|
179
|
+
# @param status_nibble_2 [String] second status nibble
|
|
180
|
+
# @return [Hash] the stored state
|
|
149
181
|
def set(offset, message_builder, status_nibble_2)
|
|
150
182
|
@state = {
|
|
151
183
|
message_builder: message_builder,
|
data/lib/midi-parser/session.rb
CHANGED
|
@@ -1,35 +1,110 @@
|
|
|
1
1
|
module MIDIParser
|
|
2
|
-
# A parser session
|
|
2
|
+
# A parser session that maintains state between parse calls.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Session is the main interface for parsing MIDI data. It wraps the {Parser}
|
|
5
|
+
# and provides a convenient API for parsing various input formats and
|
|
6
|
+
# accessing the internal buffer.
|
|
6
7
|
#
|
|
8
|
+
# The session maintains a buffer of unparsed nibbles between calls, allowing
|
|
9
|
+
# for incremental parsing of MIDI data as it arrives.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic parsing
|
|
12
|
+
# session = MIDIParser::Session.new
|
|
13
|
+
# messages = session.parse(0x90, 0x40, 0x40)
|
|
14
|
+
# # => [#<MIDIEvents::NoteOn ...>]
|
|
15
|
+
#
|
|
16
|
+
# @example Incremental parsing
|
|
17
|
+
# session = MIDIParser::Session.new
|
|
18
|
+
# session.parse("90") # => []
|
|
19
|
+
# session.parse("40") # => []
|
|
20
|
+
# session.buffer # => ["9", "0", "4", "0"]
|
|
21
|
+
# session.parse("40") # => [#<MIDIEvents::NoteOn ...>]
|
|
22
|
+
#
|
|
23
|
+
# @example Mixed input types
|
|
24
|
+
# session = MIDIParser::Session.new
|
|
25
|
+
# session.parse("9", "0", 0x40, 64)
|
|
26
|
+
# # => [#<MIDIEvents::NoteOn ...>]
|
|
27
|
+
#
|
|
28
|
+
# @see MIDIParser.new Convenience constructor
|
|
29
|
+
# @see Parser The underlying parser
|
|
30
|
+
#
|
|
31
|
+
# @api public
|
|
7
32
|
class Session
|
|
33
|
+
# Creates a new parser session.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# session = MIDIParser::Session.new
|
|
8
37
|
def initialize
|
|
9
38
|
@parser = Parser.new
|
|
10
39
|
end
|
|
11
40
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
41
|
+
# Returns the current buffer contents as an array of hex nibble strings.
|
|
42
|
+
#
|
|
43
|
+
# The buffer contains unparsed MIDI data waiting for more bytes
|
|
44
|
+
# to complete a message.
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<String>] array of hex nibble strings (e.g., ["9", "0", "4", "0"])
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# session = MIDIParser::Session.new
|
|
50
|
+
# session.parse("90", "40")
|
|
51
|
+
# session.buffer # => ["9", "0", "4", "0"]
|
|
14
52
|
def buffer
|
|
15
53
|
@parser.buffer
|
|
16
54
|
end
|
|
17
55
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
56
|
+
# Returns the current buffer contents as a single hex string.
|
|
57
|
+
#
|
|
58
|
+
# @return [String] concatenated hex string of buffer contents
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# session = MIDIParser::Session.new
|
|
62
|
+
# session.parse("90", "40")
|
|
63
|
+
# session.buffer_s # => "9040"
|
|
20
64
|
def buffer_s
|
|
21
65
|
@parser.buffer.join
|
|
22
66
|
end
|
|
23
67
|
alias buffer_hex buffer_s
|
|
24
68
|
|
|
25
|
-
#
|
|
69
|
+
# Clears the parser buffer, discarding any unparsed data.
|
|
70
|
+
#
|
|
71
|
+
# @return [Array] empty array
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# session = MIDIParser::Session.new
|
|
75
|
+
# session.parse("90", "40")
|
|
76
|
+
# session.clear_buffer
|
|
77
|
+
# session.buffer # => []
|
|
26
78
|
def clear_buffer
|
|
27
79
|
@parser.buffer.clear
|
|
28
80
|
end
|
|
29
81
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
82
|
+
# Parses MIDI data and returns any complete messages.
|
|
83
|
+
#
|
|
84
|
+
# Accepts various input formats: bytes (Integer), hex strings (String),
|
|
85
|
+
# nibbles, or arrays. Input is accumulated in an internal buffer until
|
|
86
|
+
# complete MIDI messages can be formed.
|
|
87
|
+
#
|
|
88
|
+
# @param args [Array<Integer, String>] MIDI data in various formats:
|
|
89
|
+
# - Bytes as integers (e.g., 0x90, 0x40, 0x40)
|
|
90
|
+
# - Hex strings (e.g., "904040" or "90", "40", "40")
|
|
91
|
+
# - Nibbles as strings (e.g., "9", "0", "4", "0")
|
|
92
|
+
# - Mixed formats
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<MIDIEvents::ChannelMessage, MIDIEvents::SystemMessage>]
|
|
95
|
+
# array of parsed MIDI message objects (empty if no complete messages)
|
|
96
|
+
#
|
|
97
|
+
# @example Parse bytes
|
|
98
|
+
# session.parse(0x90, 0x40, 0x40)
|
|
99
|
+
# # => [#<MIDIEvents::NoteOn @channel=0, @note=64, @velocity=64>]
|
|
100
|
+
#
|
|
101
|
+
# @example Parse hex string
|
|
102
|
+
# session.parse("904040")
|
|
103
|
+
# # => [#<MIDIEvents::NoteOn ...>]
|
|
104
|
+
#
|
|
105
|
+
# @example Parse multiple messages
|
|
106
|
+
# session.parse("90404080404000")
|
|
107
|
+
# # => [#<MIDIEvents::NoteOn ...>, #<MIDIEvents::NoteOff ...>]
|
|
33
108
|
def parse(*args)
|
|
34
109
|
queue = DataProcessor.process(args)
|
|
35
110
|
@parser.process(queue)
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
module MIDIParser
|
|
2
|
-
#
|
|
2
|
+
# Utility module for converting between hex strings, nibbles, and bytes.
|
|
3
|
+
#
|
|
4
|
+
# TypeConversion provides methods to transform MIDI data between different
|
|
5
|
+
# representations: hex character strings, numeric nibbles, and numeric bytes.
|
|
6
|
+
#
|
|
7
|
+
# @example Converting hex string to bytes
|
|
8
|
+
# TypeConversion.hex_str_to_numeric_bytes("904040")
|
|
9
|
+
# # => [0x90, 0x40, 0x40]
|
|
10
|
+
#
|
|
11
|
+
# @example Converting bytes to nibbles
|
|
12
|
+
# TypeConversion.numeric_byte_to_numeric_nibbles(0x90)
|
|
13
|
+
# # => [0x9, 0x0]
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
3
16
|
module TypeConversion
|
|
4
17
|
extend self
|
|
5
18
|
|
|
6
|
-
# Converts
|
|
7
|
-
#
|
|
8
|
-
# @param [Array<String>]
|
|
9
|
-
# @return [Array<Integer>]
|
|
19
|
+
# Converts hex nibble strings to numeric bytes.
|
|
20
|
+
#
|
|
21
|
+
# @param nibbles [Array<String>] hex character strings (e.g., ["9", "0", "4", "0"])
|
|
22
|
+
# @return [Array<Integer>] numeric bytes (e.g., [0x90, 0x40])
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# hex_chars_to_numeric_bytes(["9", "0", "4", "0", "4", "0"])
|
|
26
|
+
# # => [0x90, 0x40, 0x40]
|
|
10
27
|
def hex_chars_to_numeric_bytes(nibbles)
|
|
11
28
|
nibbles = nibbles.dup
|
|
12
29
|
# get rid of last nibble if there's an odd number
|
|
@@ -20,51 +37,77 @@ module MIDIParser
|
|
|
20
37
|
bytes
|
|
21
38
|
end
|
|
22
39
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# @param [String] string
|
|
26
|
-
# @return [Array<String>]
|
|
40
|
+
# Splits a hex string into individual character strings.
|
|
41
|
+
#
|
|
42
|
+
# @param string [String] hex digit string (e.g., "904040")
|
|
43
|
+
# @return [Array<String>] individual hex characters (e.g., ["9", "0", "4", "0", "4", "0"])
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# hex_str_to_hex_chars("904040")
|
|
47
|
+
# # => ["9", "0", "4", "0", "4", "0"]
|
|
27
48
|
def hex_str_to_hex_chars(string)
|
|
28
49
|
string.split(//)
|
|
29
50
|
end
|
|
30
51
|
|
|
31
|
-
# Converts a
|
|
32
|
-
#
|
|
33
|
-
# @param [String] string
|
|
34
|
-
# @return [Array<
|
|
52
|
+
# Converts a hex string to numeric nibbles.
|
|
53
|
+
#
|
|
54
|
+
# @param string [String] hex digit string (e.g., "904040")
|
|
55
|
+
# @return [Array<Integer>] numeric nibbles (e.g., [0x9, 0x0, 0x4, 0x0, 0x4, 0x0])
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# hex_str_to_numeric_nibbles("904040")
|
|
59
|
+
# # => [9, 0, 4, 0, 4, 0]
|
|
35
60
|
def hex_str_to_numeric_nibbles(string)
|
|
36
61
|
bytes = hex_str_to_numeric_bytes(string)
|
|
37
62
|
numeric_bytes_to_numeric_nibbles(bytes)
|
|
38
63
|
end
|
|
39
64
|
|
|
40
|
-
# Converts a
|
|
41
|
-
#
|
|
42
|
-
# @param [String] string
|
|
43
|
-
# @return [Array<
|
|
65
|
+
# Converts a hex string to numeric bytes.
|
|
66
|
+
#
|
|
67
|
+
# @param string [String] hex digit string (e.g., "904040")
|
|
68
|
+
# @return [Array<Integer>] numeric bytes (e.g., [0x90, 0x40, 0x40])
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# hex_str_to_numeric_bytes("904040")
|
|
72
|
+
# # => [144, 64, 64]
|
|
44
73
|
def hex_str_to_numeric_bytes(string)
|
|
45
74
|
chars = hex_str_to_hex_chars(string)
|
|
46
75
|
hex_chars_to_numeric_bytes(chars)
|
|
47
76
|
end
|
|
48
77
|
|
|
49
|
-
# Converts
|
|
50
|
-
#
|
|
51
|
-
# @param [Array<Integer>]
|
|
52
|
-
# @return [Array<
|
|
78
|
+
# Converts numeric bytes to numeric nibbles.
|
|
79
|
+
#
|
|
80
|
+
# @param bytes [Array<Integer>] byte values (e.g., [0x90, 0x40])
|
|
81
|
+
# @return [Array<Integer>] nibble values (e.g., [0x9, 0x0, 0x4, 0x0])
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# numeric_bytes_to_numeric_nibbles([0x90, 0x40, 0x40])
|
|
85
|
+
# # => [9, 0, 4, 0, 4, 0]
|
|
53
86
|
def numeric_bytes_to_numeric_nibbles(bytes)
|
|
54
87
|
bytes.map { |byte| numeric_byte_to_numeric_nibbles(byte) }.flatten
|
|
55
88
|
end
|
|
56
89
|
|
|
57
|
-
# Converts a numeric byte to
|
|
58
|
-
#
|
|
59
|
-
# @
|
|
90
|
+
# Converts a numeric byte to hex character strings.
|
|
91
|
+
#
|
|
92
|
+
# @param num [Integer] byte value (e.g., 0x90)
|
|
93
|
+
# @return [Array<String>] hex characters (e.g., ["9", "0"])
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# numeric_byte_to_hex_chars(0x90)
|
|
97
|
+
# # => ["9", "0"]
|
|
60
98
|
def numeric_byte_to_hex_chars(num)
|
|
61
99
|
nibbles = numeric_byte_to_numeric_nibbles(num)
|
|
62
100
|
nibbles.map { |n| n.to_s(16) }
|
|
63
101
|
end
|
|
64
102
|
|
|
65
|
-
# Converts a numeric byte to
|
|
66
|
-
#
|
|
67
|
-
# @
|
|
103
|
+
# Converts a numeric byte to numeric nibbles.
|
|
104
|
+
#
|
|
105
|
+
# @param num [Integer] byte value (e.g., 0x90)
|
|
106
|
+
# @return [Array<Integer>] nibble values (e.g., [0x9, 0x0])
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# numeric_byte_to_numeric_nibbles(0x90)
|
|
110
|
+
# # => [9, 0]
|
|
68
111
|
def numeric_byte_to_numeric_nibbles(num)
|
|
69
112
|
[((num & 0xF0) >> 4), (num & 0x0F)]
|
|
70
113
|
end
|