midi-eye 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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: []