patch 0.4.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +13 -0
  3. data/README.md +176 -0
  4. data/bin/patchrb +40 -0
  5. data/lib/patch/config.rb +124 -0
  6. data/lib/patch/em_patch.rb +47 -0
  7. data/lib/patch/hub.rb +68 -0
  8. data/lib/patch/io/midi/action.rb +42 -0
  9. data/lib/patch/io/midi/input.rb +110 -0
  10. data/lib/patch/io/midi/message.rb +112 -0
  11. data/lib/patch/io/midi/output.rb +58 -0
  12. data/lib/patch/io/midi.rb +45 -0
  13. data/lib/patch/io/module.rb +35 -0
  14. data/lib/patch/io/osc/action.rb +43 -0
  15. data/lib/patch/io/osc/client.rb +60 -0
  16. data/lib/patch/io/osc/message.rb +109 -0
  17. data/lib/patch/io/osc/server.rb +159 -0
  18. data/lib/patch/io/osc.rb +43 -0
  19. data/lib/patch/io/websocket/node.rb +103 -0
  20. data/lib/patch/io/websocket/socket.rb +103 -0
  21. data/lib/patch/io/websocket.rb +27 -0
  22. data/lib/patch/io.rb +15 -0
  23. data/lib/patch/log.rb +97 -0
  24. data/lib/patch/message.rb +67 -0
  25. data/lib/patch/node/container.rb +69 -0
  26. data/lib/patch/node/map.rb +71 -0
  27. data/lib/patch/node.rb +10 -0
  28. data/lib/patch/patch.rb +59 -0
  29. data/lib/patch/report.rb +132 -0
  30. data/lib/patch/thread.rb +19 -0
  31. data/lib/patch.rb +42 -0
  32. data/test/config/nodes.yml +16 -0
  33. data/test/config/patches.yml +41 -0
  34. data/test/config_test.rb +216 -0
  35. data/test/helper.rb +20 -0
  36. data/test/hub_test.rb +49 -0
  37. data/test/io/midi/action_test.rb +82 -0
  38. data/test/io/midi/input_test.rb +130 -0
  39. data/test/io/midi/message_test.rb +54 -0
  40. data/test/io/midi/output_test.rb +44 -0
  41. data/test/io/midi_test.rb +94 -0
  42. data/test/io/module_test.rb +21 -0
  43. data/test/io/osc/action_test.rb +76 -0
  44. data/test/io/osc/client_test.rb +49 -0
  45. data/test/io/osc/message_test.rb +53 -0
  46. data/test/io/osc/server_test.rb +116 -0
  47. data/test/io/osc_test.rb +111 -0
  48. data/test/io/websocket/node_test.rb +96 -0
  49. data/test/io/websocket_test.rb +37 -0
  50. data/test/js/logger.js +67 -0
  51. data/test/js/message.js +62 -0
  52. data/test/js/qunit-1.18.0.js +3828 -0
  53. data/test/js/qunit.css +291 -0
  54. data/test/js/test.html +15 -0
  55. data/test/js/websocket.js +12 -0
  56. data/test/log_test.rb +96 -0
  57. data/test/message_test.rb +109 -0
  58. data/test/node/container_test.rb +104 -0
  59. data/test/node/map_test.rb +50 -0
  60. data/test/node_test.rb +14 -0
  61. data/test/patch_test.rb +57 -0
  62. data/test/report_test.rb +37 -0
  63. 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
@@ -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