midi-parser 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,10 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task default: [:test]
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