midi-jruby 0.0.12 → 0.1.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 +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
|