midi-jruby 0.0.12 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +2 -2
- data/README.md +45 -0
- data/lib/midi-jruby/api.rb +172 -0
- data/lib/midi-jruby/device.rb +41 -44
- data/lib/midi-jruby/input.rb +91 -95
- data/lib/midi-jruby/output.rb +49 -33
- data/lib/midi-jruby.rb +18 -10
- data/test/helper.rb +60 -0
- data/test/input_buffer_test.rb +46 -0
- data/test/io_test.rb +80 -0
- metadata +41 -49
- data/README.rdoc +0 -52
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3556d36dc4530a743c39ee52843ac49c6106d7d1
|
4
|
+
data.tar.gz: 1f9d70df4153c5a152e34260da63f408cabb71d2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c3adf3f2a801d5c7a56db5f64260d1868ca59ce10da867b4d27f970dbc3cfb891dae40905728afdcb16547491b4d5365f4e25e5dc19e5212cefbedc91b572122
|
7
|
+
data.tar.gz: ab5f514ffd17459df997e5be3a84b58696193c8803b4e89d28a99ef519179e15c0b1442b5b8cd57aa99c0bdc3d2e62920d62397fe67a920495fe36cd1ae1cbed
|
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright
|
1
|
+
Copyright 2011-2014 Ari Russo
|
2
2
|
|
3
3
|
Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
you may not use this file except in compliance with the License.
|
@@ -10,4 +10,4 @@ Unless required by applicable law or agreed to in writing, software
|
|
10
10
|
distributed under the License is distributed on an "AS IS" BASIS,
|
11
11
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
See the License for the specific language governing permissions and
|
13
|
-
limitations under the License.
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# midi-jruby
|
2
|
+
|
3
|
+
Realtime MIDI IO with JRuby using the javax.sound.midi API.
|
4
|
+
|
5
|
+
In the interest of allowing people on other platforms to utilize your code, you should consider using [unimidi](http://github.com/arirusso/unimidi). Unimidi is a platform independent wrapper that implements midi-jruby and has a similar API.
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
* Simplified API
|
10
|
+
* Input and output on multiple devices concurrently
|
11
|
+
* Generalized handling of different MIDI Message types (including SysEx)
|
12
|
+
* Timestamped input events
|
13
|
+
|
14
|
+
## Install
|
15
|
+
|
16
|
+
If you're using Bundler, add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
`gem "midi-jruby"`
|
19
|
+
|
20
|
+
Otherwise
|
21
|
+
|
22
|
+
`gem install midi-jruby`
|
23
|
+
|
24
|
+
## Examples
|
25
|
+
|
26
|
+
* [Input](http://github.com/arirusso/midi-jruby/blob/master/examples/input.rb)
|
27
|
+
* [Output](http://github.com/arirusso/midi-jruby/blob/master/examples/output.rb)
|
28
|
+
|
29
|
+
## Issues
|
30
|
+
|
31
|
+
There is [an issue](http://stackoverflow.com/questions/8148898/java-midi-in-mac-osx-broken) that causes javax.sound.midi not to be able to send SysEx messages in some versions.
|
32
|
+
|
33
|
+
## Documentation
|
34
|
+
|
35
|
+
* [rdoc](http://rdoc.info/gems/midi-jruby)
|
36
|
+
|
37
|
+
## Author
|
38
|
+
|
39
|
+
[Ari Russo](http://github.com/arirusso) <ari.russo at gmail.com>
|
40
|
+
|
41
|
+
## License
|
42
|
+
|
43
|
+
Apache 2.0, See the file LICENSE
|
44
|
+
|
45
|
+
Copyright (c) 2011-2014 Ari Russo
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module MIDIJRuby
|
2
|
+
|
3
|
+
# Access to javax.sound.midi
|
4
|
+
module API
|
5
|
+
|
6
|
+
import javax.sound.midi.MidiSystem
|
7
|
+
import javax.sound.midi.MidiDevice
|
8
|
+
import javax.sound.midi.MidiEvent
|
9
|
+
import javax.sound.midi.Receiver
|
10
|
+
import javax.sound.midi.ShortMessage
|
11
|
+
import javax.sound.midi.SysexMessage
|
12
|
+
import javax.sound.midi.Transmitter
|
13
|
+
|
14
|
+
extend self
|
15
|
+
|
16
|
+
# Get all MIDI devices that are available via javax.sound.midi
|
17
|
+
# @return [Array<Hash>] A set of hashes for each available device
|
18
|
+
def get_devices
|
19
|
+
MidiSystem.get_midi_device_info.map do |info|
|
20
|
+
jdevice = MidiSystem.get_midi_device(info)
|
21
|
+
{
|
22
|
+
:device => jdevice,
|
23
|
+
:id => get_uuid,
|
24
|
+
:name => info.get_name,
|
25
|
+
:description => info.get_description,
|
26
|
+
:vendor => info.get_vendor
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get all MIDI inputs that are available via javax.sound.midi
|
32
|
+
# @return [Array<Input>]
|
33
|
+
def get_inputs
|
34
|
+
jinputs = get_devices.select { |device| !device[:device].get_max_transmitters.zero? }
|
35
|
+
jinputs.map { |jinput| Input.new(jinput[:id], jinput[:device], jinput) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get all MIDI outputs that are available via javax.sound.midi
|
39
|
+
# @return [Array<Output>]
|
40
|
+
def get_outputs
|
41
|
+
joutputs = get_devices.select { |device| !device[:device].get_max_receivers.zero? }
|
42
|
+
joutputs.map { |joutput| Output.new(joutput[:id], joutput[:device], joutput) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Enable the given input device to receive MIDI messages
|
46
|
+
# @param [Java::ComSunMediaSound::MidiInDevice] device
|
47
|
+
# @return [Boolean]
|
48
|
+
def enable_input(device)
|
49
|
+
device.open
|
50
|
+
@transmitter ||= {}
|
51
|
+
@transmitter[device] = device.get_transmitter
|
52
|
+
@transmitter[device].set_receiver(InputReceiver.new)
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Enable the given output to emit MIDI messages
|
57
|
+
# @param [Java::ComSunMediaSound::MidiOutDevice] device
|
58
|
+
# @return [Boolean]
|
59
|
+
def enable_output(device)
|
60
|
+
@receiver ||= {}
|
61
|
+
@receiver[device] = device.get_receiver
|
62
|
+
device.open
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Close the given output device
|
67
|
+
# @param [Java::ComSunMediaSound::MidiOutDevice] device
|
68
|
+
# @return [Boolean]
|
69
|
+
def close_output(device)
|
70
|
+
@receiver[device].close
|
71
|
+
@receiver.delete(device)
|
72
|
+
device.close
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Close the given input device
|
77
|
+
# @param [Java::ComSunMediaSound::MidiInDevice] device
|
78
|
+
# @return [Boolean]
|
79
|
+
def close_input(device)
|
80
|
+
# http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4914667
|
81
|
+
# @transmitter[device].close
|
82
|
+
# device.close
|
83
|
+
@transmitter.delete(device)
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
87
|
+
# Read any new MIDI messages from the given input device
|
88
|
+
# @param [Java::ComSunMediaSound::MidiInDevice] device
|
89
|
+
# @return [Array<Array<Fixnum>>]
|
90
|
+
def read_input(device)
|
91
|
+
@transmitter[device].get_receiver.read
|
92
|
+
end
|
93
|
+
|
94
|
+
# Write the given MIDI message to the given output device
|
95
|
+
# @param [Java::ComSunMediaSound::MidiOutDevice] device
|
96
|
+
# @param [Array<Fixnum>] data
|
97
|
+
# @return [Boolean]
|
98
|
+
def write_output(device, data)
|
99
|
+
bytes = Java::byte[data.size].new
|
100
|
+
data.each_with_index { |byte, i| bytes.ubyte_set(i, byte) }
|
101
|
+
message = data.first.eql?(0xF0) ? SysexMessage.new : ShortMessage.new
|
102
|
+
message.set_message(bytes, data.length.to_java(:int))
|
103
|
+
@receiver[device].send(message, 0)
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# Generate a uuid for a MIDI device
|
110
|
+
# @return [Fixnum]
|
111
|
+
def get_uuid
|
112
|
+
@id ||= -1
|
113
|
+
@id += 1
|
114
|
+
end
|
115
|
+
|
116
|
+
# Input event handler class
|
117
|
+
class InputReceiver
|
118
|
+
|
119
|
+
include javax.sound.midi.Receiver
|
120
|
+
|
121
|
+
attr_reader :stream
|
122
|
+
|
123
|
+
def initialize
|
124
|
+
@buffer = []
|
125
|
+
end
|
126
|
+
|
127
|
+
# Pluck messages from the buffer
|
128
|
+
# @return [Array<Array<Fixnum>>]
|
129
|
+
def read
|
130
|
+
messages = @buffer.dup
|
131
|
+
@buffer.clear
|
132
|
+
messages
|
133
|
+
end
|
134
|
+
|
135
|
+
# Add a new message to the buffer
|
136
|
+
# @param [Array<Fixnum>] message
|
137
|
+
# @param [Fixnum] timestamp
|
138
|
+
# @return [Array<Array<Fixnum>>]
|
139
|
+
def send(message, timestamp = -1)
|
140
|
+
bytes = if message.respond_to?(:get_packed_message)
|
141
|
+
packed = message.get_packed_message
|
142
|
+
unpack(packed)
|
143
|
+
else
|
144
|
+
string = String.from_java_bytes(message.get_message)
|
145
|
+
string.unpack("C" * string.length)
|
146
|
+
end
|
147
|
+
@buffer << bytes
|
148
|
+
end
|
149
|
+
|
150
|
+
def close
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# @param [String]
|
156
|
+
# @return [Array<Fixnum>]
|
157
|
+
def unpack(message)
|
158
|
+
bytes = []
|
159
|
+
string = message.to_s(16)
|
160
|
+
string = "0#{s}" if string.length.divmod(2).last > 0
|
161
|
+
while string.length > 0
|
162
|
+
string_byte = string.slice!(0,2)
|
163
|
+
bytes << string_byte.hex
|
164
|
+
end
|
165
|
+
bytes.reverse
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
data/lib/midi-jruby/device.rb
CHANGED
@@ -1,73 +1,70 @@
|
|
1
1
|
module MIDIJRuby
|
2
2
|
|
3
|
-
#
|
4
|
-
# Module containing methods used by both input and output devices
|
5
|
-
#
|
3
|
+
# Common methods used by both input and output devices
|
6
4
|
module Device
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
#
|
14
|
-
attr_reader :enabled,
|
15
|
-
# unique int id
|
16
|
-
:id,
|
17
|
-
# name property from javax.sound.midi.MidiDevice.Info
|
18
|
-
:name,
|
19
|
-
# description property from javax.sound.midi.MidiDevice.Info
|
20
|
-
:description,
|
21
|
-
# vendor property from javax.sound.midi.MidiDevice.Info
|
22
|
-
:vendor,
|
23
|
-
# :input or :output
|
24
|
-
:type
|
6
|
+
attr_reader :enabled, # has the device been initialized?
|
7
|
+
:id, # unique int id
|
8
|
+
:name, # name property from javax.sound.midi.MidiDevice.Info
|
9
|
+
:description, # description property from javax.sound.midi.MidiDevice.Info
|
10
|
+
:vendor, # vendor property from javax.sound.midi.MidiDevice.Info
|
11
|
+
:type # :input or :output
|
25
12
|
|
26
13
|
alias_method :enabled?, :enabled
|
27
14
|
|
28
|
-
|
15
|
+
# @param [Fixnum] The uuid for the given device
|
16
|
+
# @param [Java::ComSunMediaSound::MidiInDevice, Java::ComSunMediaSound::MidiOutDevice] device The underlying Java device object
|
17
|
+
# @param [Hash] options
|
18
|
+
# @option options [String] :description
|
19
|
+
# @option options [String] :name
|
20
|
+
# @option options [String] :vendor
|
21
|
+
def initialize(id, device, options = {})
|
29
22
|
@name = options[:name]
|
30
23
|
@description = options[:description]
|
31
24
|
@vendor = options[:vendor]
|
32
25
|
@id = id
|
33
26
|
@device = device
|
34
27
|
|
35
|
-
|
36
|
-
@type = self.class.name.split('::').last.downcase.to_sym
|
37
|
-
|
28
|
+
@type = get_type
|
38
29
|
@enabled = false
|
39
30
|
end
|
40
31
|
|
41
|
-
#
|
42
|
-
|
43
|
-
|
32
|
+
# Select the first device of the given direction
|
33
|
+
# @param [Symbol] direction
|
34
|
+
# @return [Input, Output]
|
35
|
+
def self.first(direction)
|
36
|
+
all_by_type[direction].first
|
44
37
|
end
|
45
38
|
|
46
|
-
#
|
47
|
-
|
48
|
-
|
39
|
+
# Select the last device of the given direction
|
40
|
+
# @param [Symbol] direction
|
41
|
+
# @return [Input, Output]
|
42
|
+
def self.last(direction)
|
43
|
+
all_by_type[direction].last
|
49
44
|
end
|
50
45
|
|
51
|
-
#
|
46
|
+
# A hash of :input and :output devices
|
47
|
+
# @return [Hash]
|
52
48
|
def self.all_by_type
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
opts = { :name => info.get_name,
|
58
|
-
:description => info.get_description,
|
59
|
-
:vendor => info.get_vendor }
|
60
|
-
available_devices[:output] << Output.new(count += 1, device, opts) unless device.get_max_receivers.zero?
|
61
|
-
available_devices[:input] << Input.new(count += 1, device, opts) unless device.get_max_transmitters.zero?
|
62
|
-
end
|
63
|
-
available_devices
|
49
|
+
@devices ||= {
|
50
|
+
:input => API.get_inputs,
|
51
|
+
:output => API.get_outputs
|
52
|
+
}
|
64
53
|
end
|
65
54
|
|
66
|
-
#
|
55
|
+
# All devices of both directions
|
56
|
+
# @return [Array<Input, Output>]
|
67
57
|
def self.all
|
68
58
|
all_by_type.values.flatten
|
69
59
|
end
|
70
60
|
|
61
|
+
private
|
62
|
+
|
63
|
+
# @return [String]
|
64
|
+
def get_type
|
65
|
+
self.class.name.split('::').last.downcase.to_sym
|
66
|
+
end
|
67
|
+
|
71
68
|
end
|
72
69
|
|
73
|
-
end
|
70
|
+
end
|
data/lib/midi-jruby/input.rb
CHANGED
@@ -1,108 +1,58 @@
|
|
1
1
|
module MIDIJRuby
|
2
2
|
|
3
|
-
#
|
4
3
|
# Input device class
|
5
|
-
#
|
6
4
|
class Input
|
7
5
|
|
8
|
-
import javax.sound.midi.Transmitter
|
9
|
-
|
10
6
|
include Device
|
11
7
|
|
12
8
|
attr_reader :buffer
|
13
|
-
|
14
|
-
class InputReceiver
|
15
|
-
|
16
|
-
include javax.sound.midi.Receiver
|
17
|
-
extend Forwardable
|
18
|
-
|
19
|
-
attr_reader :stream
|
20
|
-
|
21
|
-
def initialize
|
22
|
-
@buf = []
|
23
|
-
end
|
24
|
-
|
25
|
-
def read
|
26
|
-
to_return = @buf.dup
|
27
|
-
@buf.clear
|
28
|
-
to_return
|
29
|
-
end
|
30
|
-
|
31
|
-
def send(msg, timestamp = -1)
|
32
|
-
if msg.respond_to?(:get_packed_msg)
|
33
|
-
m = msg.get_packed_msg
|
34
|
-
@buf << unpack(m)
|
35
|
-
else
|
36
|
-
str = String.from_java_bytes(msg.get_data)
|
37
|
-
arr = str.unpack("C" * str.length)
|
38
|
-
arr.insert(0, msg.get_status)
|
39
|
-
@buf << arr
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def unpack(msg)
|
46
|
-
# there's probably a better way of doing this
|
47
|
-
o = []
|
48
|
-
s = msg.to_s(16)
|
49
|
-
s = "0" + s if s.length.divmod(2).last > 0
|
50
|
-
while s.length > 0
|
51
|
-
o << s.slice!(0,2).hex
|
52
|
-
end
|
53
|
-
o.reverse
|
54
|
-
end
|
55
|
-
|
56
|
-
end
|
57
|
-
|
9
|
+
|
58
10
|
#
|
59
|
-
#
|
11
|
+
# An array of MIDI event hashes as such:
|
60
12
|
# [
|
61
13
|
# { :data => [144, 60, 100], :timestamp => 1024 },
|
62
14
|
# { :data => [128, 60, 100], :timestamp => 1100 },
|
63
15
|
# { :data => [144, 40, 120], :timestamp => 1200 }
|
64
16
|
# ]
|
65
17
|
#
|
66
|
-
#
|
67
|
-
#
|
18
|
+
# The data is an array of numeric bytes
|
19
|
+
# The timestamp is the number of millis since this input was enabled
|
68
20
|
#
|
21
|
+
# @return [Array<Hash>]
|
69
22
|
def gets
|
70
|
-
until queued_messages?
|
71
|
-
|
72
|
-
msgs = queued_messages
|
23
|
+
loop until queued_messages?
|
24
|
+
messages = queued_messages
|
73
25
|
@pointer = @buffer.length
|
74
|
-
|
26
|
+
messages
|
75
27
|
end
|
76
28
|
alias_method :read, :gets
|
77
29
|
|
78
|
-
#
|
30
|
+
# Same as Input#gets but returns message data as string of hex digits:
|
79
31
|
# [
|
80
32
|
# { :data => "904060", :timestamp => 904 },
|
81
33
|
# { :data => "804060", :timestamp => 1150 },
|
82
34
|
# { :data => "90447F", :timestamp => 1300 }
|
83
35
|
# ]
|
84
36
|
#
|
85
|
-
#
|
37
|
+
# @return [Array<Hash>]
|
86
38
|
def gets_s
|
87
|
-
|
88
|
-
|
89
|
-
|
39
|
+
messages = gets
|
40
|
+
messages.each { |message| message[:data] = numeric_bytes_to_hex_string(message[:data]) }
|
41
|
+
messages
|
90
42
|
end
|
91
43
|
alias_method :gets_bytestr, :gets_s
|
92
44
|
alias_method :gets_hex, :gets_s
|
93
45
|
|
94
|
-
#
|
46
|
+
# Enable this the input for use; can be passed a block
|
47
|
+
# @param [Hash] options
|
48
|
+
# @param [Proc] block
|
49
|
+
# @return [Input] self
|
95
50
|
def enable(options = {}, &block)
|
96
|
-
|
97
|
-
@transmitter = @device.get_transmitter
|
98
|
-
@transmitter.set_receiver(InputReceiver.new)
|
99
|
-
initialize_buffer
|
100
|
-
@start_time = Time.now.to_f
|
101
|
-
spawn_listener!
|
51
|
+
initialize_input
|
102
52
|
@enabled = true
|
103
53
|
if block_given?
|
104
54
|
begin
|
105
|
-
|
55
|
+
yield(self)
|
106
56
|
ensure
|
107
57
|
close
|
108
58
|
end
|
@@ -113,28 +63,46 @@ module MIDIJRuby
|
|
113
63
|
alias_method :open, :enable
|
114
64
|
alias_method :start, :enable
|
115
65
|
|
116
|
-
#
|
66
|
+
# Close this input
|
67
|
+
# @return [Boolean]
|
117
68
|
def close
|
118
69
|
@listener.kill
|
119
|
-
@
|
120
|
-
@device.close
|
70
|
+
API.close_input(@device)
|
121
71
|
@enabled = false
|
122
72
|
end
|
123
73
|
|
74
|
+
# Select the first input
|
75
|
+
# @return [Input]
|
124
76
|
def self.first
|
125
77
|
Device.first(:input)
|
126
78
|
end
|
127
79
|
|
80
|
+
# Select the last input
|
81
|
+
# @return [Input]
|
128
82
|
def self.last
|
129
83
|
Device.last(:input)
|
130
84
|
end
|
131
85
|
|
86
|
+
# All inputs
|
87
|
+
# @return [Array<Input>]
|
132
88
|
def self.all
|
133
89
|
Device.all_by_type[:input]
|
134
90
|
end
|
135
91
|
|
136
92
|
private
|
93
|
+
|
94
|
+
# Initialize the input components
|
95
|
+
# @return [Boolean]
|
96
|
+
def initialize_input
|
97
|
+
initialize_buffer
|
98
|
+
API.enable_input(@device)
|
99
|
+
@start_time = Time.now.to_f
|
100
|
+
initialize_listener
|
101
|
+
true
|
102
|
+
end
|
137
103
|
|
104
|
+
# Initialize the input buffer
|
105
|
+
# @return [Boolean]
|
138
106
|
def initialize_buffer
|
139
107
|
@buffer = []
|
140
108
|
@pointer = 0
|
@@ -144,47 +112,75 @@ module MIDIJRuby
|
|
144
112
|
end
|
145
113
|
end
|
146
114
|
|
115
|
+
# Get a timestamp for the current time
|
116
|
+
# @return [Fixnum]
|
147
117
|
def now
|
148
|
-
|
118
|
+
now = Time.now.to_f - @start_time
|
119
|
+
now * 1000
|
149
120
|
end
|
150
121
|
|
151
|
-
#
|
152
|
-
|
153
|
-
|
122
|
+
# A hash of a MIDI message and corresponding timestamp
|
123
|
+
# @param [Array<Fixnum>] message
|
124
|
+
# @param [Fixnum] timestamp
|
125
|
+
# @return [Hash]
|
126
|
+
def get_message_formatted(message, timestamp)
|
127
|
+
{
|
128
|
+
:data => message,
|
129
|
+
:timestamp => timestamp
|
130
|
+
}
|
154
131
|
end
|
155
132
|
|
133
|
+
# Messages in the buffer
|
134
|
+
# @return [Array<Array<Fixnum>>]
|
156
135
|
def queued_messages
|
157
136
|
@buffer.slice(@pointer, @buffer.length - @pointer)
|
158
137
|
end
|
159
138
|
|
139
|
+
# Are there any new messages in the buffer?
|
140
|
+
# @return [Boolean]
|
160
141
|
def queued_messages?
|
161
142
|
@pointer < @buffer.length
|
162
143
|
end
|
163
144
|
|
164
|
-
#
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
145
|
+
# Launch a background thread that collects messages
|
146
|
+
# @return [Thread]
|
147
|
+
def initialize_listener
|
148
|
+
@listener = Thread.new do
|
149
|
+
begin
|
150
|
+
loop do
|
151
|
+
while (messages = API.read_input(@device)).empty?
|
152
|
+
sleep(1.0/1000)
|
153
|
+
end
|
154
|
+
add_to_buffer(messages) unless messages.empty?
|
170
155
|
end
|
171
|
-
|
156
|
+
rescue Exception => exception
|
157
|
+
Thread.main.raise(exception)
|
172
158
|
end
|
173
159
|
end
|
160
|
+
@listener.abort_on_exception = true
|
161
|
+
@listener
|
162
|
+
end
|
163
|
+
|
164
|
+
# @param [Array<Array<Fixnum>>]
|
165
|
+
# @return [Array<Array<Fixnum>>]
|
166
|
+
def add_to_buffer(messages)
|
167
|
+
@buffer += messages.compact.map do |message|
|
168
|
+
get_message_formatted(message, now)
|
169
|
+
end
|
174
170
|
end
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
def populate_local_buffer(msgs)
|
181
|
-
msgs.each { |raw| @buffer << get_message_formatted(raw, now) unless raw.nil? }
|
182
|
-
end
|
183
|
-
|
171
|
+
|
172
|
+
# Convert an array of numeric bytes to a hex string (eg [0x90, 0x40, 0x40] -> "904040")
|
173
|
+
# @param [Array<Fixnum>] bytes
|
174
|
+
# @return [String]
|
184
175
|
def numeric_bytes_to_hex_string(bytes)
|
185
|
-
bytes.map
|
186
|
-
|
176
|
+
string_bytes = bytes.map do |byte|
|
177
|
+
string = byte.to_s(16).upcase
|
178
|
+
string = "0#{string}" if byte < 0x10
|
179
|
+
string
|
180
|
+
end
|
181
|
+
string_bytes.join
|
182
|
+
end
|
187
183
|
|
188
184
|
end
|
189
185
|
|
190
|
-
end
|
186
|
+
end
|
data/lib/midi-jruby/output.rb
CHANGED
@@ -1,61 +1,56 @@
|
|
1
1
|
module MIDIJRuby
|
2
2
|
|
3
|
-
#
|
4
3
|
# Output device class
|
5
|
-
#
|
6
4
|
class Output
|
7
|
-
import javax.sound.midi.ShortMessage
|
8
|
-
import javax.sound.midi.SysexMessage
|
9
5
|
|
10
6
|
include Device
|
11
7
|
|
12
|
-
#
|
8
|
+
# Close this output
|
9
|
+
# @return [Boolean]
|
13
10
|
def close
|
14
|
-
@device
|
11
|
+
API.close_output(@device)
|
15
12
|
@enabled = false
|
16
13
|
end
|
17
14
|
|
18
|
-
#
|
15
|
+
# Output the given MIDI message
|
16
|
+
# @param [String] data A MIDI message expressed as a string of hex digits
|
17
|
+
# @return [Boolean]
|
19
18
|
def puts_s(data)
|
20
|
-
|
21
|
-
|
22
|
-
until (str = data.slice!(0,2)).eql?("")
|
23
|
-
output << str.hex
|
24
|
-
end
|
25
|
-
puts_bytes(*output)
|
19
|
+
bytes = hex_string_to_numeric_bytes(data)
|
20
|
+
puts_bytes(*bytes)
|
26
21
|
end
|
27
22
|
alias_method :puts_bytestr, :puts_s
|
28
23
|
alias_method :puts_hex, :puts_s
|
29
24
|
|
30
|
-
#
|
25
|
+
# Output the given MIDI message
|
26
|
+
# @param [*Fixnum] data A MIDI messages expressed as Numeric bytes
|
27
|
+
# @return [Boolean]
|
31
28
|
def puts_bytes(*data)
|
32
|
-
|
33
|
-
msg = SysexMessage.new
|
34
|
-
msg.set_message(data.to_java(:byte), data.length)
|
35
|
-
else
|
36
|
-
msg = ShortMessage.new
|
37
|
-
msg.set_message(*data)
|
38
|
-
end
|
39
|
-
@device.get_receiver.send(msg, 0)
|
29
|
+
API.write_output(@device, data)
|
40
30
|
end
|
41
31
|
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
when
|
32
|
+
# Output the given MIDI message
|
33
|
+
# @param [*Fixnum, *String] args
|
34
|
+
# @return [Boolean]
|
35
|
+
def puts(*args)
|
36
|
+
case args.first
|
37
|
+
when Array then puts_bytes(*args.first)
|
38
|
+
when Numeric then puts_bytes(*args)
|
39
|
+
when String then puts_bytestr(*args)
|
48
40
|
end
|
49
41
|
end
|
50
42
|
alias_method :write, :puts
|
51
43
|
|
52
|
-
#
|
44
|
+
# Enable this device; also takes a block
|
45
|
+
# @param [Hash] options
|
46
|
+
# @param [Proc] block
|
47
|
+
# @return [Output]
|
53
48
|
def enable(options = {}, &block)
|
54
|
-
@device
|
49
|
+
API.enable_output(@device)
|
55
50
|
@enabled = true
|
56
|
-
|
51
|
+
if block_given?
|
57
52
|
begin
|
58
|
-
|
53
|
+
yield(self)
|
59
54
|
ensure
|
60
55
|
close
|
61
56
|
end
|
@@ -66,17 +61,38 @@ module MIDIJRuby
|
|
66
61
|
alias_method :open, :enable
|
67
62
|
alias_method :start, :enable
|
68
63
|
|
64
|
+
# Select the first output
|
65
|
+
# @return [Output]
|
69
66
|
def self.first
|
70
67
|
Device.first(:output)
|
71
68
|
end
|
72
69
|
|
70
|
+
# Select the last output
|
71
|
+
# @return [Output]
|
73
72
|
def self.last
|
74
73
|
Device.last(:output)
|
75
74
|
end
|
76
75
|
|
76
|
+
# All outputs
|
77
|
+
# @return [Array<Output>]
|
77
78
|
def self.all
|
78
79
|
Device.all_by_type[:output]
|
79
80
|
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Convert a hex string to numeric bytes (eg "904040" -> [0x90, 0x40, 0x40])
|
85
|
+
# @param [String] string
|
86
|
+
# @return [Array<Fixnum>]
|
87
|
+
def hex_string_to_numeric_bytes(string)
|
88
|
+
string = string.dup
|
89
|
+
bytes = []
|
90
|
+
until (string_byte = string.slice!(0,2)) == ""
|
91
|
+
bytes << string_byte.hex
|
92
|
+
end
|
93
|
+
bytes
|
94
|
+
end
|
95
|
+
|
80
96
|
end
|
81
97
|
|
82
|
-
end
|
98
|
+
end
|
data/lib/midi-jruby.rb
CHANGED
@@ -1,15 +1,23 @@
|
|
1
|
-
|
2
|
-
require 'forwardable'
|
3
|
-
|
4
|
-
#
|
5
|
-
# Set of modules and classes for interacting with javax.sound.midi
|
1
|
+
# Realtime MIDI IO in JRuby using the javax.sound.midi API
|
6
2
|
#
|
3
|
+
# Ari Russo
|
4
|
+
# (c) 2011-2014
|
5
|
+
# Licensed under Apache 2.0
|
6
|
+
|
7
|
+
# libs
|
8
|
+
require "java"
|
9
|
+
require "forwardable"
|
10
|
+
|
11
|
+
# modules
|
12
|
+
require "midi-jruby/api"
|
13
|
+
require "midi-jruby/device"
|
14
|
+
|
15
|
+
# classes
|
16
|
+
require "midi-jruby/input"
|
17
|
+
require "midi-jruby/output"
|
18
|
+
|
7
19
|
module MIDIJRuby
|
8
20
|
|
9
|
-
|
21
|
+
VERSION = "0.1.1"
|
10
22
|
|
11
23
|
end
|
12
|
-
|
13
|
-
require 'midi-jruby/device'
|
14
|
-
require 'midi-jruby/input'
|
15
|
-
require 'midi-jruby/output'
|
data/test/helper.rb
ADDED
@@ -0,0 +1,60 @@
|
|
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 "midi-jruby"
|
8
|
+
|
9
|
+
module TestHelper
|
10
|
+
|
11
|
+
extend self
|
12
|
+
|
13
|
+
# http://stackoverflow.com/questions/8148898/java-midi-in-mac-osx-broken
|
14
|
+
def sysex_ok?
|
15
|
+
ENV["_system_name"] != "OSX"
|
16
|
+
end
|
17
|
+
|
18
|
+
def bytestrs_to_ints(arr)
|
19
|
+
data = arr.map { |m| m[:data] }.join
|
20
|
+
output = []
|
21
|
+
until (bytestr = data.slice!(0,2)).eql?("")
|
22
|
+
output << bytestr.hex
|
23
|
+
end
|
24
|
+
output
|
25
|
+
end
|
26
|
+
|
27
|
+
def numeric_messages
|
28
|
+
messages = [
|
29
|
+
[0x90, 100, 100], # note on
|
30
|
+
[0x90, 43, 100], # note on
|
31
|
+
[0x90, 76, 100], # note on
|
32
|
+
[0x90, 60, 100], # note on
|
33
|
+
[0x80, 100, 100] # note off
|
34
|
+
]
|
35
|
+
if sysex_ok?
|
36
|
+
messages << [0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7]
|
37
|
+
end
|
38
|
+
messages
|
39
|
+
end
|
40
|
+
|
41
|
+
def string_messages
|
42
|
+
messages = [
|
43
|
+
"906440", # note on
|
44
|
+
"804340" # note off
|
45
|
+
]
|
46
|
+
if sysex_ok?
|
47
|
+
messages << "F04110421240007F0041F7"
|
48
|
+
end
|
49
|
+
messages
|
50
|
+
end
|
51
|
+
|
52
|
+
def input
|
53
|
+
MIDIJRuby::Input.first
|
54
|
+
end
|
55
|
+
|
56
|
+
def output
|
57
|
+
MIDIJRuby::Output.first
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class MIDIJRuby::InputBufferTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "MIDIJRuby" do
|
6
|
+
|
7
|
+
setup do
|
8
|
+
@input = TestHelper.input.open
|
9
|
+
@output = TestHelper.output.open
|
10
|
+
@input.buffer.clear
|
11
|
+
@pointer = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
teardown do
|
15
|
+
@input.close
|
16
|
+
@output.close
|
17
|
+
end
|
18
|
+
|
19
|
+
context "Source#buffer" do
|
20
|
+
|
21
|
+
setup do
|
22
|
+
@messages = TestHelper.numeric_messages
|
23
|
+
@messages_arr = @messages.inject(&:+).flatten
|
24
|
+
@received_arr = []
|
25
|
+
end
|
26
|
+
|
27
|
+
should "have the correct messages in the buffer" do
|
28
|
+
bytes = []
|
29
|
+
@messages.each do |message|
|
30
|
+
p "sending: #{message}"
|
31
|
+
@output.puts(message)
|
32
|
+
bytes += message
|
33
|
+
|
34
|
+
sleep(1)
|
35
|
+
|
36
|
+
buffer = @input.buffer.map { |m| m[:data] }.flatten
|
37
|
+
p "received: #{buffer.to_s}"
|
38
|
+
assert_equal(bytes, buffer)
|
39
|
+
end
|
40
|
+
assert_equal(bytes.length, @input.buffer.map { |m| m[:data] }.flatten.length)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/test/io_test.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class MIDIJRuby::IOTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# These tests assume that TestOutput is connected to TestInput
|
6
|
+
context "MIDIJRuby" do
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@input = TestHelper.input.open
|
10
|
+
@output = TestHelper.output.open
|
11
|
+
@input.buffer.clear
|
12
|
+
@pointer = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
teardown do
|
16
|
+
@input.close
|
17
|
+
@output.close
|
18
|
+
end
|
19
|
+
|
20
|
+
context "full IO" do
|
21
|
+
|
22
|
+
context "using Arrays" do
|
23
|
+
|
24
|
+
setup do
|
25
|
+
@messages = TestHelper.numeric_messages
|
26
|
+
@messages_arr = @messages.inject(&:+).flatten
|
27
|
+
@received_arr = []
|
28
|
+
end
|
29
|
+
|
30
|
+
should "do IO" do
|
31
|
+
@messages.each do |message|
|
32
|
+
|
33
|
+
p "sending: #{message}"
|
34
|
+
|
35
|
+
@output.puts(message)
|
36
|
+
sleep(1)
|
37
|
+
received = @input.gets.map { |m| m[:data] }.flatten
|
38
|
+
|
39
|
+
p "received: #{received}"
|
40
|
+
|
41
|
+
assert_equal(@messages_arr.slice(@pointer, received.length), received)
|
42
|
+
@pointer += received.length
|
43
|
+
@received_arr += received
|
44
|
+
end
|
45
|
+
assert_equal(@messages_arr.length, @received_arr.length)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "using byte Strings" do
|
50
|
+
|
51
|
+
setup do
|
52
|
+
@messages = TestHelper.string_messages
|
53
|
+
@messages_str = @messages.join
|
54
|
+
@received_str = ""
|
55
|
+
end
|
56
|
+
|
57
|
+
should "do IO" do
|
58
|
+
@messages.each do |message|
|
59
|
+
|
60
|
+
p "sending: #{message}"
|
61
|
+
|
62
|
+
@output.puts(message)
|
63
|
+
sleep(1)
|
64
|
+
received = @input.gets_bytestr.map { |m| m[:data] }.flatten.join
|
65
|
+
p "received: #{received}"
|
66
|
+
|
67
|
+
assert_equal(@messages_str.slice(@pointer, received.length), received)
|
68
|
+
@pointer += received.length
|
69
|
+
@received_str += received
|
70
|
+
end
|
71
|
+
assert_equal(@messages_str, @received_str)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
metadata
CHANGED
@@ -1,62 +1,54 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: midi-jruby
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
version: 0.0.12
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
6
5
|
platform: ruby
|
7
|
-
authors:
|
8
|
-
|
9
|
-
autorequire:
|
6
|
+
authors:
|
7
|
+
- Ari Russo
|
8
|
+
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
|
13
|
-
date: 2012-03-04 00:00:00 -05:00
|
14
|
-
default_executable:
|
11
|
+
date: 2014-09-28 00:00:00.000000000 Z
|
15
12
|
dependencies: []
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
- ari.russo@gmail.com
|
13
|
+
description: Realtime MIDI IO with JRuby using the javax.sound.midi API
|
14
|
+
email:
|
15
|
+
- ari.russo@gmail.com
|
20
16
|
executables: []
|
21
|
-
|
22
17
|
extensions: []
|
23
|
-
|
24
18
|
extra_rdoc_files: []
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
19
|
+
files:
|
20
|
+
- lib/midi-jruby.rb
|
21
|
+
- lib/midi-jruby/api.rb
|
22
|
+
- lib/midi-jruby/device.rb
|
23
|
+
- lib/midi-jruby/input.rb
|
24
|
+
- lib/midi-jruby/output.rb
|
25
|
+
- test/helper.rb
|
26
|
+
- test/input_buffer_test.rb
|
27
|
+
- test/io_test.rb
|
28
|
+
- LICENSE
|
29
|
+
- README.md
|
34
30
|
homepage: http://github.com/arirusso/midi-jruby
|
35
|
-
licenses:
|
36
|
-
|
37
|
-
|
31
|
+
licenses:
|
32
|
+
- Apache 2.0
|
33
|
+
metadata: {}
|
34
|
+
post_install_message:
|
38
35
|
rdoc_options: []
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
- - ">="
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: 1.3.6
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.3.6
|
54
48
|
requirements: []
|
55
|
-
|
56
49
|
rubyforge_project: midi-jruby
|
57
|
-
rubygems_version: 1.
|
58
|
-
signing_key:
|
59
|
-
specification_version:
|
60
|
-
summary:
|
50
|
+
rubygems_version: 2.1.9
|
51
|
+
signing_key:
|
52
|
+
specification_version: 4
|
53
|
+
summary: Realtime MIDI IO with JRuby
|
61
54
|
test_files: []
|
62
|
-
|
data/README.rdoc
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
= midi-jruby
|
2
|
-
|
3
|
-
== Summary
|
4
|
-
|
5
|
-
Simple MIDI wrapper for realtime IO in JRuby. Uses javax.sound.midi.
|
6
|
-
|
7
|
-
In the interest of allowing people on other platforms to utilize your code, you should consider using {unimidi}[http://github.com/arirusso/unimidi]. Unimidi is a platform independent wrapper which implements midi-jruby for users who are using jruby.
|
8
|
-
|
9
|
-
== Features
|
10
|
-
|
11
|
-
* Input and output on multiple devices concurrently
|
12
|
-
* Agnostically handle different MIDI Message types (including SysEx)
|
13
|
-
* Timestamped input events
|
14
|
-
|
15
|
-
== Install
|
16
|
-
|
17
|
-
* gem install midi-jruby
|
18
|
-
|
19
|
-
== Usage
|
20
|
-
|
21
|
-
This library requires that JRuby be run in '1.9 mode'. This is normally done by passing --1.9 to JRuby at the command line
|
22
|
-
|
23
|
-
== Examples
|
24
|
-
|
25
|
-
* {input}[http://github.com/arirusso/midi-jruby/blob/master/examples/input.rb]
|
26
|
-
* {output}[http://github.com/arirusso/midi-jruby/blob/master/examples/output.rb]
|
27
|
-
|
28
|
-
== Issues
|
29
|
-
|
30
|
-
There is an issue that causes javax.sound.midi not to be able to send SysEx messages in some OSX Snow Leopard versions.
|
31
|
-
|
32
|
-
== Tests
|
33
|
-
|
34
|
-
Use
|
35
|
-
|
36
|
-
jruby --1.9 -S rake test
|
37
|
-
|
38
|
-
* please see {test/config.rb}[http://github.com/arirusso/midi-jruby/blob/master/test/config.rb] before running tests
|
39
|
-
|
40
|
-
== Documentation
|
41
|
-
|
42
|
-
* {rdoc}[http://rdoc.info/gems/midi-jruby]
|
43
|
-
|
44
|
-
== Author
|
45
|
-
|
46
|
-
{Ari Russo}[http://github.com/arirusso] <ari.russo at gmail.com>
|
47
|
-
|
48
|
-
== License
|
49
|
-
|
50
|
-
Apache 2.0, See the file LICENSE
|
51
|
-
|
52
|
-
Copyright (c) 2011 Ari Russo
|