midi-eye 0.2.2
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.
- 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: []
|