alsa-rawmidi 0.2.14 → 0.3.1
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/LICENSE +2 -2
- data/README.md +48 -0
- data/lib/alsa-rawmidi.rb +20 -12
- data/lib/alsa-rawmidi/api.rb +435 -0
- data/lib/alsa-rawmidi/device.rb +85 -56
- data/lib/alsa-rawmidi/input.rb +199 -168
- data/lib/alsa-rawmidi/output.rb +63 -51
- data/lib/alsa-rawmidi/soundcard.rb +45 -62
- data/test/helper.rb +51 -0
- data/test/input_buffer_test.rb +46 -0
- data/test/io_test.rb +80 -0
- metadata +42 -47
- data/README.rdoc +0 -45
- data/lib/alsa-rawmidi/map.rb +0 -242
data/lib/alsa-rawmidi/device.rb
CHANGED
@@ -1,79 +1,108 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
module AlsaRawMIDI
|
4
2
|
|
5
|
-
#
|
6
|
-
# Module containing methods used by both input and output devices when using the
|
7
|
-
# ALSA driver interface
|
8
|
-
#
|
3
|
+
# Functionality common to both inputs and outputs
|
9
4
|
module Device
|
10
5
|
|
11
|
-
|
12
|
-
attr_reader :enabled,
|
13
|
-
# the alsa id of the device
|
14
|
-
:system_id,
|
15
|
-
# a unique numerical id for the device
|
16
|
-
:id,
|
17
|
-
:name,
|
18
|
-
:subname,
|
19
|
-
# :input or :output
|
20
|
-
:type
|
6
|
+
module ClassMethods
|
21
7
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
@system_id = options[:system_id]
|
29
|
-
|
30
|
-
# cache the type name so that inspecting the class isn't necessary each time
|
31
|
-
@type = self.class.name.split('::').last.downcase.to_sym
|
8
|
+
# Select the first device of the given direction
|
9
|
+
# @param [Symbol] direction
|
10
|
+
# @return [Input, Output]
|
11
|
+
def first(direction)
|
12
|
+
all_by_type[direction].first
|
13
|
+
end
|
32
14
|
|
33
|
-
|
34
|
-
|
15
|
+
# Select the last device of the given direction
|
16
|
+
# @param [Symbol] direction
|
17
|
+
# @return [Input, Output]
|
18
|
+
def last(direction)
|
19
|
+
all_by_type[direction].last
|
20
|
+
end
|
35
21
|
|
36
|
-
|
37
|
-
|
38
|
-
all_by_type
|
39
|
-
|
22
|
+
# A hash of devices, partitioned by direction
|
23
|
+
# @return [Hash]
|
24
|
+
def all_by_type
|
25
|
+
@devices ||= get_devices
|
26
|
+
end
|
40
27
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
28
|
+
# All devices
|
29
|
+
# @return [Array<Input, Output>]
|
30
|
+
def all
|
31
|
+
all_by_type.values.flatten
|
32
|
+
end
|
45
33
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
34
|
+
private
|
35
|
+
|
36
|
+
# Get all available devices from the system
|
37
|
+
# @return [Hash]
|
38
|
+
def get_devices
|
39
|
+
available_devices = {
|
40
|
+
:input => [],
|
41
|
+
:output => []
|
42
|
+
}
|
43
|
+
device_count = 0
|
44
|
+
32.times do |i|
|
45
|
+
card = Soundcard.find(i)
|
46
|
+
unless card.nil?
|
47
|
+
available_devices.keys.each do |direction|
|
48
|
+
devices = card.subdevices[direction]
|
49
|
+
devices.each do |dev|
|
50
|
+
dev.send(:id=, device_count)
|
51
|
+
device_count += 1
|
52
|
+
end
|
53
|
+
available_devices[direction] += devices
|
58
54
|
end
|
59
|
-
available_devices[type] += devices
|
60
55
|
end
|
61
56
|
end
|
57
|
+
available_devices
|
62
58
|
end
|
63
|
-
|
59
|
+
|
64
60
|
end
|
65
61
|
|
66
|
-
|
67
|
-
|
68
|
-
|
62
|
+
extend ClassMethods
|
63
|
+
|
64
|
+
attr_reader :enabled, # has the device been initialized?
|
65
|
+
:system_id, # the alsa id of the device
|
66
|
+
:id, # a local uuid for the device
|
67
|
+
:name,
|
68
|
+
:subname,
|
69
|
+
:type # :input or :output
|
70
|
+
|
71
|
+
alias_method :enabled?, :enabled
|
72
|
+
|
73
|
+
def self.included(base)
|
74
|
+
base.send(:extend, ClassMethods)
|
69
75
|
end
|
70
|
-
|
76
|
+
|
77
|
+
# @param [Hash] options
|
78
|
+
# @option options [Fixnum] :id
|
79
|
+
# @option options [String] :name
|
80
|
+
# @option options [String] :subname
|
81
|
+
# @option options [String] :system_id
|
82
|
+
def initialize(options = {})
|
83
|
+
@id = options[:id]
|
84
|
+
@name = options[:name]
|
85
|
+
@subname = options[:subname]
|
86
|
+
@system_id = options[:system_id]
|
87
|
+
@type = get_type
|
88
|
+
@enabled = false
|
89
|
+
end
|
90
|
+
|
71
91
|
private
|
72
|
-
|
92
|
+
|
93
|
+
# Assign an id
|
94
|
+
# @param [Fixnum] id
|
95
|
+
# @return [Fixnum]
|
73
96
|
def id=(id)
|
74
97
|
@id = id
|
75
98
|
end
|
76
99
|
|
100
|
+
# Get the device type
|
101
|
+
# @return [Symbol]
|
102
|
+
def get_type
|
103
|
+
self.class.name.split('::').last.downcase.to_sym
|
104
|
+
end
|
105
|
+
|
77
106
|
end
|
78
107
|
|
79
|
-
end
|
108
|
+
end
|
data/lib/alsa-rawmidi/input.rb
CHANGED
@@ -1,168 +1,199 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
#
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
def self.
|
86
|
-
Device.
|
87
|
-
end
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
def
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
#
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
#
|
141
|
-
def
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
end
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
1
|
+
module AlsaRawMIDI
|
2
|
+
|
3
|
+
# Input device class
|
4
|
+
class Input
|
5
|
+
|
6
|
+
include Device
|
7
|
+
|
8
|
+
attr_reader :buffer
|
9
|
+
|
10
|
+
#
|
11
|
+
# An array of MIDI event hashes as such:
|
12
|
+
# [
|
13
|
+
# { :data => [144, 60, 100], :timestamp => 1024 },
|
14
|
+
# { :data => [128, 60, 100], :timestamp => 1100 },
|
15
|
+
# { :data => [144, 40, 120], :timestamp => 1200 }
|
16
|
+
# ]
|
17
|
+
#
|
18
|
+
# The data is an array of numeric bytes
|
19
|
+
# The timestamp is the number of millis since this input was enabled
|
20
|
+
#
|
21
|
+
# @return [Array<Hash>]
|
22
|
+
def gets
|
23
|
+
loop until enqueued_messages?
|
24
|
+
msgs = enqueued_messages
|
25
|
+
@pointer = @buffer.length
|
26
|
+
msgs
|
27
|
+
end
|
28
|
+
alias_method :read, :gets
|
29
|
+
|
30
|
+
# Like Input#gets but returns message data as string of hex digits as such:
|
31
|
+
# [
|
32
|
+
# { :data => "904060", :timestamp => 904 },
|
33
|
+
# { :data => "804060", :timestamp => 1150 },
|
34
|
+
# { :data => "90447F", :timestamp => 1300 }
|
35
|
+
# ]
|
36
|
+
#
|
37
|
+
# @return [Array<Hash>]
|
38
|
+
def gets_s
|
39
|
+
msgs = gets
|
40
|
+
msgs.each { |m| m[:data] = numeric_bytes_to_hex_string(m[:data]) }
|
41
|
+
msgs
|
42
|
+
end
|
43
|
+
alias_method :gets_bytestr, :gets_s
|
44
|
+
alias_method :gets_hex, :gets_s
|
45
|
+
|
46
|
+
# Enable this the input for use; yields
|
47
|
+
# @param [Hash] options
|
48
|
+
# @param [Proc] block
|
49
|
+
# @return [Input] self
|
50
|
+
def enable(options = {}, &block)
|
51
|
+
unless @enabled
|
52
|
+
@start_time = Time.now.to_f
|
53
|
+
@resource = API::Input.open(@system_id)
|
54
|
+
@enabled = true
|
55
|
+
initialize_buffer
|
56
|
+
spawn_listener
|
57
|
+
end
|
58
|
+
if block_given?
|
59
|
+
begin
|
60
|
+
yield(self)
|
61
|
+
ensure
|
62
|
+
close
|
63
|
+
end
|
64
|
+
end
|
65
|
+
self
|
66
|
+
end
|
67
|
+
alias_method :open, :enable
|
68
|
+
alias_method :start, :enable
|
69
|
+
|
70
|
+
# Close this input
|
71
|
+
# @return [Boolean]
|
72
|
+
def close
|
73
|
+
if @enabled
|
74
|
+
Thread.kill(@listener)
|
75
|
+
API::Device.close(@resource)
|
76
|
+
@enabled = false
|
77
|
+
true
|
78
|
+
else
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# The first input available
|
84
|
+
# @return [Input]
|
85
|
+
def self.first
|
86
|
+
Device.first(:input)
|
87
|
+
end
|
88
|
+
|
89
|
+
# The last input available
|
90
|
+
# @return [Input]
|
91
|
+
def self.last
|
92
|
+
Device.last(:input)
|
93
|
+
end
|
94
|
+
|
95
|
+
# All available inputs
|
96
|
+
# @return [Array<Input>]
|
97
|
+
def self.all
|
98
|
+
Device.all_by_type[:input]
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Initialize the input buffer
|
104
|
+
# @return [Array]
|
105
|
+
def initialize_buffer
|
106
|
+
@pointer = 0
|
107
|
+
@buffer = []
|
108
|
+
def @buffer.clear
|
109
|
+
super
|
110
|
+
@pointer = 0
|
111
|
+
end
|
112
|
+
@buffer
|
113
|
+
end
|
114
|
+
|
115
|
+
# A timestamp for the current time
|
116
|
+
# @return [Float]
|
117
|
+
def now
|
118
|
+
time = Time.now.to_f - @start_time
|
119
|
+
time * 1000
|
120
|
+
end
|
121
|
+
|
122
|
+
# A message paired with timestamp
|
123
|
+
# @param [String] hexstring
|
124
|
+
# @param [Float] timestamp
|
125
|
+
# @return [Hash]
|
126
|
+
def get_message_formatted(hexstring, timestamp)
|
127
|
+
{
|
128
|
+
:data => hex_string_to_numeric_bytes(hexstring),
|
129
|
+
:timestamp => timestamp
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
# The messages enqueued in the buffer
|
134
|
+
# @return [Array<Hash>]
|
135
|
+
def enqueued_messages
|
136
|
+
@buffer.slice(@pointer, @buffer.length - @pointer)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Are there messages enqueued?
|
140
|
+
# @return [Boolean]
|
141
|
+
def enqueued_messages?
|
142
|
+
@pointer < @buffer.length
|
143
|
+
end
|
144
|
+
|
145
|
+
# Launch a background thread that collects messages
|
146
|
+
# and holds them for the next call to gets*
|
147
|
+
# @return [Thread]
|
148
|
+
def spawn_listener
|
149
|
+
interval = 1.0/1000
|
150
|
+
@listener = Thread.new do
|
151
|
+
begin
|
152
|
+
loop do
|
153
|
+
while (messages = API::Input.poll(@resource)).nil?
|
154
|
+
sleep(interval)
|
155
|
+
end
|
156
|
+
populate_buffer(messages) unless messages.nil?
|
157
|
+
end
|
158
|
+
rescue Exception => exception
|
159
|
+
Thread.main.raise(exception)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
@listener.abort_on_exception = true
|
163
|
+
@listener
|
164
|
+
end
|
165
|
+
|
166
|
+
# Collect messages from the system buffer
|
167
|
+
# @return [Array<String>, nil]
|
168
|
+
def populate_buffer(messages)
|
169
|
+
@buffer << get_message_formatted(messages, now) unless messages.nil?
|
170
|
+
end
|
171
|
+
|
172
|
+
# Convert a hex string to an array of numeric bytes eg "904040" -> [0x90, 0x40, 0x40]
|
173
|
+
# @param [String] string
|
174
|
+
# @return [Array<Fixnum>]
|
175
|
+
def hex_string_to_numeric_bytes(string)
|
176
|
+
string = string.dup
|
177
|
+
bytes = []
|
178
|
+
until string.length.zero?
|
179
|
+
string_byte = string.slice!(0, 2)
|
180
|
+
bytes << string_byte.hex
|
181
|
+
end
|
182
|
+
bytes
|
183
|
+
end
|
184
|
+
|
185
|
+
# Convert an array of numeric bytes to a hex string eg [0x90, 0x40, 0x40] -> "904040"
|
186
|
+
# @param [Array<Fixnum>] bytes
|
187
|
+
# @return [String]
|
188
|
+
def numeric_bytes_to_hex_string(bytes)
|
189
|
+
string_bytes = bytes.map do |byte|
|
190
|
+
string_byte = byte.to_s(16).upcase
|
191
|
+
string_byte = "0#{string_byte}" if byte < 16
|
192
|
+
string_byte
|
193
|
+
end
|
194
|
+
string_bytes.join
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|