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 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: []