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.
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