patch 0.4.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +13 -0
- data/README.md +176 -0
- data/bin/patchrb +40 -0
- data/lib/patch/config.rb +124 -0
- data/lib/patch/em_patch.rb +47 -0
- data/lib/patch/hub.rb +68 -0
- data/lib/patch/io/midi/action.rb +42 -0
- data/lib/patch/io/midi/input.rb +110 -0
- data/lib/patch/io/midi/message.rb +112 -0
- data/lib/patch/io/midi/output.rb +58 -0
- data/lib/patch/io/midi.rb +45 -0
- data/lib/patch/io/module.rb +35 -0
- data/lib/patch/io/osc/action.rb +43 -0
- data/lib/patch/io/osc/client.rb +60 -0
- data/lib/patch/io/osc/message.rb +109 -0
- data/lib/patch/io/osc/server.rb +159 -0
- data/lib/patch/io/osc.rb +43 -0
- data/lib/patch/io/websocket/node.rb +103 -0
- data/lib/patch/io/websocket/socket.rb +103 -0
- data/lib/patch/io/websocket.rb +27 -0
- data/lib/patch/io.rb +15 -0
- data/lib/patch/log.rb +97 -0
- data/lib/patch/message.rb +67 -0
- data/lib/patch/node/container.rb +69 -0
- data/lib/patch/node/map.rb +71 -0
- data/lib/patch/node.rb +10 -0
- data/lib/patch/patch.rb +59 -0
- data/lib/patch/report.rb +132 -0
- data/lib/patch/thread.rb +19 -0
- data/lib/patch.rb +42 -0
- data/test/config/nodes.yml +16 -0
- data/test/config/patches.yml +41 -0
- data/test/config_test.rb +216 -0
- data/test/helper.rb +20 -0
- data/test/hub_test.rb +49 -0
- data/test/io/midi/action_test.rb +82 -0
- data/test/io/midi/input_test.rb +130 -0
- data/test/io/midi/message_test.rb +54 -0
- data/test/io/midi/output_test.rb +44 -0
- data/test/io/midi_test.rb +94 -0
- data/test/io/module_test.rb +21 -0
- data/test/io/osc/action_test.rb +76 -0
- data/test/io/osc/client_test.rb +49 -0
- data/test/io/osc/message_test.rb +53 -0
- data/test/io/osc/server_test.rb +116 -0
- data/test/io/osc_test.rb +111 -0
- data/test/io/websocket/node_test.rb +96 -0
- data/test/io/websocket_test.rb +37 -0
- data/test/js/logger.js +67 -0
- data/test/js/message.js +62 -0
- data/test/js/qunit-1.18.0.js +3828 -0
- data/test/js/qunit.css +291 -0
- data/test/js/test.html +15 -0
- data/test/js/websocket.js +12 -0
- data/test/log_test.rb +96 -0
- data/test/message_test.rb +109 -0
- data/test/node/container_test.rb +104 -0
- data/test/node/map_test.rb +50 -0
- data/test/node_test.rb +14 -0
- data/test/patch_test.rb +57 -0
- data/test/report_test.rb +37 -0
- metadata +320 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 222f130c3489c502fab026e74c0d669a9a84fb43
|
4
|
+
data.tar.gz: a2a352a10308ee799ea3053606b29229cc523376
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0d51314a03aa34d4bdf3ddb9fe42af9315c810bdf72b0d43e636b854e0aa267211a826ebb26f10024a19bf80713d6742999d724a86df10960eb49a5552d9e7d0
|
7
|
+
data.tar.gz: ba72546ab948910ffe80cf20aa68c64274642fa8e5be8672d522fb4aac5eae4462f2c58af3f5b1130b7e93f267c452bc092295ae3a8fe5bbe4f30b97c9b7040b
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2014-2015 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.md
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# Patch
|
2
|
+
|
3
|
+
Patch is a universal hub for controller messages
|
4
|
+
|
5
|
+
These message protocols are currently supported
|
6
|
+
|
7
|
+
* [MIDI](http://en.wikipedia.org/wiki/MIDI)
|
8
|
+
* [OSC](http://en.wikipedia.org/wiki/Open_Sound_Control)
|
9
|
+
* JSON over [Websocket](http://en.wikipedia.org/wiki/WebSocket)
|
10
|
+
|
11
|
+
Other possibilities:
|
12
|
+
|
13
|
+
* [HTML5 Server-Sent Events](http://www.w3schools.com/html/html5_serversentevents.asp)
|
14
|
+
* HTTP
|
15
|
+
* [JSON RPC 2.0](http://en.wikipedia.org/wiki/JSON-RPC)
|
16
|
+
|
17
|
+
Patch receives messages in these formats and converts them to a generic `Patch::Message` object.
|
18
|
+
|
19
|
+
At that point, these generic objects can be converted to another one of these formats and sent accordingly.
|
20
|
+
|
21
|
+
For example:
|
22
|
+
|
23
|
+
Patch can receive messages from a MIDI drum machine and relay them to a web API. The web API can then respond with JSON which Patch converts to MIDI and sends back to the drum machine.
|
24
|
+
|
25
|
+
While this particular example can probably be accomplished using other utilities or scripts, Patch makes it possible to receive, merge, split and send different types of messages like this freely in one session.
|
26
|
+
|
27
|
+
By doing so, Patch creates an interface that functions as though devices like that with different control messaging protocols had been designed to control each other.
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
### Installation
|
32
|
+
|
33
|
+
Patch is packaged as a Ruby gem.
|
34
|
+
|
35
|
+
It can be installed by using `gem install patch` on the command line or by adding `gem "patch"` to a project's Gemfile.
|
36
|
+
|
37
|
+
### Configuration
|
38
|
+
|
39
|
+
Configuring Patch can be done two ways:
|
40
|
+
|
41
|
+
* [In Ruby code](#in-ruby)
|
42
|
+
* [Using configuration files](#using-configuration-files)
|
43
|
+
|
44
|
+
### In Ruby
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require "patch"
|
48
|
+
```
|
49
|
+
|
50
|
+
A *node* is a single source and/or destination of control messages. Here, we define three nodes:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
websocket = Patch::IO::Websocket.new(1, "localhost", 9006)
|
54
|
+
|
55
|
+
midi = Patch::IO::MIDI::Input.new(2, "Apple Inc. IAC Driver")
|
56
|
+
|
57
|
+
osc = Patch::IO::OSC::Server.new(3, 8000)
|
58
|
+
```
|
59
|
+
|
60
|
+
A *node map* defines where messages should flow to and from.
|
61
|
+
|
62
|
+
In this example, when our MIDI and OSC nodes receive messages, those messages are then echoed to the Websocket node.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
map = { [midi, osc] => websocket }
|
66
|
+
```
|
67
|
+
|
68
|
+
The message protocols used by Patch have no implicit way to translate between each other. Therefore *actions* are used to describe how to do that.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
|
72
|
+
action = {
|
73
|
+
:name => "Zoom",
|
74
|
+
:key => "zoom",
|
75
|
+
:default => {
|
76
|
+
:scale => 10..200.0
|
77
|
+
},
|
78
|
+
:midi => {
|
79
|
+
:channel => 0,
|
80
|
+
:index => 1
|
81
|
+
},
|
82
|
+
:osc => {
|
83
|
+
:address=>"/1/rotaryA",
|
84
|
+
:scale => 0..1.0
|
85
|
+
}
|
86
|
+
}
|
87
|
+
```
|
88
|
+
|
89
|
+
Given these example actions,
|
90
|
+
|
91
|
+
1. When a MIDI control change message is received on channel 0 with index 1, send a JSON over websocket message with the key `zoom`. The value of the MIDI message should be scaled to a float between 10 and 200.
|
92
|
+
|
93
|
+
2. When an OSC message is received for address `/1/rotaryA`, send a JSON over websocket message with the key `zoom`. Scale the OSC value, which will be a float between 0 and 1 to a float between 10 and 200.
|
94
|
+
|
95
|
+
Now start Patch listening for messages:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
patch = Patch::Patch.new(:simple, map, action)
|
99
|
+
|
100
|
+
hub = Patch::Hub.new(:patch => patch)
|
101
|
+
hub.listen
|
102
|
+
```
|
103
|
+
|
104
|
+
The full example can be found [here](https://github.com/arirusso/patch/blob/master/examples/simple/simple.rb).
|
105
|
+
|
106
|
+
### Using Configuration Files
|
107
|
+
|
108
|
+
It's also possible to configure Patch using configuration files. To do that, two files are necessary:
|
109
|
+
|
110
|
+
* [nodes.yml](#nodesyml)
|
111
|
+
* [patches.yml](#patchesyml)
|
112
|
+
|
113
|
+
The configuration in these example files is similar to the one in Ruby above.
|
114
|
+
|
115
|
+
##### nodes.yml
|
116
|
+
|
117
|
+
`nodes.yml` describes what nodes to use and how to configure them.
|
118
|
+
|
119
|
+
In addition, each node is given an ID number for reference later.
|
120
|
+
|
121
|
+
```yaml
|
122
|
+
:nodes:
|
123
|
+
- :id: 1
|
124
|
+
:type: websocket
|
125
|
+
:host: localhost
|
126
|
+
:port: 9006
|
127
|
+
- :id: 2
|
128
|
+
:type: midi
|
129
|
+
:direction: input
|
130
|
+
:name: Apple Inc. IAC Driver
|
131
|
+
- :id: 3
|
132
|
+
:type: osc
|
133
|
+
:server:
|
134
|
+
:port: 8000
|
135
|
+
:client:
|
136
|
+
:host: 192.168.1.136
|
137
|
+
:port: 9000
|
138
|
+
```
|
139
|
+
|
140
|
+
##### patches.yml
|
141
|
+
|
142
|
+
Node maps and actions are specified in the second configuration file, `patches.yml`.
|
143
|
+
|
144
|
+
```yaml
|
145
|
+
:patches:
|
146
|
+
:simple:
|
147
|
+
:node_map:
|
148
|
+
[2, 3]: 1
|
149
|
+
:actions:
|
150
|
+
- :name: Zoom
|
151
|
+
:key: zoom
|
152
|
+
:default:
|
153
|
+
:scale: !ruby/range 10..200.0
|
154
|
+
:midi:
|
155
|
+
:channel: 0
|
156
|
+
:index: 1
|
157
|
+
:osc:
|
158
|
+
:address: /1/rotaryA
|
159
|
+
:scale: !ruby/range 0..1.0
|
160
|
+
|
161
|
+
```
|
162
|
+
|
163
|
+
The `patches.yml` file can contain any number of patches, they will all be run concurrently.
|
164
|
+
|
165
|
+
### Command Line
|
166
|
+
|
167
|
+
You can run Patch at the command line by executing `patchrb nodes.yml patches.yml`.
|
168
|
+
|
169
|
+
## Author
|
170
|
+
|
171
|
+
[Ari Russo](http://github.com/arirusso) <ari.russo at gmail.com>
|
172
|
+
|
173
|
+
## License
|
174
|
+
|
175
|
+
This version under Apache 2.0, See the file LICENSE
|
176
|
+
Copyright (c) 2014-2015 [Ari Russo](http://arirusso.com)
|
data/bin/patchrb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
3
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require "optparse"
|
6
|
+
require "patch"
|
7
|
+
|
8
|
+
nodes_config_path, patches_config_path = *ARGV
|
9
|
+
|
10
|
+
options = {}
|
11
|
+
|
12
|
+
args = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: patch [nodes config file] [patches config file] [options]"
|
14
|
+
|
15
|
+
opts.on("-l", "--log [FILE]", "Enable logging with optional file name") do |file|
|
16
|
+
options[:log] = file || true
|
17
|
+
end
|
18
|
+
opts.on("-q", "--quiet", "Quiet mode for no console output") do
|
19
|
+
options[:quiet] = true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
args.parse!
|
24
|
+
|
25
|
+
if nodes_config_path.nil? || !File.exist?(nodes_config_path) || patches_config_path.nil? || !File.exist?(patches_config_path)
|
26
|
+
puts args.help
|
27
|
+
exit 0
|
28
|
+
else
|
29
|
+
nodes_config_file = File.new(nodes_config_path)
|
30
|
+
patches_config_file = File.new(patches_config_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
if !!options[:log]
|
34
|
+
log_filename = options[:log] === true ? "patch_#{Time.now.to_i}.log" : options[:log]
|
35
|
+
log_file = File.open(log_filename, "w")
|
36
|
+
end
|
37
|
+
|
38
|
+
hub = Patch::Config.to_hub(nodes_config_file, :log => log_file, :patches => patches_config_file)
|
39
|
+
Patch::Report.print(hub) unless options[:quiet]
|
40
|
+
hub.listen
|
data/lib/patch/config.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
module Patch
|
2
|
+
|
3
|
+
# Deal with config files, hashes
|
4
|
+
module Config
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
# @param [Hash] nodes_config
|
9
|
+
# @param [Hash] options
|
10
|
+
# @option options [IO] :log
|
11
|
+
# @return [Hub]
|
12
|
+
def to_hub(nodes_config, options = {})
|
13
|
+
log = Log.new(options.fetch(:log, $>)) unless options[:log].nil?
|
14
|
+
nodes = to_nodes(nodes_config, :log => log)
|
15
|
+
patches = to_patches(nodes, options[:patches]) unless options[:patches].nil?
|
16
|
+
Hub.new(:log => log, :patches => patches)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Instantiate patch objects from the given patch config file, filename or hash
|
20
|
+
# @param [NodeContainer] nodes
|
21
|
+
# @param [File, Hash, String] config
|
22
|
+
# @return [Array<Patch>]
|
23
|
+
def to_patches(nodes, config)
|
24
|
+
config = ensure_hash(config)
|
25
|
+
patches = []
|
26
|
+
config[:patches].each do |name, patch_config|
|
27
|
+
patches << to_patch(name, nodes, patch_config)
|
28
|
+
end
|
29
|
+
patches
|
30
|
+
end
|
31
|
+
|
32
|
+
# Instantiate node objects from the given node config or config file
|
33
|
+
# @param [File, Hash, String] config
|
34
|
+
# @param [Hash] options
|
35
|
+
# @option options [Log] :log
|
36
|
+
# @return [Node::Container]
|
37
|
+
def to_nodes(config, options = {})
|
38
|
+
config = ensure_hash(config)
|
39
|
+
node_array = config[:nodes].map { |node_config| to_node(node_config, options) }
|
40
|
+
Node::Container.new(node_array)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Instantiate Node::Map objects given a map config hash
|
44
|
+
# @param [NodeContainer] nodes
|
45
|
+
# @param [Hash] config
|
46
|
+
# @return [Array<Node::Map>]
|
47
|
+
def to_node_maps(nodes, config)
|
48
|
+
config.map { |from, to| get_node_map(nodes, from, to) }
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# @param [NodeContainer] nodes
|
54
|
+
# @param [Array<Object>, Object] from (id)
|
55
|
+
# @param [Array<Object>, Object] to (id)
|
56
|
+
# @return [Node::Map]
|
57
|
+
def get_node_map(nodes, from, to)
|
58
|
+
from_nodes = get_nodes(nodes, from)
|
59
|
+
to_nodes = get_nodes(nodes, to)
|
60
|
+
Node::Map.new(from_nodes, to_nodes)
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param [NodeContainer] nodes
|
64
|
+
# @param [Array<Object>, Object] from (id)
|
65
|
+
# @return [Array<Patch::IO::MIDI, Patch::IO::OSC, Patch::IO::Websocket>]
|
66
|
+
def get_nodes(nodes, ids)
|
67
|
+
ids = [ids].flatten
|
68
|
+
ids.map { |id| nodes.find_by_id(id) }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Instantiate a node from the given node config
|
72
|
+
# @param [Hash] config
|
73
|
+
# @param [Hash] options
|
74
|
+
# @option options [Log] :log
|
75
|
+
# @return [Patch::IO::MIDI, Patch::IO::OSC, Patch::IO::Websocket]
|
76
|
+
def to_node(node_config, options = {})
|
77
|
+
module_key = node_config[:type].to_sym
|
78
|
+
mod = IO::Module.find_by_key(module_key)
|
79
|
+
mod.new_from_config(node_config, :log => options[:log])
|
80
|
+
end
|
81
|
+
|
82
|
+
# Instantiate a patch object for the given config hash
|
83
|
+
# @param [Symbol, String] name
|
84
|
+
# @param [NodeContainer] nodes
|
85
|
+
# @param [Hash] config
|
86
|
+
# @return [Patch]
|
87
|
+
def to_patch(name, nodes, config)
|
88
|
+
actions = config[:actions] || config[:action]
|
89
|
+
maps = to_node_maps(nodes, config[:node_map])
|
90
|
+
Patch.new(name, maps, actions)
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param [File, Hash, String] object
|
94
|
+
# @return [File, String]
|
95
|
+
def get_config_file(object)
|
96
|
+
case object
|
97
|
+
when File, String then object
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Given a file name, file or hash, populate a config hash and freeze it
|
102
|
+
# @param [File, Hash, String] object
|
103
|
+
# @return [Hash]
|
104
|
+
def ensure_hash(object)
|
105
|
+
hash = if (config_file = get_config_file(object)).nil?
|
106
|
+
object
|
107
|
+
else
|
108
|
+
YAML.load_file(config_file)
|
109
|
+
end
|
110
|
+
deep_freeze_config(hash) unless hash.nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param [Enumerable] container
|
114
|
+
# @return [Enumerable]
|
115
|
+
def deep_freeze_config(container)
|
116
|
+
container.freeze
|
117
|
+
values = container.respond_to?(:values) ? container.values : container
|
118
|
+
enums = values.select { |item| item.kind_of?(Array) || item.kind_of?(Hash) }
|
119
|
+
enums.each { |item| deep_freeze_config(item) }
|
120
|
+
container
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# These patches will allow starting both an OSC and Websocket server concurrently
|
2
|
+
#
|
3
|
+
module OSC
|
4
|
+
class EMServer
|
5
|
+
|
6
|
+
def run
|
7
|
+
open
|
8
|
+
end
|
9
|
+
|
10
|
+
def open
|
11
|
+
EM::open_datagram_socket("0.0.0.0", @port, Connection)
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove_method(address_pattern)
|
15
|
+
matcher = AddressPattern.new( address_pattern )
|
16
|
+
|
17
|
+
@tuples.delete_if { |pattern, proc| pattern == matcher }
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module EventMachine
|
24
|
+
module WebSocket
|
25
|
+
def self.start(options, &blk)
|
26
|
+
#EM.epoll
|
27
|
+
#EM.run {
|
28
|
+
trap("TERM") { stop }
|
29
|
+
trap("INT") { stop }
|
30
|
+
|
31
|
+
run(options, &blk)
|
32
|
+
#}
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.run(options)
|
36
|
+
host, port = options.values_at(:host, :port)
|
37
|
+
EM.start_server(host, port, Connection, options) do |c|
|
38
|
+
::Thread.current.abort_on_exception = true
|
39
|
+
begin
|
40
|
+
yield c
|
41
|
+
rescue Exception => exception
|
42
|
+
::Thread.main.raise(exception)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/patch/hub.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
module Patch
|
2
|
+
|
3
|
+
# The main application object
|
4
|
+
class Hub
|
5
|
+
|
6
|
+
attr_reader :log, :patches
|
7
|
+
|
8
|
+
# @param [Hash] options
|
9
|
+
# @option options [IO] :log
|
10
|
+
# @option options [Array<Patch>] :patches
|
11
|
+
def initialize(options = {})
|
12
|
+
@log = Log.new(options[:log]) unless options[:log].nil?
|
13
|
+
populate_patches(options[:patches] || options[:patch])
|
14
|
+
end
|
15
|
+
|
16
|
+
# Collected IP addresses for the nodes
|
17
|
+
# @return [Array<String>]
|
18
|
+
def ips
|
19
|
+
regex = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/
|
20
|
+
all_ips = Socket.ip_address_list.map(&:inspect_sockaddr)
|
21
|
+
all_ips.select { |ip| !!ip.match(regex) }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Start the hub
|
25
|
+
# @param [Hash] options
|
26
|
+
# @option options [Boolean] :background Run in a background thread (default: false)
|
27
|
+
# @return [Hub] self
|
28
|
+
def listen(options = {})
|
29
|
+
begin
|
30
|
+
enable_nodes
|
31
|
+
@thread.join unless !!options[:background]
|
32
|
+
self
|
33
|
+
rescue SystemExit, Interrupt => exception
|
34
|
+
exit 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# All of the nodes used by the patches
|
39
|
+
# @return [Node::Container]
|
40
|
+
def nodes
|
41
|
+
nodes = @patches.map { |patch| patch.maps.map(&:nodes) }.flatten.compact.uniq
|
42
|
+
Node::Container.new(nodes)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Enable the nodes
|
48
|
+
# @return [Thread]
|
49
|
+
def enable_nodes
|
50
|
+
@thread = ::Patch::Thread.new do
|
51
|
+
EM.epoll
|
52
|
+
EM.run {
|
53
|
+
@patches.each(&:enable)
|
54
|
+
nodes.enable
|
55
|
+
}
|
56
|
+
!nodes.empty?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Populate the patches given various arg formats
|
61
|
+
# @param [Array<Patch>, Patch] patches
|
62
|
+
# @return [Array<Patch>]
|
63
|
+
def populate_patches(patches)
|
64
|
+
@patches = [patches].flatten.compact
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Patch
|
2
|
+
|
3
|
+
module IO
|
4
|
+
|
5
|
+
module MIDI
|
6
|
+
|
7
|
+
# Find and identify MIDI Actions
|
8
|
+
module Action
|
9
|
+
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Is the given action MIDI?
|
13
|
+
# @param [Hash] action
|
14
|
+
# @return [Boolean]
|
15
|
+
def midi?(action)
|
16
|
+
!action[:midi].nil? && !action[:midi][:index].nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Filter the given actions only to return MIDI actions
|
20
|
+
# @param [Array<Hash>] actions
|
21
|
+
# @return [Array<Hash>]
|
22
|
+
def midi_actions(actions)
|
23
|
+
actions.select { |action| midi?(action) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Find an action in the given patch for the given cc index
|
27
|
+
# @param [Array<Hash>] actions
|
28
|
+
# @param [Fixnum] index
|
29
|
+
# @return [Hash]
|
30
|
+
def find_by_index(actions, index)
|
31
|
+
midi_actions(actions).find do |action|
|
32
|
+
action[:midi][:index] == index
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Patch
|
2
|
+
|
3
|
+
module IO
|
4
|
+
|
5
|
+
module MIDI
|
6
|
+
|
7
|
+
# MIDI Input functions
|
8
|
+
class Input
|
9
|
+
|
10
|
+
attr_reader :device, :id, :listener
|
11
|
+
|
12
|
+
# @param [Fixnum] id
|
13
|
+
# @param [String, UniMIDI::Input] device
|
14
|
+
# @param [Hash] options
|
15
|
+
# @option options [Log] :log
|
16
|
+
def initialize(id, device, options = {})
|
17
|
+
@log = options[:log]
|
18
|
+
@id = id
|
19
|
+
@device = get_input(device)
|
20
|
+
@listener = MIDIEye::Listener.new(@device) unless @device.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Start listening for MIDI messages
|
24
|
+
# @return [Boolean] Whether the listener was started
|
25
|
+
def start
|
26
|
+
if !@listener.nil?
|
27
|
+
@listener.run(:background => true)
|
28
|
+
true
|
29
|
+
else
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Is the input active?
|
35
|
+
# @return [Boolean]
|
36
|
+
def active?
|
37
|
+
@listener.running?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Stop the MIDI listener
|
41
|
+
# @return [Boolean]
|
42
|
+
def stop
|
43
|
+
if !@listener.nil?
|
44
|
+
@listener.stop
|
45
|
+
true
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Clear message handlers
|
52
|
+
# @return [Boolean]
|
53
|
+
def disable(patch)
|
54
|
+
@listener.event.clear
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
# Specify a mpatch context and handler callback to use when messages are received
|
59
|
+
# @param [::Patch::Patch] patch
|
60
|
+
# @param [Proc] callback
|
61
|
+
# @return [Boolean] Whether adding the callback was successful
|
62
|
+
def listen(patch, &callback)
|
63
|
+
if !@listener.nil?
|
64
|
+
@listener.listen_for(:class => [MIDIMessage::ControlChange]) do |event|
|
65
|
+
handle_event_received(patch, event, &callback)
|
66
|
+
end
|
67
|
+
true
|
68
|
+
else
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Handle receiving new MIDI messages from the input
|
76
|
+
# @param [::Patch::Patch] patch
|
77
|
+
# @param [Hash] event
|
78
|
+
# @param [Proc] callback
|
79
|
+
# @return [Array<::Patch::Message>]
|
80
|
+
def handle_event_received(patch, event, &callback)
|
81
|
+
messages = event[:message]
|
82
|
+
patch_messages = ::Patch::IO::MIDI::Message.to_patch_messages(patch, messages)
|
83
|
+
yield(patch_messages) if block_given?
|
84
|
+
patch_messages
|
85
|
+
end
|
86
|
+
|
87
|
+
# Initialize the input device using the given string or input. If the device is the string "choose",
|
88
|
+
# the user is prompted to select an available MIDI input.
|
89
|
+
# @param [String, UniMIDI::Input, nil] device
|
90
|
+
# @return [UniMIDI::Input, nil]
|
91
|
+
def get_input(device)
|
92
|
+
if device.kind_of?(String)
|
93
|
+
if device == "choose"
|
94
|
+
UniMIDI::Input.gets
|
95
|
+
else
|
96
|
+
UniMIDI::Input.find_by_name(device)
|
97
|
+
end
|
98
|
+
elsif device.respond_to?(:gets)
|
99
|
+
device.open if device.kind_of?(UniMIDI::Input)
|
100
|
+
device
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|