midi-eye 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.rdoc +73 -0
- data/lib/midi-eye.rb +20 -0
- data/lib/midi-eye/listener.rb +150 -0
- data/lib/midi-eye/unimidi_input.rb +44 -0
- data/test/helper.rb +28 -0
- data/test/test_listener.rb +156 -0
- metadata +85 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2011 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.rdoc
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
= midi-eye
|
2
|
+
|
3
|
+
A transparent MIDI input event listener for Ruby
|
4
|
+
|
5
|
+
== Requirements
|
6
|
+
|
7
|
+
* {midi-message}[http://github.com/arirusso/midi-message]
|
8
|
+
* {nibbler}[http://github.com/arirusso/nibbler]
|
9
|
+
* {unimidi}[http://github.com/arirusso/unimidi]
|
10
|
+
|
11
|
+
== Install
|
12
|
+
|
13
|
+
gem install midi-eye
|
14
|
+
|
15
|
+
== Usage
|
16
|
+
|
17
|
+
require 'midi-eye'
|
18
|
+
|
19
|
+
The following is an example that takes any note messages received from a unimidi input, transposes them up one octave and then sends them to an output
|
20
|
+
|
21
|
+
First, initialize the MIDI IO ports
|
22
|
+
|
23
|
+
@input = UniMIDI::Input.gets
|
24
|
+
@output = UniMIDI::Output.gets
|
25
|
+
|
26
|
+
Then create a listener for the input port
|
27
|
+
|
28
|
+
transpose = MIDIEye::Listener.new(@input)
|
29
|
+
|
30
|
+
You can bind an event to the listener using Listener#listen_for
|
31
|
+
|
32
|
+
The listener will try to positively match the parameters you pass in to the properties of the messages it receives.
|
33
|
+
|
34
|
+
In this example, we will tell the listener to listen for note on/off messages which are easily identifiable by their class
|
35
|
+
|
36
|
+
You also have the option of leaving out the parameters altogether and including using conditional if/unless/case/etc statements in your callback
|
37
|
+
|
38
|
+
transpose.listen_for(:class => [MIDIMessage::NoteOn, MIDIMessage::NoteOff]) do |event|
|
39
|
+
|
40
|
+
# raise the note value by an octave
|
41
|
+
event[:message].note += 12
|
42
|
+
|
43
|
+
# send the altered note message to the output you chose earlier
|
44
|
+
@output.puts(event[:message])
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
You can bind as many events to a listener as you wish, just keep calling Listener#listen_for
|
49
|
+
|
50
|
+
Once all the events are bound, start the listener
|
51
|
+
|
52
|
+
transpose.run
|
53
|
+
|
54
|
+
A listener can also be run in a background thread by passing in :background => true.
|
55
|
+
|
56
|
+
transpose.run(:background => true)
|
57
|
+
|
58
|
+
transpose.join # join the background thread later
|
59
|
+
|
60
|
+
== Documentation
|
61
|
+
|
62
|
+
* {examples}[http://github.com/arirusso/midi-eye/tree/master/examples]
|
63
|
+
* {rdoc}[http://rdoc.info/gems/midi-eye]
|
64
|
+
|
65
|
+
== Author
|
66
|
+
|
67
|
+
* {Ari Russo}[http://github.com/arirusso] <ari.russo at gmail.com>
|
68
|
+
|
69
|
+
== License
|
70
|
+
|
71
|
+
Apache 2.0, See the file LICENSE
|
72
|
+
|
73
|
+
Copyright (c) 2011 Ari Russo
|
data/lib/midi-eye.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# midi-eye
|
4
|
+
# Transparent MIDI event listener for Ruby
|
5
|
+
# (c)2011 Ari Russo
|
6
|
+
# licensed under the Apache 2.0 License
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'midi-message'
|
10
|
+
require 'nibbler'
|
11
|
+
require 'unimidi'
|
12
|
+
|
13
|
+
require 'midi-eye/listener'
|
14
|
+
require 'midi-eye/unimidi_input'
|
15
|
+
|
16
|
+
module MIDIEye
|
17
|
+
|
18
|
+
VERSION = "0.2.2"
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
module MIDIEye
|
3
|
+
|
4
|
+
class Listener
|
5
|
+
|
6
|
+
attr_reader :events
|
7
|
+
attr_accessor :sources
|
8
|
+
|
9
|
+
@input_types = []
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# a registry of input types
|
13
|
+
attr_reader :input_types
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(input, options = {})
|
17
|
+
@sources = []
|
18
|
+
@event_queue = []
|
19
|
+
@events = []
|
20
|
+
|
21
|
+
add_input(input)
|
22
|
+
end
|
23
|
+
|
24
|
+
# does this listener use <em>input</em>?
|
25
|
+
def uses_input?(input)
|
26
|
+
!@sources.find_all { |source| source.uses?(input) }.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
# add a source
|
30
|
+
# takes a raw input or array of
|
31
|
+
def add_input(input)
|
32
|
+
@sources += [input].flatten.map do |i|
|
33
|
+
klass = self.class.input_types.find { |type| type.is_compatible?(i) }
|
34
|
+
raise "Input class type #{i.class.name} not compatible" if klass.nil?
|
35
|
+
klass.new(i) unless uses_input?(i)
|
36
|
+
end.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
# remove a source
|
40
|
+
# takes a raw input or array of
|
41
|
+
def remove_input(inputs)
|
42
|
+
to_remove = [inputs].flatten
|
43
|
+
to_remove.each do |input|
|
44
|
+
@sources.delete_if { |source| source.uses?(input) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete_event(name)
|
49
|
+
@events.delete_if { |e| e[:listener_name] == name }
|
50
|
+
end
|
51
|
+
|
52
|
+
# start the listener. pass in :background => true to run only in a background thread. returns self
|
53
|
+
def run(options = {})
|
54
|
+
listen!
|
55
|
+
unless options[:background]
|
56
|
+
@listener.join
|
57
|
+
end
|
58
|
+
self
|
59
|
+
end
|
60
|
+
alias_method :start, :run
|
61
|
+
|
62
|
+
# stop the listener. returns self
|
63
|
+
def close
|
64
|
+
@listener.kill unless @listener.nil?
|
65
|
+
@events.clear
|
66
|
+
@sources.clear
|
67
|
+
@event_queue.clear
|
68
|
+
self
|
69
|
+
end
|
70
|
+
alias_method :stop, :close
|
71
|
+
|
72
|
+
# join the listener if it's being run in the background. returns self
|
73
|
+
def join
|
74
|
+
@listener.join
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# add an event to listen for. returns self
|
79
|
+
def listen_for(options = {}, &proc)
|
80
|
+
raise 'listener must have a block' if proc.nil?
|
81
|
+
name = options[:listener_name]
|
82
|
+
options.delete(:listener_name)
|
83
|
+
@events << { :conditions => options, :proc => proc, :listener_name => name }
|
84
|
+
self
|
85
|
+
end
|
86
|
+
alias_method :on_message, :listen_for
|
87
|
+
alias_method :listen, :listen_for
|
88
|
+
|
89
|
+
# poll the input source for new input. this will normally be done by the background thread
|
90
|
+
def poll
|
91
|
+
@sources.each do |input|
|
92
|
+
input.poll do |objs|
|
93
|
+
objs.each do |batch|
|
94
|
+
[batch[:messages]].flatten.each do |single_message|
|
95
|
+
unless single_message.nil?
|
96
|
+
data = { :message => single_message, :timestamp => batch[:timestamp] }
|
97
|
+
@events.each { |name| queue_event(name, data) }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# start the background listener thread
|
108
|
+
def listen!
|
109
|
+
t = 1.0/1000
|
110
|
+
@listener = Thread.fork do
|
111
|
+
Thread.abort_on_exception = true
|
112
|
+
loop do
|
113
|
+
poll
|
114
|
+
trigger_queued_events unless @event_queue.empty?
|
115
|
+
sleep(t)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# trigger all queued events
|
121
|
+
def trigger_queued_events
|
122
|
+
@event_queue.length.times { trigger_event(@event_queue.shift) }
|
123
|
+
end
|
124
|
+
|
125
|
+
# does <em>message</em> meet <em>conditions</em>?
|
126
|
+
def meets_conditions?(conditions, message)
|
127
|
+
!conditions.map do |key, value|
|
128
|
+
message.respond_to?(key) && (value.kind_of?(Array) ? value.include?(message.send(key)) : value.eql?(message.send(key)))
|
129
|
+
end.include?(false)
|
130
|
+
end
|
131
|
+
|
132
|
+
# trigger an event
|
133
|
+
def trigger_event(event)
|
134
|
+
begin
|
135
|
+
action = event[:action]
|
136
|
+
if meets_conditions?(action[:conditions], event[:message][:message]) || action[:conditions].nil?
|
137
|
+
action[:proc].call(event[:message])
|
138
|
+
end
|
139
|
+
rescue
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# add an event to the trigger queue
|
144
|
+
def queue_event(event, message)
|
145
|
+
@event_queue << { :action => event, :message => message }
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
module MIDIEye
|
4
|
+
|
5
|
+
# this class deals with retrieving new messages from
|
6
|
+
# a unimidi input buffer
|
7
|
+
class UniMIDIInput
|
8
|
+
|
9
|
+
attr_reader :device, :pointer
|
10
|
+
|
11
|
+
def initialize(input)
|
12
|
+
@parser = Nibbler.new
|
13
|
+
@pointer = 0
|
14
|
+
@device = input
|
15
|
+
end
|
16
|
+
|
17
|
+
# this grabs new messages from the unimidi buffer
|
18
|
+
def poll(&block)
|
19
|
+
msgs = @device.buffer.slice(@pointer, @device.buffer.length - @pointer)
|
20
|
+
@pointer = @device.buffer.length
|
21
|
+
msgs.each do |raw_msg|
|
22
|
+
unless raw_msg.nil?
|
23
|
+
objs = [@parser.parse(raw_msg[:data], :timestamp => raw_msg[:timestamp])].flatten.compact rescue []
|
24
|
+
yield(objs)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# if <em>input</em> looks like a unimidi input, this returns true
|
30
|
+
def self.is_compatible?(input)
|
31
|
+
input.respond_to?(:gets) && input.respond_to?(:buffer)
|
32
|
+
end
|
33
|
+
|
34
|
+
# if this source was created from <em>input</em>
|
35
|
+
def uses?(input)
|
36
|
+
@device == input
|
37
|
+
end
|
38
|
+
|
39
|
+
# add this class to the Listener class' known input types
|
40
|
+
Listener.input_types << self
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
4
|
+
$LOAD_PATH.unshift dir + '/../lib'
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'midi-eye'
|
8
|
+
|
9
|
+
module TestHelper
|
10
|
+
|
11
|
+
def self.select_devices
|
12
|
+
$test_device ||= {}
|
13
|
+
{ :input => UniMIDI::Input, :output => UniMIDI::Output }.each do |type, klass|
|
14
|
+
$test_device[type] = klass.gets
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def close_all(input, output, listener)
|
19
|
+
listener.close
|
20
|
+
input.clear_buffer
|
21
|
+
input.close
|
22
|
+
output.close
|
23
|
+
sleep(0.1)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
TestHelper.select_devices
|
@@ -0,0 +1,156 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
class ListenerTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
include MIDIEye
|
8
|
+
include MIDIMessage
|
9
|
+
include TestHelper
|
10
|
+
|
11
|
+
def test_rapid_control_change_message
|
12
|
+
sleep(0.2)
|
13
|
+
output = $test_device[:output]
|
14
|
+
input = $test_device[:input]
|
15
|
+
listener = Listener.new(input)
|
16
|
+
@i = 0
|
17
|
+
listener.listen_for(:class => ControlChange) do |event|
|
18
|
+
@i += 1
|
19
|
+
if @i == 5 * 126
|
20
|
+
close_all(input, output, listener)
|
21
|
+
assert_equal(5 * 126, @i)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
listener.start(:background => true)
|
25
|
+
sleep(0.5)
|
26
|
+
5.times do
|
27
|
+
126.times do |i|
|
28
|
+
output.puts(176, 1, i+1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
listener.join
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_control_change_message
|
35
|
+
sleep(0.2)
|
36
|
+
output = $test_device[:output]
|
37
|
+
input = $test_device[:input]
|
38
|
+
listener = Listener.new(input)
|
39
|
+
listener.listen_for(:class => ControlChange) do |event|
|
40
|
+
assert_equal(ControlChange, event[:message].class)
|
41
|
+
assert_equal(1, event[:message].index)
|
42
|
+
assert_equal(35, event[:message].value)
|
43
|
+
assert_equal([176, 1, 35], event[:message].to_bytes)
|
44
|
+
close_all(input, output, listener)
|
45
|
+
end
|
46
|
+
listener.start(:background => true)
|
47
|
+
sleep(0.5)
|
48
|
+
output.puts(176, 1, 35)
|
49
|
+
listener.join
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_delete_event
|
53
|
+
sleep(0.2)
|
54
|
+
output = $test_device[:output]
|
55
|
+
input = $test_device[:input]
|
56
|
+
listener = Listener.new(input)
|
57
|
+
listener.listen_for(:listener_name => :test) do |event|
|
58
|
+
assert_equal(1, listener.events.size)
|
59
|
+
listener.delete_event(:test)
|
60
|
+
assert_equal(0, listener.events.size)
|
61
|
+
close_all(input, output, listener)
|
62
|
+
end
|
63
|
+
listener.start(:background => true)
|
64
|
+
sleep(0.5)
|
65
|
+
output.puts(0x90, 0x70, 0x20)
|
66
|
+
listener.join
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_uses_input
|
70
|
+
sleep(0.2)
|
71
|
+
output = $test_device[:output]
|
72
|
+
input = $test_device[:input]
|
73
|
+
listener = Listener.new(input)
|
74
|
+
assert_equal(true, listener.uses_input?(input))
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_reject_dup_input
|
78
|
+
sleep(0.2)
|
79
|
+
output = $test_device[:output]
|
80
|
+
input = $test_device[:input]
|
81
|
+
listener = Listener.new(input)
|
82
|
+
listener.add_input(input)
|
83
|
+
assert_equal(1, listener.sources.size)
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_remove_input
|
87
|
+
sleep(0.2)
|
88
|
+
output = $test_device[:output]
|
89
|
+
input = $test_device[:input]
|
90
|
+
listener = Listener.new(input)
|
91
|
+
assert_equal(1, listener.sources.size)
|
92
|
+
listener.remove_input(input)
|
93
|
+
assert_equal(0, listener.sources.size)
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_recognize_input_class
|
97
|
+
sleep(0.2)
|
98
|
+
input = $test_device[:input]
|
99
|
+
output = $test_device[:output]
|
100
|
+
listener = Listener.new(input)
|
101
|
+
assert_equal(UniMIDIInput, listener.sources.first.class)
|
102
|
+
close_all(input, output, listener)
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_listen_for_basic
|
106
|
+
sleep(0.2)
|
107
|
+
@i = 0
|
108
|
+
output = $test_device[:output]
|
109
|
+
input = $test_device[:input]
|
110
|
+
listener = Listener.new(input)
|
111
|
+
listener.listen_for do |event|
|
112
|
+
@i += 1
|
113
|
+
assert_equal(1, @i)
|
114
|
+
close_all(input, output, listener)
|
115
|
+
end
|
116
|
+
listener.start(:background => true)
|
117
|
+
sleep(0.5)
|
118
|
+
output.puts(0x90, 0x40, 0x10)
|
119
|
+
listener.join
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_listen_for_sysex
|
123
|
+
sleep(0.2)
|
124
|
+
output = $test_device[:output]
|
125
|
+
input = $test_device[:input]
|
126
|
+
listener = Listener.new(input)
|
127
|
+
listener.listen_for(:class => SystemExclusive::Command) do |event|
|
128
|
+
assert_equal(SystemExclusive::Command, event[:message].class)
|
129
|
+
assert_equal([0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7], event[:message].to_byte_array)
|
130
|
+
close_all(input, output, listener)
|
131
|
+
end
|
132
|
+
listener.start(:background => true)
|
133
|
+
sleep(0.5)
|
134
|
+
output.puts(0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7)
|
135
|
+
listener.join
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_listen_for_note_on
|
139
|
+
sleep(0.2)
|
140
|
+
output = $test_device[:output]
|
141
|
+
input = $test_device[:input]
|
142
|
+
listener = Listener.new(input)
|
143
|
+
listener.listen_for(:class => NoteOff) do |event|
|
144
|
+
assert_equal(NoteOff, event[:message].class)
|
145
|
+
assert_equal(0x50, event[:message].note)
|
146
|
+
assert_equal(0x40, event[:message].velocity)
|
147
|
+
assert_equal([0x80, 0x50, 0x40], event[:message].to_bytes)
|
148
|
+
close_all(input, output, listener)
|
149
|
+
end
|
150
|
+
listener.start(:background => true)
|
151
|
+
sleep(0.5)
|
152
|
+
output.puts(0x80, 0x50, 0x40)
|
153
|
+
listener.join
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: midi-eye
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ari Russo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-05 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: midi-message
|
16
|
+
requirement: &70223965375620 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70223965375620
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: midi-nibbler
|
27
|
+
requirement: &70223965375080 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70223965375080
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: unimidi
|
38
|
+
requirement: &70223965374640 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70223965374640
|
47
|
+
description: MIDI event listener for Ruby
|
48
|
+
email:
|
49
|
+
- ari.russo@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- lib/midi-eye/listener.rb
|
55
|
+
- lib/midi-eye/unimidi_input.rb
|
56
|
+
- lib/midi-eye.rb
|
57
|
+
- test/helper.rb
|
58
|
+
- test/test_listener.rb
|
59
|
+
- LICENSE
|
60
|
+
- README.rdoc
|
61
|
+
homepage: http://github.com/arirusso/midi-eye
|
62
|
+
licenses: []
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 1.3.6
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project: midi-eye
|
81
|
+
rubygems_version: 1.8.6
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: MIDI event listener for Ruby
|
85
|
+
test_files: []
|