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/output.rb
CHANGED
@@ -1,84 +1,96 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
module AlsaRawMIDI
|
4
|
-
|
5
|
-
#
|
2
|
+
|
6
3
|
# Output device class
|
7
|
-
#
|
8
4
|
class Output
|
9
|
-
|
5
|
+
|
10
6
|
include Device
|
11
|
-
|
12
|
-
#
|
7
|
+
|
8
|
+
# Close this output
|
9
|
+
# @return [Boolean]
|
13
10
|
def close
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
if @enabled
|
12
|
+
API::Device.close(@resource)
|
13
|
+
@enabled = false
|
14
|
+
true
|
15
|
+
else
|
16
|
+
false
|
17
|
+
end
|
17
18
|
end
|
18
|
-
|
19
|
-
#
|
19
|
+
|
20
|
+
# Output a MIDI message in hex string format
|
21
|
+
# @param [String] data
|
22
|
+
# @return [Boolean]
|
20
23
|
def puts_s(data)
|
21
24
|
data = data.dup
|
22
|
-
|
23
|
-
until (str = data.slice!(0,2))
|
24
|
-
|
25
|
+
output = []
|
26
|
+
until (str = data.slice!(0,2)) == ""
|
27
|
+
output << str.hex
|
25
28
|
end
|
26
29
|
puts_bytes(*output)
|
30
|
+
true
|
27
31
|
end
|
28
32
|
alias_method :puts_bytestr, :puts_s
|
29
33
|
alias_method :puts_hex, :puts_s
|
30
34
|
|
31
|
-
#
|
35
|
+
# Output a MIDI message in numeric byte format
|
36
|
+
# @param [*Fixnum] data
|
37
|
+
# @return [Boolean]
|
32
38
|
def puts_bytes(*data)
|
33
|
-
|
34
|
-
|
35
|
-
bytes = FFI::MemoryPointer.new(data.size).put_bytes(0, data.pack(format))
|
36
|
-
|
37
|
-
Map.snd_rawmidi_write(@handle, bytes.to_i, data.size)
|
38
|
-
Map.snd_rawmidi_drain(@handle)
|
39
|
-
|
39
|
+
API::Output.puts(@resource, data)
|
40
|
+
true
|
40
41
|
end
|
41
|
-
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
|
43
|
+
# Output the given MIDI message
|
44
|
+
# @param [*Fixnum, *String] args
|
45
|
+
# @return [Boolean]
|
46
|
+
def puts(*args)
|
47
|
+
case args.first
|
48
|
+
when Array then args.each { |arg| puts(*arg) }
|
49
|
+
when Numeric then puts_bytes(*args)
|
50
|
+
when String then puts_bytestr(*args)
|
48
51
|
end
|
49
52
|
end
|
50
53
|
alias_method :write, :puts
|
51
|
-
|
52
|
-
#
|
54
|
+
|
55
|
+
# Enable this device; yields
|
56
|
+
# @param [Hash] options
|
57
|
+
# @param [Proc] block
|
58
|
+
# @return [Output]
|
53
59
|
def enable(options = {}, &block)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
else
|
65
|
-
self
|
60
|
+
unless @enabled
|
61
|
+
@resource = API::Output.open(@system_id)
|
62
|
+
@enabled = true
|
63
|
+
end
|
64
|
+
if block_given?
|
65
|
+
begin
|
66
|
+
yield(self)
|
67
|
+
ensure
|
68
|
+
close
|
69
|
+
end
|
66
70
|
end
|
71
|
+
self
|
67
72
|
end
|
68
73
|
alias_method :open, :enable
|
69
74
|
alias_method :start, :enable
|
70
|
-
|
75
|
+
|
76
|
+
# The first available output
|
77
|
+
# @return [Output]
|
71
78
|
def self.first
|
72
|
-
Device.first(:output)
|
79
|
+
Device.first(:output)
|
73
80
|
end
|
74
81
|
|
82
|
+
# The last available output
|
83
|
+
# @return [Output]
|
75
84
|
def self.last
|
76
|
-
Device.last(:output)
|
85
|
+
Device.last(:output)
|
77
86
|
end
|
78
|
-
|
87
|
+
|
88
|
+
# All outputs
|
89
|
+
# @return [Array<Output>]
|
79
90
|
def self.all
|
80
91
|
Device.all_by_type[:output]
|
81
92
|
end
|
93
|
+
|
82
94
|
end
|
83
|
-
|
84
|
-
end
|
95
|
+
|
96
|
+
end
|
@@ -1,78 +1,61 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
module AlsaRawMIDI
|
4
|
-
|
5
|
-
class Soundcard
|
6
|
-
|
7
|
-
attr_reader :subdevices
|
8
|
-
|
9
|
-
def initialize(card_num)
|
10
|
-
@subdevices = { :input => [], :output => [] }
|
11
|
-
name = "hw:#{card_num}"
|
12
|
-
handle_ptr = FFI::MemoryPointer.new(FFI.type_size(:int))
|
13
|
-
#snd_ctl = Map::SndCtl.new
|
14
|
-
#snd_ctl_pointer = FFI::MemoryPointer.new(:pointer).write_pointer(snd_ctl.pointer)
|
15
|
-
Map.snd_ctl_open(handle_ptr, name, 0)
|
16
|
-
handle = handle_ptr.read_int
|
17
|
-
32.times do |i|
|
18
|
-
devnum = FFI::MemoryPointer.new(:int).write_int(i)
|
19
|
-
if (err = Map.snd_ctl_rawmidi_next_device(handle, devnum)) < 0
|
20
|
-
break # fix this
|
21
|
-
end
|
22
|
-
@subdevices.keys.each do |type|
|
23
|
-
populate_subdevices(type, handle, card_num, i)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
2
|
|
28
|
-
|
29
|
-
Soundcard.new(card_num) if Map.snd_card_load(card_num).eql?(1)
|
30
|
-
end
|
3
|
+
class Soundcard
|
31
4
|
|
32
|
-
|
5
|
+
attr_reader :id, :subdevices
|
33
6
|
|
34
|
-
|
35
|
-
|
36
|
-
|
7
|
+
# @param [Fixnum] id
|
8
|
+
def initialize(id)
|
9
|
+
@subdevices = {
|
10
|
+
:input => [],
|
11
|
+
:output => []
|
12
|
+
}
|
13
|
+
@id = id
|
14
|
+
populate_subdevices
|
37
15
|
end
|
38
16
|
|
39
|
-
|
40
|
-
|
41
|
-
|
17
|
+
# Find a soundcard by its ID
|
18
|
+
# @param [Fixnum] id
|
19
|
+
# @return [Soundcard]
|
20
|
+
def self.find(id)
|
21
|
+
@soundcards ||= {}
|
22
|
+
if API::Soundcard.exists?(id)
|
23
|
+
@soundcards[id] ||= Soundcard.new(id)
|
24
|
+
end
|
42
25
|
end
|
43
26
|
|
44
|
-
|
45
|
-
ctype, dtype = *case type
|
46
|
-
when :input then [Map::Constants[:SND_RAWMIDI_STREAM_INPUT], Input]
|
47
|
-
when :output then [Map::Constants[:SND_RAWMIDI_STREAM_OUTPUT], Output]
|
48
|
-
end
|
49
|
-
info = Map::SndRawMIDIInfo.new
|
50
|
-
Map.snd_rawmidi_info_set_device(info.pointer, device_num)
|
51
|
-
Map.snd_rawmidi_info_set_stream(info.pointer, ctype)
|
52
|
-
i = 0
|
53
|
-
subdev_count = 1
|
54
|
-
available = []
|
55
|
-
while (i <= subdev_count)
|
56
|
-
Map.snd_rawmidi_info_set_subdevice(info.pointer, i)
|
57
|
-
# fix this
|
58
|
-
if (err = Map.snd_ctl_rawmidi_info(ctl_ptr, info.pointer)) < 0
|
59
|
-
break
|
60
|
-
end
|
27
|
+
private
|
61
28
|
|
62
|
-
|
63
|
-
|
64
|
-
|
29
|
+
# @return [Hash]
|
30
|
+
def populate_subdevices
|
31
|
+
device_ids = API::Soundcard.get_device_ids(@id)
|
32
|
+
device_ids.each do |device_id|
|
33
|
+
@subdevices.keys.each do |direction|
|
34
|
+
devices = API::Soundcard.get_subdevices(direction, @id, device_id) do |device_hash|
|
35
|
+
new_device(direction, device_hash)
|
36
|
+
end
|
37
|
+
@subdevices[direction] += devices
|
65
38
|
end
|
39
|
+
end
|
40
|
+
@subdevices
|
41
|
+
end
|
66
42
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
43
|
+
# Instantiate a new device object
|
44
|
+
# @param [Hash]
|
45
|
+
# @return [Input, Output]
|
46
|
+
def new_device(direction, device_hash)
|
47
|
+
device_class = case direction
|
48
|
+
when :input then Input
|
49
|
+
when :output then Output
|
72
50
|
end
|
73
|
-
|
51
|
+
device_properties = {
|
52
|
+
:system_id => device_hash[:id],
|
53
|
+
:name => device_hash[:name],
|
54
|
+
:subname => device_hash[:subname]
|
55
|
+
}
|
56
|
+
device_class.new(device_properties)
|
74
57
|
end
|
75
58
|
|
76
59
|
end
|
77
60
|
|
78
|
-
end
|
61
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
2
|
+
$LOAD_PATH.unshift dir + "/../lib"
|
3
|
+
|
4
|
+
require "test/unit"
|
5
|
+
require "mocha/test_unit"
|
6
|
+
require "shoulda-context"
|
7
|
+
require "alsa-rawmidi"
|
8
|
+
|
9
|
+
module TestHelper
|
10
|
+
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def bytestrs_to_ints(arr)
|
14
|
+
data = arr.map { |m| m[:data] }.join
|
15
|
+
output = []
|
16
|
+
until (bytestr = data.slice!(0,2)).eql?("")
|
17
|
+
output << bytestr.hex
|
18
|
+
end
|
19
|
+
output
|
20
|
+
end
|
21
|
+
|
22
|
+
# some MIDI messages
|
23
|
+
def numeric_messages
|
24
|
+
[
|
25
|
+
[0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7], # SysEx
|
26
|
+
[0x90, 100, 100], # note on
|
27
|
+
[0x90, 43, 100], # note on
|
28
|
+
[0x90, 76, 100], # note on
|
29
|
+
[0x90, 60, 100], # note on
|
30
|
+
[0x80, 100, 100] # note off
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
# some MIDI messages
|
35
|
+
def string_messages
|
36
|
+
[
|
37
|
+
"F04110421240007F0041F7", # SysEx
|
38
|
+
"906440", # note on
|
39
|
+
"804340" # note off
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
def input
|
44
|
+
AlsaRawMIDI::Input.first
|
45
|
+
end
|
46
|
+
|
47
|
+
def output
|
48
|
+
AlsaRawMIDI::Output.first
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class AlsaRawMIDI::InputBufferTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "AlsaRawMIDI" do
|
6
|
+
|
7
|
+
setup do
|
8
|
+
sleep(1)
|
9
|
+
@input = TestHelper.input.open
|
10
|
+
@output = TestHelper.output.open
|
11
|
+
@input.buffer.clear
|
12
|
+
@pointer = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
context "Source#buffer" do
|
16
|
+
|
17
|
+
setup do
|
18
|
+
@messages = TestHelper.numeric_messages
|
19
|
+
@messages_arr = @messages.inject(&:+).flatten
|
20
|
+
@received_arr = []
|
21
|
+
end
|
22
|
+
|
23
|
+
teardown do
|
24
|
+
@input.close
|
25
|
+
@output.close
|
26
|
+
end
|
27
|
+
|
28
|
+
should "have the correct messages in the buffer" do
|
29
|
+
bytes = []
|
30
|
+
@messages.each do |message|
|
31
|
+
p "sending: #{message}"
|
32
|
+
@output.puts(message)
|
33
|
+
bytes += message
|
34
|
+
|
35
|
+
sleep(1)
|
36
|
+
|
37
|
+
buffer = @input.buffer.map { |m| m[:data] }.flatten
|
38
|
+
p "received: #{buffer.to_s}"
|
39
|
+
assert_equal(bytes, buffer)
|
40
|
+
end
|
41
|
+
assert_equal(bytes.length, @input.buffer.map { |m| m[:data] }.flatten.length)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/test/io_test.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class AlsaRawMIDI::IoTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# ** this test assumes that TestOutput is connected to TestInput
|
6
|
+
context "AlsaRawMIDI" do
|
7
|
+
|
8
|
+
setup do
|
9
|
+
sleep(1)
|
10
|
+
@input = TestHelper.input.open
|
11
|
+
@output = TestHelper.output.open
|
12
|
+
@input.buffer.clear
|
13
|
+
@pointer = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
context "full IO" do
|
17
|
+
|
18
|
+
context "using Arrays" do
|
19
|
+
|
20
|
+
setup do
|
21
|
+
@messages = TestHelper.numeric_messages
|
22
|
+
@messages_arr = @messages.inject(&:+).flatten
|
23
|
+
@received_arr = []
|
24
|
+
end
|
25
|
+
|
26
|
+
teardown do
|
27
|
+
@input.close
|
28
|
+
@output.close
|
29
|
+
end
|
30
|
+
|
31
|
+
should "do IO" do
|
32
|
+
@messages.each do |message|
|
33
|
+
|
34
|
+
p "sending: #{message}"
|
35
|
+
|
36
|
+
@output.puts(message)
|
37
|
+
sleep(1)
|
38
|
+
received = @input.gets.map { |m| m[:data] }.flatten
|
39
|
+
|
40
|
+
p "received: #{received}"
|
41
|
+
|
42
|
+
assert_equal(@messages_arr.slice(@pointer, received.length), received)
|
43
|
+
@pointer += received.length
|
44
|
+
@received_arr += received
|
45
|
+
end
|
46
|
+
assert_equal(@messages_arr.length, @received_arr.length)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "using byte Strings" do
|
51
|
+
|
52
|
+
setup do
|
53
|
+
@messages = TestHelper.string_messages
|
54
|
+
@messages_str = @messages.join
|
55
|
+
@received_str = ""
|
56
|
+
end
|
57
|
+
|
58
|
+
should "do IO" do
|
59
|
+
@messages.each do |message|
|
60
|
+
|
61
|
+
p "sending: #{message}"
|
62
|
+
|
63
|
+
@output.puts(message)
|
64
|
+
sleep(1)
|
65
|
+
received = @input.gets_bytestr.map { |m| m[:data] }.flatten.join
|
66
|
+
p "received: #{received}"
|
67
|
+
|
68
|
+
assert_equal(@messages_str.slice(@pointer, received.length), received)
|
69
|
+
@pointer += received.length
|
70
|
+
@received_str += received
|
71
|
+
end
|
72
|
+
assert_equal(@messages_str, @received_str)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|