midi-parser 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|