midi-parser 0.3.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 +10 -0
- data/LICENSE +674 -0
- data/LICENSE.nibbler +13 -0
- data/README.md +187 -0
- data/Rakefile +10 -0
- data/examples/usage.rb +71 -0
- data/lib/midi-parser/data_processor.rb +49 -0
- data/lib/midi-parser/message_builder.rb +79 -0
- data/lib/midi-parser/parser.rb +154 -0
- data/lib/midi-parser/session.rb +38 -0
- data/lib/midi-parser/type_conversion.rb +72 -0
- data/lib/midi-parser.rb +29 -0
- data/midi-parser.gemspec +22 -0
- metadata +57 -0
data/LICENSE.nibbler
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2011-2015 Ari Russo
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# MIDI Parser
|
2
|
+
|
3
|
+
**Ruby Parser for Raw MIDI Messages**
|
4
|
+
|
5
|
+
This library is part of a suite of Ruby libraries for MIDI:
|
6
|
+
|
7
|
+
| Function | Library |
|
8
|
+
| --- | --- |
|
9
|
+
| MIDI Events representation | [MIDI Events](https://github.com/javier-sy/midi-events) |
|
10
|
+
| MIDI Data parsing | [MIDI Parser](https://github.com/javier-sy/midi-parser) |
|
11
|
+
| MIDI communication with Instruments and Control Surfaces | [MIDI Communications](https://github.com/javier-sy/midi-communications) |
|
12
|
+
| Low level MIDI interface to MacOS | [MIDI Communications MacOS Layer](https://github.com/javier-sy/midi-communications-macos) |
|
13
|
+
| Low level MIDI interface to Linux | **TO DO** |
|
14
|
+
| Low level MIDI interface to JRuby | **TO DO** |
|
15
|
+
| Low level MIDI interface to Windows | **TO DO** |
|
16
|
+
|
17
|
+
This library is based on [Ari Russo's](http://github.com/arirusso) library [Nibbler](https://github.com/arirusso/nibbler).
|
18
|
+
|
19
|
+
## Install
|
20
|
+
|
21
|
+
`gem install midi-parser`
|
22
|
+
|
23
|
+
or using Bundler, add this to your Gemfile
|
24
|
+
|
25
|
+
`gem "midi-parser"`
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'midi-parser'
|
31
|
+
|
32
|
+
midi_parser = MIDIParser.new
|
33
|
+
```
|
34
|
+
|
35
|
+
Enter a message piece by piece:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
midi_parser.parse("90")
|
39
|
+
=> nil
|
40
|
+
|
41
|
+
midi_parser.parse("40")
|
42
|
+
=> nil
|
43
|
+
|
44
|
+
midi_parser.parse("40")
|
45
|
+
=> #<MIDIEvents::NoteOn:0x98c9818
|
46
|
+
@channel=0,
|
47
|
+
@data=[64, 100],
|
48
|
+
@name="C3",
|
49
|
+
@note=64,
|
50
|
+
@status=[9, 0],
|
51
|
+
@velocity=100,
|
52
|
+
@verbose_name="Note On: C3">
|
53
|
+
```
|
54
|
+
|
55
|
+
Enter a message all at once:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
midi_parser.parse("904040")
|
59
|
+
|
60
|
+
=> #<MIDIEvents::NoteOn:0x98c9818
|
61
|
+
@channel=0,
|
62
|
+
@data=[64, 100],
|
63
|
+
@name="C3",
|
64
|
+
@note=64,
|
65
|
+
@status=[9, 0],
|
66
|
+
@velocity=100,
|
67
|
+
@verbose_name="Note On: C3">
|
68
|
+
```
|
69
|
+
|
70
|
+
Use bytes:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
midi_parser.parse(0x90, 0x40, 0x40)
|
74
|
+
=> #<MIDIEvents::NoteOn:0x98c9818 ...>
|
75
|
+
```
|
76
|
+
|
77
|
+
You can use nibbles in string format:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
midi_parser.parse("9", "0", "4", "0", "4", "0")
|
81
|
+
=> #<MIDIEvents::NoteOn:0x98c9818 ...>
|
82
|
+
```
|
83
|
+
|
84
|
+
Interchange the different types:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
midi_parser.parse("9", "0", 0x40, 64)
|
88
|
+
=> #<MIDIEvents::NoteOn:0x98c9818 ...>
|
89
|
+
```
|
90
|
+
|
91
|
+
Use running status:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
midi_parser.parse(0x40, 64)
|
95
|
+
=> #<MIDIMEvents::NoteOn:0x98c9818 ...>
|
96
|
+
```
|
97
|
+
|
98
|
+
Add an incomplete message:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
midi_parser.parse("9")
|
102
|
+
midi_parser.parse("40")
|
103
|
+
```
|
104
|
+
|
105
|
+
See progress:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
midi_parser.buffer
|
109
|
+
=> ["9", "4", "0"]
|
110
|
+
|
111
|
+
midi_parser.buffer_s
|
112
|
+
=> "940"
|
113
|
+
```
|
114
|
+
|
115
|
+
MIDI Parser generates [midi-events](http://github.com/javier-sy/midi-events) objects.
|
116
|
+
|
117
|
+
## Documentation
|
118
|
+
|
119
|
+
* (**TO DO**) [rdoc](http://rubydoc.info/github/javier-sy/midi-parser)
|
120
|
+
|
121
|
+
## Differences between [MIDI Parser](https://github.com/javier-sy/midi-parser) and [Nibbler](https://github.com/arirusso/nibbler)
|
122
|
+
[MIDI Parser](https://github.com/javier-sy/midi-parser) is mostly a clone of [Nibbler](https://github.com/arirusso/nibbler) with some modifications:
|
123
|
+
* Removed logging attributes (messages, rejected, processed) to reduce parsing overhead
|
124
|
+
* Updated dependencies versions
|
125
|
+
* Source updated to Ruby 2.7 code conventions (method keyword parameters instead of options = {}, hash keys as 'key:' instead of ':key =>', etc.)
|
126
|
+
* Changed backend library midi-message to midi-events
|
127
|
+
* Removed backend library MIDIlib
|
128
|
+
* Renamed module to MIDIParser instead of Nibbler
|
129
|
+
* Renamed gem to midi-parser instead of nibbler
|
130
|
+
* 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
|
+
|
134
|
+
## Then, why does exist this library if it is mostly a clone of another library?
|
135
|
+
|
136
|
+
The author has been developing since 2016 a Ruby project called
|
137
|
+
[Musa DSL](https://github.com/javier-sy/musa-dsl) that needs a way
|
138
|
+
of representing MIDI Events and a way of communicating with
|
139
|
+
MIDI Instruments and MIDI Control Surfaces.
|
140
|
+
|
141
|
+
[Ari Russo](https://github.com/arirusso) has done a great job creating
|
142
|
+
several interdependent Ruby libraries that allow
|
143
|
+
MIDI Events representation ([MIDI Message](https://github.com/arirusso/midi-message)
|
144
|
+
and [Nibbler](https://github.com/arirusso/nibbler))
|
145
|
+
and communication with MIDI Instruments and MIDI Control Surfaces
|
146
|
+
([unimidi](https://github.com/arirusso/unimidi),
|
147
|
+
[ffi-coremidi](https://github.com/arirusso/ffi-coremidi) and others)
|
148
|
+
that, **with some modifications**, I've been using in MusaDSL.
|
149
|
+
|
150
|
+
After thinking about the best approach to publish MusaDSL
|
151
|
+
I've decided to publish my own renamed version of the modified dependencies because:
|
152
|
+
|
153
|
+
* Some differences on the approach of the modifications vs the original library doesn't allow to merge the modifications on the original libraries.
|
154
|
+
* Then the renaming of the libraries is needed to avoid confusing existent users of the original libraries.
|
155
|
+
* Due to some of the interdependencies of Ari Russo libraries,
|
156
|
+
the modification and renaming on some of the low level libraries (ffi-coremidi, etc.)
|
157
|
+
forces to modify and rename unimidi library.
|
158
|
+
* The original libraries have features
|
159
|
+
(very detailed logging and processing history information, not locking behaviour when waiting input midi messages)
|
160
|
+
that are not needed in MusaDSL and, in fact,
|
161
|
+
can degrade the performance on some use case scenarios in MusaDSL.
|
162
|
+
|
163
|
+
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.
|
164
|
+
|
165
|
+
| Function | Library | Based on Ari Russo's| Difference |
|
166
|
+
| --- | --- | --- | --- |
|
167
|
+
| MIDI Events representation | [MIDI Events](https://github.com/javier-sy/midi-events) | [MIDI Message](https://github.com/arirusso/midi-message) | removed parsing, small improvements |
|
168
|
+
| MIDI Data parsing | [MIDI Parser](https://github.com/javier-sy/midi-parser) | [Nibbler](https://github.com/arirusso/nibbler) | removed process history information, minor optimizations |
|
169
|
+
| 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)
|
170
|
+
| 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 process history information, locking behaviour when waiting midi events, improved midi devices name detection, minor optimizations |
|
171
|
+
| Low level MIDI interface to Linux | **TO DO** | | |
|
172
|
+
| Low level MIDI interface to JRuby | **TO DO** | | |
|
173
|
+
| Low level MIDI interface to Windows | **TO DO** | | |
|
174
|
+
|
175
|
+
## Author
|
176
|
+
|
177
|
+
* [Javier Sánchez Yeste](https://github.com/javier-sy)
|
178
|
+
|
179
|
+
## Acknowledgements
|
180
|
+
|
181
|
+
Thanks to [Ari Russo](http://github.com/arirusso) for his ruby library [Nibbler](https://github.com/arirusso/nibbler) licensed as Apache License 2.0.
|
182
|
+
|
183
|
+
## License
|
184
|
+
|
185
|
+
[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
|
186
|
+
|
187
|
+
[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/Rakefile
ADDED
data/examples/usage.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift(File.join('..', 'lib'))
|
3
|
+
|
4
|
+
#
|
5
|
+
# Walk through different ways to use Nibbler
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'midi-parser'
|
9
|
+
|
10
|
+
midi_parser = MIDIParser.new
|
11
|
+
|
12
|
+
pp 'Enter a message piece by piece'
|
13
|
+
|
14
|
+
pp midi_parser.parse('90')
|
15
|
+
|
16
|
+
pp midi_parser.parse('40')
|
17
|
+
|
18
|
+
pp midi_parser.parse('40')
|
19
|
+
|
20
|
+
pp 'Enter a message all at once'
|
21
|
+
|
22
|
+
pp midi_parser.parse('904040')
|
23
|
+
|
24
|
+
pp 'Use Bytes'
|
25
|
+
|
26
|
+
pp midi_parser.parse(0x90, 0x40, 0x40) # this should return a message
|
27
|
+
|
28
|
+
pp 'Use nibbles in string format'
|
29
|
+
|
30
|
+
pp midi_parser.parse('9', '0', 0x40, 0x40) # this should return a message
|
31
|
+
|
32
|
+
pp 'Interchange the different types'
|
33
|
+
|
34
|
+
pp midi_parser.parse('9', '0', 0x40, 64)
|
35
|
+
|
36
|
+
pp 'Use running status'
|
37
|
+
|
38
|
+
pp midi_parser.parse(0x40, 64)
|
39
|
+
|
40
|
+
pp 'Add an incomplete message'
|
41
|
+
|
42
|
+
pp midi_parser.parse('9')
|
43
|
+
pp midi_parser.parse('40')
|
44
|
+
|
45
|
+
pp 'See progress'
|
46
|
+
|
47
|
+
pp midi_parser.buffer # should give you an array of bits
|
48
|
+
|
49
|
+
pp midi_parser.buffer_s # should give you an array of bytestrs
|
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')
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module MIDIParser
|
2
|
+
# Accepts various types of input and returns an array of hex digit chars
|
3
|
+
#
|
4
|
+
# Ideally this would output Integer objects. However, given that Ruby numerics 0x0 and 0x00 result in the same
|
5
|
+
# object (0 Integer), this would limit the parser to only working with bytes instead of both nibbles and bytes.
|
6
|
+
#
|
7
|
+
# For example, if the input were "5" then the processor would return an ambiguous 0x5
|
8
|
+
#
|
9
|
+
module DataProcessor
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Accepts various types of input and returns an array of hex digit chars
|
13
|
+
# Invalid input is disregarded
|
14
|
+
#
|
15
|
+
# @param [*String, *Integer] args
|
16
|
+
# @return [Array<String>] An array of hex string nibbles eg "6", "a"
|
17
|
+
def process(*args)
|
18
|
+
args.map { |arg| convert(arg) }.flatten.compact.map(&:upcase)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Convert a single value to hex chars
|
24
|
+
# @param [Array<Integer>, Array<String>, Integer, String] value
|
25
|
+
# @return [Array<String>]
|
26
|
+
def convert(value)
|
27
|
+
case value
|
28
|
+
when Array then value.map { |arr| process(*arr) }.reduce(:+)
|
29
|
+
when String then TypeConversion.hex_str_to_hex_chars(filter_string(value))
|
30
|
+
when Integer then TypeConversion.numeric_byte_to_hex_chars(filter_numeric(value))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Limit the given number to bytes usable in MIDI ie values (0..240)
|
35
|
+
# returns nil if the byte is outside of that range
|
36
|
+
# @param [Integer] num
|
37
|
+
# @return [Integer, nil]
|
38
|
+
def filter_numeric(num)
|
39
|
+
num if (0x00..0xFF).include?(num)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Only return valid hex string characters
|
43
|
+
# @param [String] string
|
44
|
+
# @return [String]
|
45
|
+
def filter_string(string)
|
46
|
+
string.gsub(/[^0-9a-fA-F]/, '').upcase
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module MIDIParser
|
2
|
+
class MessageBuilder
|
3
|
+
CHANNEL_MESSAGE = [
|
4
|
+
{
|
5
|
+
status: 0x8,
|
6
|
+
class: MIDIEvents::NoteOff,
|
7
|
+
nibbles: 6
|
8
|
+
},
|
9
|
+
{
|
10
|
+
status: 0x9,
|
11
|
+
class: MIDIEvents::NoteOn,
|
12
|
+
nibbles: 6
|
13
|
+
},
|
14
|
+
{
|
15
|
+
status: 0xA,
|
16
|
+
class: MIDIEvents::PolyphonicAftertouch,
|
17
|
+
nibbles: 6
|
18
|
+
},
|
19
|
+
{
|
20
|
+
status: 0xB,
|
21
|
+
class: MIDIEvents::ControlChange,
|
22
|
+
nibbles: 6
|
23
|
+
},
|
24
|
+
{
|
25
|
+
status: 0xC,
|
26
|
+
class: MIDIEvents::ProgramChange,
|
27
|
+
nibbles: 4
|
28
|
+
},
|
29
|
+
{
|
30
|
+
status: 0xD,
|
31
|
+
class: MIDIEvents::ChannelAftertouch,
|
32
|
+
nibbles: 4
|
33
|
+
},
|
34
|
+
{
|
35
|
+
status: 0xE,
|
36
|
+
class: MIDIEvents::PitchBend,
|
37
|
+
nibbles: 6
|
38
|
+
}
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
SYSTEM_MESSAGE = [
|
42
|
+
{
|
43
|
+
status: 0x1..0x6,
|
44
|
+
class: MIDIEvents::SystemCommon,
|
45
|
+
nibbles: 6
|
46
|
+
},
|
47
|
+
{
|
48
|
+
status: 0x8..0xF,
|
49
|
+
class: MIDIEvents::SystemRealtime,
|
50
|
+
nibbles: 2
|
51
|
+
}
|
52
|
+
].freeze
|
53
|
+
|
54
|
+
attr_reader :num_nibbles, :name, :clazz
|
55
|
+
|
56
|
+
def self.build_system_exclusive(*message_data)
|
57
|
+
MIDIEvents::SystemExclusive.new(*message_data)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.for_system_message(status)
|
61
|
+
type = SYSTEM_MESSAGE.find { |type| type[:status].cover?(status) }
|
62
|
+
new(type[:nibbles], type[:class])
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.for_channel_message(status)
|
66
|
+
type = CHANNEL_MESSAGE.find { |type| type[:status] == status }
|
67
|
+
new(type[:nibbles], type[:class])
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(num_nibbles, clazz)
|
71
|
+
@num_nibbles = num_nibbles
|
72
|
+
@clazz = clazz
|
73
|
+
end
|
74
|
+
|
75
|
+
def build(*message_data)
|
76
|
+
@clazz.new(*message_data)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module MIDIParser
|
2
|
+
class Parser
|
3
|
+
attr_reader :buffer
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@running_status = RunningStatus.new
|
7
|
+
@buffer = []
|
8
|
+
end
|
9
|
+
|
10
|
+
# Process the given nibbles and add them to the buffer
|
11
|
+
# @param [Array<String, Integer>] nibbles
|
12
|
+
# @return [Array<MIDIEvent>]
|
13
|
+
def process(nibbles)
|
14
|
+
messages = []
|
15
|
+
pointer = 0
|
16
|
+
@buffer += nibbles
|
17
|
+
# Iterate through nibbles in the buffer until a status message is found
|
18
|
+
while pointer <= (@buffer.length - 1)
|
19
|
+
# fragment is the piece of the buffer to look at
|
20
|
+
fragment = get_fragment(pointer)
|
21
|
+
# See if there really is a message there
|
22
|
+
unless (processed = nibbles_to_message(fragment)).nil?
|
23
|
+
# if fragment contains a real message, reject the nibbles that precede it
|
24
|
+
@buffer = fragment.dup # fragment now has the remaining nibbles for next pass
|
25
|
+
fragment = nil # Reset fragment
|
26
|
+
pointer = 0 # Reset iterator
|
27
|
+
messages << processed[:message]
|
28
|
+
else
|
29
|
+
@running_status.cancel
|
30
|
+
pointer += 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
messages
|
34
|
+
end
|
35
|
+
|
36
|
+
# If possible, convert the given fragment to a MIDI message
|
37
|
+
# @param [Array<String>] fragment A fragment of data eg ["9", "0", "4", "0", "5", "0"]
|
38
|
+
# @return [Hash, nil]
|
39
|
+
def nibbles_to_message(fragment)
|
40
|
+
if fragment.length >= 2
|
41
|
+
# convert the part of the fragment to start with to a numeric
|
42
|
+
slice = fragment.slice(0..1).map(&:hex)
|
43
|
+
compute_message(slice, fragment)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Attempt to convert the given nibbles into a MIDI message
|
50
|
+
# @param [Array<Integer>] nibbles
|
51
|
+
# @return [Hash, nil]
|
52
|
+
def compute_message(nibbles, fragment)
|
53
|
+
case nibbles[0]
|
54
|
+
when 0x8..0xE then lookahead(fragment, MessageBuilder.for_channel_message(nibbles[0]))
|
55
|
+
when 0xF
|
56
|
+
case nibbles[1]
|
57
|
+
when 0x0 then lookahead_for_sysex(fragment)
|
58
|
+
else lookahead(fragment, MessageBuilder.for_system_message(nibbles[1]), recursive: true)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
lookahead_using_running_status(fragment) if @running_status.possible?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Attempt to convert the fragment to a MIDI message using the given fragment and cached running status
|
66
|
+
# @param [Array<String>] fragment A fragment of data eg ["4", "0", "5", "0"]
|
67
|
+
# @return [Hash, nil]
|
68
|
+
def lookahead_using_running_status(fragment)
|
69
|
+
lookahead(fragment, @running_status[:message_builder], offset: @running_status[:offset], status_nibble_2: @running_status[:status_nibble_2])
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get the data in the buffer for the given pointer
|
73
|
+
# @param [Integer] pointer
|
74
|
+
# @return [Array<String>]
|
75
|
+
def get_fragment(pointer)
|
76
|
+
@buffer[pointer, (@buffer.length - pointer)]
|
77
|
+
end
|
78
|
+
|
79
|
+
# If the given fragment has at least the given number of nibbles, use it to build a hash that can be used
|
80
|
+
# to build a MIDI message
|
81
|
+
#
|
82
|
+
# @param [Integer] num_nibbles
|
83
|
+
# @param [Array<String>] fragment
|
84
|
+
# @param [Hash] options
|
85
|
+
# @option options [String] :status_nibble_2
|
86
|
+
# @option options [Boolean] :recursive
|
87
|
+
# @return [Hash, nil]
|
88
|
+
def lookahead(fragment, message_builder, options = {})
|
89
|
+
offset = options.fetch(:offset, 0)
|
90
|
+
num_nibbles = message_builder.num_nibbles + offset
|
91
|
+
if fragment.size >= num_nibbles
|
92
|
+
# if so shift those nibbles off of the array and call block with them
|
93
|
+
nibbles = fragment.slice!(0, num_nibbles)
|
94
|
+
status_nibble_2 ||= options[:status_nibble_2] || nibbles[1]
|
95
|
+
|
96
|
+
# send the nibbles to the block as bytes
|
97
|
+
# return the evaluated block and the remaining nibbles
|
98
|
+
bytes = TypeConversion.hex_chars_to_numeric_bytes(nibbles)
|
99
|
+
bytes = bytes[1..-1] if options[:status_nibble_2].nil?
|
100
|
+
|
101
|
+
# record the fragment situation in case running status comes up next round
|
102
|
+
@running_status.set(offset - 2, message_builder, status_nibble_2)
|
103
|
+
|
104
|
+
message_args = [status_nibble_2.hex]
|
105
|
+
message_args += bytes if num_nibbles > 2
|
106
|
+
|
107
|
+
message = message_builder.build(*message_args)
|
108
|
+
{
|
109
|
+
message: message,
|
110
|
+
processed: nibbles
|
111
|
+
}
|
112
|
+
elsif num_nibbles > 0 && !!options[:recursive]
|
113
|
+
lookahead(fragment, message_builder, options.merge({ offset: offset - 2 }))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def lookahead_for_sysex(fragment)
|
118
|
+
@running_status.cancel
|
119
|
+
bytes = TypeConversion.hex_chars_to_numeric_bytes(fragment)
|
120
|
+
unless (index = bytes.index(0xF7)).nil?
|
121
|
+
message_data = bytes.slice!(0, index + 1)
|
122
|
+
message = MessageBuilder.build_system_exclusive(*message_data)
|
123
|
+
{
|
124
|
+
message: message,
|
125
|
+
processed: fragment.slice!(0, (index + 1) * 2)
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class RunningStatus
|
131
|
+
extend Forwardable
|
132
|
+
|
133
|
+
def_delegators :@state, :[]
|
134
|
+
|
135
|
+
def cancel
|
136
|
+
@state = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
# Is there an active cached running status?
|
140
|
+
# @return [Boolean]
|
141
|
+
def possible?
|
142
|
+
!@state.nil?
|
143
|
+
end
|
144
|
+
|
145
|
+
def set(offset, message_builder, status_nibble_2)
|
146
|
+
@state = {
|
147
|
+
message_builder: message_builder,
|
148
|
+
offset: offset,
|
149
|
+
status_nibble_2: status_nibble_2
|
150
|
+
}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module MIDIParser
|
2
|
+
# A parser session
|
3
|
+
#
|
4
|
+
# Holds on to data that is not relevant to the parser between calls. For instance,
|
5
|
+
# past messages, rejected bytes
|
6
|
+
#
|
7
|
+
class Session
|
8
|
+
def initialize
|
9
|
+
@parser = Parser.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# The buffer
|
13
|
+
# @return [Array<Object>]
|
14
|
+
def buffer
|
15
|
+
@parser.buffer
|
16
|
+
end
|
17
|
+
|
18
|
+
# The buffer as a single hex string
|
19
|
+
# @return [String]
|
20
|
+
def buffer_s
|
21
|
+
@parser.buffer.join
|
22
|
+
end
|
23
|
+
alias buffer_hex buffer_s
|
24
|
+
|
25
|
+
# Clear the parser buffer
|
26
|
+
def clear_buffer
|
27
|
+
@parser.buffer.clear
|
28
|
+
end
|
29
|
+
|
30
|
+
# Parse some input
|
31
|
+
# @param [*Object] args
|
32
|
+
# @return [Array<MIDIEvent>]
|
33
|
+
def parse(*args)
|
34
|
+
queue = DataProcessor.process(args)
|
35
|
+
@parser.process(queue)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module MIDIParser
|
2
|
+
# A helper for converting between different types of nibbles and bytes
|
3
|
+
module TypeConversion
|
4
|
+
extend self
|
5
|
+
|
6
|
+
# Converts an array of hex nibble strings to numeric bytes
|
7
|
+
# eg ["9", "0", "5", "0", "4", "0"] => [0x90, 0x50, 0x40]
|
8
|
+
# @param [Array<String>] nibbles
|
9
|
+
# @return [Array<Integer>]
|
10
|
+
def hex_chars_to_numeric_bytes(nibbles)
|
11
|
+
nibbles = nibbles.dup
|
12
|
+
# get rid of last nibble if there's an odd number
|
13
|
+
# it will be processed later anyway
|
14
|
+
nibbles.slice!(nibbles.length - 2, 1) if nibbles.length.odd?
|
15
|
+
bytes = []
|
16
|
+
until (nibs = nibbles.slice!(0, 2)).empty?
|
17
|
+
byte = (nibs[0].hex << 4) + nibs[1].hex
|
18
|
+
bytes << byte
|
19
|
+
end
|
20
|
+
bytes
|
21
|
+
end
|
22
|
+
|
23
|
+
# Converts a string of hex digits to string nibbles
|
24
|
+
# eg "905040" => ["9", "0", "5", "0", "4", "0"]
|
25
|
+
# @param [String] string
|
26
|
+
# @return [Array<String>]
|
27
|
+
def hex_str_to_hex_chars(string)
|
28
|
+
string.split(//)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Converts a string of hex digits to numeric nibbles
|
32
|
+
# eg "905040" => [0x9, 0x0, 0x5, 0x0, 0x4, 0x0]
|
33
|
+
# @param [String] string
|
34
|
+
# @return [Array<String>]
|
35
|
+
def hex_str_to_numeric_nibbles(string)
|
36
|
+
bytes = hex_str_to_numeric_bytes(string)
|
37
|
+
numeric_bytes_to_numeric_nibbles(bytes)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Converts a string of hex digits to numeric bytes
|
41
|
+
# eg "905040" => [0x90, 0x50, 0x40]
|
42
|
+
# @param [String] string
|
43
|
+
# @return [Array<String>]
|
44
|
+
def hex_str_to_numeric_bytes(string)
|
45
|
+
chars = hex_str_to_hex_chars(string)
|
46
|
+
hex_chars_to_numeric_bytes(chars)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Converts an array bytes to an array of nibbles
|
50
|
+
# eg [0x90, 0x50, 0x40] => [0x9, 0x0, 0x5, 0x0, 0x4, 0x0]
|
51
|
+
# @param [Array<Integer>] bytes
|
52
|
+
# @return [Array<String>]
|
53
|
+
def numeric_bytes_to_numeric_nibbles(bytes)
|
54
|
+
bytes.map { |byte| numeric_byte_to_numeric_nibbles(byte) }.flatten
|
55
|
+
end
|
56
|
+
|
57
|
+
# Converts a numeric byte to an array of hex nibble strings eg 0x90 => ["9", "0"]
|
58
|
+
# @param [Integer] num
|
59
|
+
# @return [Array<String>]
|
60
|
+
def numeric_byte_to_hex_chars(num)
|
61
|
+
nibbles = numeric_byte_to_numeric_nibbles(num)
|
62
|
+
nibbles.map { |n| n.to_s(16) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Converts a numeric byte to an array of numeric nibbles eg 0x90 => [0x9, 0x0]
|
66
|
+
# @param [Integer] num
|
67
|
+
# @return [Array<String>]
|
68
|
+
def numeric_byte_to_numeric_nibbles(num)
|
69
|
+
[((num & 0xF0) >> 4), (num & 0x0F)]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|