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
@@ -0,0 +1,112 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ module MIDI
6
+
7
+ # Convert between MIDI message objects and Patch::Message objects
8
+ module Message
9
+
10
+ extend self
11
+
12
+ # @param [::Patch::Patch] patch
13
+ # @param [Array<::Patch::Message>, ::Patch::Message] messages
14
+ # @return [Array<::MIDIMessage>]
15
+ def to_midi_messages(patch, patch_messages)
16
+ patch_messages = [patch_messages].flatten
17
+ midi_messages = patch_messages.map do |patch_message|
18
+ unless (action = patch.actions.at(patch_message.index)).nil?
19
+ to_midi_message(action, patch_message) unless action[:midi].nil?
20
+ end
21
+ end
22
+ midi_messages.compact
23
+ end
24
+
25
+ # Convert the given MIDI message to Patch::Message objects using the context of the given patch
26
+ # @param [::Patch::Patch] patch
27
+ # @param [Array<MIDIMessage>, MIDIMessage] midi_messages
28
+ # @return [Array<::Patch::Message>]
29
+ def to_patch_messages(patch, midi_messages)
30
+ midi_messages = [midi_messages].flatten
31
+ patch_messages = midi_messages.map do |midi_message|
32
+ unless (action = Action.find_by_index(patch.actions, midi_message.index)).nil?
33
+ index = patch.actions.index(action)
34
+ to_patch_message(action, index, patch.name, midi_message)
35
+ end
36
+ end
37
+ patch_messages.compact
38
+ end
39
+
40
+ private
41
+
42
+ # @param [Hash] action An action to contextualize the conversion
43
+ # @param [::Patch::Message] patch_message
44
+ def get_midi_value_from_action(action, patch_message)
45
+ to = action[:midi][:scale]
46
+ to ||= 0..127
47
+ from = action[:default][:scale] unless action[:default].nil?
48
+ from ||= to
49
+ get_value(patch_message.value, from, to)
50
+ end
51
+
52
+ # @param [Hash] action An action to contextualize the conversion
53
+ # @param [MIDIMessage] midi_message
54
+ def get_patch_values_from_action(action, midi_message)
55
+ from = action[:midi][:scale]
56
+ from ||= 0..127
57
+ to = action[:default][:scale] unless action[:default].nil?
58
+ to ||= from
59
+ get_value(midi_message.value, from, to)
60
+ end
61
+
62
+ # Convert a patch message to a MIDI message
63
+ # @param [Hash] action An action to contextualize the conversion
64
+ # @param [::Patch::Message] patch_message
65
+ # @return [::MIDIMessage::ControlChange, nil]
66
+ def to_midi_message(action, patch_message)
67
+ if !action[:midi].nil?
68
+ index = action[:midi][:index] || patch_message.index
69
+ channel = action[:midi][:channel] || 0
70
+ value = get_midi_value_from_action(action, patch_message)
71
+ MIDIMessage::ControlChange.new(channel, index, value)
72
+ end
73
+ end
74
+
75
+ # Convert a MIDI message to a patch message
76
+ # @param [Hash] action An action to contextualize the conversion
77
+ # @param [Fixnum] index The index of the message
78
+ # @param [Symbol] patch_name A patch name
79
+ # @param [::MIDIMessage::ControlChange] midi_message
80
+ # @return [::Patch::Message, nil]
81
+ def to_patch_message(action, index, patch_name, midi_message)
82
+ if action[:midi][:channel].nil? || action[:midi][:channel] == midi_message.channel
83
+ value = get_patch_values_from_action(action, midi_message)
84
+ properties = {
85
+ :index => index,
86
+ :patch_name => patch_name,
87
+ :value => value
88
+ }
89
+ ::Patch::Message.new(properties)
90
+ end
91
+ end
92
+
93
+ # Translate a value
94
+ # @param [Fixnum] value
95
+ # @param [Range] from
96
+ # @param [Range] to
97
+ # @return [Fixnum]
98
+ def get_value(value, from, to)
99
+ if from == to
100
+ value
101
+ else
102
+ Scale.transform(value).from(from).to(to)
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,58 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ module MIDI
6
+
7
+ # MIDI Output functions
8
+ class Output
9
+
10
+ attr_reader :id, :device
11
+
12
+ # @param [Fixnum] id
13
+ # @param [String, UniMIDI::Output] device
14
+ # @param [Hash] options
15
+ # @option options [Debug] :log
16
+ def initialize(id, device, options = {})
17
+ @log = options[:log]
18
+ @id = id
19
+ @device = get_output(device)
20
+ end
21
+
22
+ # Convert Patch::Message objects to MIDI and send
23
+ # @param [Patch::Patch] patch Context
24
+ # @param [Array<Patch::Message>, Patch::Message] messages Message(s) to send via MIDI
25
+ # @return [Array<MIDIMessage>]
26
+ def puts(patch, patch_messages)
27
+ patch_messages = [patch_messages].flatten
28
+ messages = ::Patch::IO::MIDI::Message.to_midi_messages(patch, patch_messages)
29
+ @device.puts(messages) unless messages.empty?
30
+ messages
31
+ end
32
+
33
+ private
34
+
35
+ # Initialize the output device given a name or device object. If the name of the device is the string "choose",
36
+ # the user is prompted to select an availble MIDI output.
37
+ # @param [String, UniMIDI::Output, nil] device
38
+ # @return [UniMIDI::Output]
39
+ def get_output(device)
40
+ if device.kind_of?(String)
41
+ if device == "choose"
42
+ UniMIDI::Output.gets
43
+ else
44
+ UniMIDI::Output.find_by_name(device)
45
+ end
46
+ elsif device.respond_to?(:puts)
47
+ device.open if device.kind_of?(UniMIDI::Output)
48
+ device
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,45 @@
1
+ # Modules
2
+ require "patch/io/midi/action"
3
+ require "patch/io/midi/message"
4
+ # Classes
5
+ require "patch/io/midi/input"
6
+ require "patch/io/midi/output"
7
+
8
+ module Patch
9
+
10
+ module IO
11
+
12
+ # MIDI IO
13
+ module MIDI
14
+
15
+ # Key that will be used by Patch to identify the module
16
+ KEY = :midi
17
+ extend self
18
+ ::Patch::IO::Module.add(self)
19
+
20
+ # Instantiate a MIDI device based on the given config
21
+ # @param [Hash] config
22
+ # @param [Hash] options
23
+ # @option options [Log] :log
24
+ # @return [MIDI::Input, MIDI::Output]
25
+ def new_from_config(config, options = {})
26
+ klass = get_direction_class(config[:direction])
27
+ klass.new(config[:id], config[:name], :log => options[:log])
28
+ end
29
+
30
+ private
31
+
32
+ # Get the direction class for the given key
33
+ # @param [Symbol] key
34
+ # @return [Class]
35
+ def get_direction_class(key)
36
+ case key.to_sym
37
+ when :input then Input
38
+ when :output then Output
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ # Manage node modules
6
+ class Module
7
+
8
+ class << self
9
+
10
+ # Find an IO module by its key
11
+ # @param [Symbol] key
12
+ # @return [Module]
13
+ def find_by_key(key)
14
+ all.find { |mod| mod::KEY === key }
15
+ end
16
+
17
+ # Add an IO module to the list of modules available to Patch
18
+ # @param [Module] mod
19
+ # @return [Array<Module>]
20
+ def add(mod)
21
+ @modules ||= []
22
+ @modules << mod
23
+ end
24
+
25
+ # Mapping of node modules and names
26
+ # @return [Array<Module>]
27
+ def all
28
+ @modules ||= []
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,43 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ module OSC
6
+
7
+ # Find and identify OSC Actions
8
+ module Action
9
+
10
+ extend self
11
+
12
+ # Is the given action OSC?
13
+ # @param [Hash] action
14
+ # @return [Boolean]
15
+ def osc?(action)
16
+ !action[:osc].nil?
17
+ end
18
+
19
+ # Filter the given actions only to return OSC actions
20
+ # @param [Array<Hash>] actions
21
+ # @return [Array<Hash>]
22
+ def osc_actions(actions)
23
+ actions.select { |action| osc?(action) }
24
+ end
25
+
26
+ # Find an action by its OSC address
27
+ # @param [Array<Hash>] actions
28
+ # @param [String] address
29
+ # @return [Hash]
30
+ def find_by_address(actions, address)
31
+ osc_actions(actions).find do |action|
32
+ regex = Regexp.new(action[:osc][:address])
33
+ address.match(regex)
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,60 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ module OSC
6
+
7
+ # OSC Client
8
+ class Client
9
+
10
+ attr_reader :id
11
+
12
+ # @param [String] host
13
+ # @param [Fixnum] port
14
+ # @param [Hash] options
15
+ # @option options [Fixnum] :id
16
+ # @option options [Log] :log
17
+ def initialize(host, port, options = {})
18
+ @id = options[:id]
19
+ @log = options[:log]
20
+ @client = ::OSC::Client.new(host, port)
21
+ end
22
+
23
+ # Convert message objects to OSC and send
24
+ # @param [Patch::Patch] patch Context
25
+ # @param [Array<Patch::Message, ::OSC::Message>, ::OSC::Message, Patch::Message] messages Message(s) to send
26
+ # @return [Array<::OSC::Message>]]
27
+ def puts(patch, messages)
28
+ osc_messages = get_osc_messages(patch, messages)
29
+ osc_messages.each { |osc_message| @client.send(osc_message) }
30
+ osc_messages
31
+ end
32
+
33
+ private
34
+
35
+ # @param [::Patch::Patch] patch
36
+ # @param [Array<Patch::Message, ::OSC::Message>, ::OSC::Message, Patch::Message] messages Message(s) to send
37
+ # @return [Array<::OSC::Message>]]
38
+ def get_osc_messages(patch, messages)
39
+ messages = [messages].flatten
40
+ messages.map { |message| ensure_osc_message(patch, message) }
41
+ end
42
+
43
+ # @param [::Patch::Patch] patch
44
+ # @param [::OSC::Message, Patch::Message] message
45
+ # @return [::OSC::Message]
46
+ def ensure_osc_message(patch, message)
47
+ unless message.kind_of?(::OSC::Message)
48
+ osc_message = ::Patch::IO::OSC::Message.to_osc_messages(patch, message)
49
+ end
50
+ osc_message ||= message
51
+ osc_message
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,109 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ module OSC
6
+
7
+ # Convert between OSC message and Patch::Message objects
8
+ module Message
9
+
10
+ extend self
11
+
12
+ # Convert a message object to an OSC message given the context of the given patch
13
+ # @param [::Patch::Patch] patch
14
+ # @param [::Patch::Message] message
15
+ # @return [Array<::OSC::Message>]
16
+ def to_osc_messages(patch, patch_message)
17
+ messages = []
18
+ unless (action = get_osc_action(patch.actions, patch_message)).nil?
19
+ messages << get_osc_message(action, patch_message)
20
+ end
21
+ messages
22
+ end
23
+
24
+ # Convert the given OSC message to Patch::Message objects using the context of the given patch
25
+ # @param [::Patch::Patch] patch
26
+ # @param [Object] raw_osc
27
+ # @return [Array<::Patch::Message>]
28
+ def to_patch_messages(patch, raw_osc)
29
+ messages = []
30
+ unless (action = Action.find_by_address(patch.actions, raw_osc.address)).nil?
31
+ messages << get_patch_message(patch, action, raw_osc)
32
+ end
33
+ messages
34
+ end
35
+
36
+ private
37
+
38
+ # @param [::Patch::Patch] patch
39
+ # @param [Hash] action
40
+ # @param [Object] raw_osc
41
+ # @return [::Patch::Message]
42
+ def get_patch_message(patch, action, raw_osc)
43
+ index = patch.actions.index(action)
44
+ values = get_patch_values_from_action(raw_osc, action)
45
+ properties = {
46
+ :index => index,
47
+ :patch_name => patch.name,
48
+ :value => values[0]
49
+ }
50
+ ::Patch::Message.new(properties)
51
+ end
52
+
53
+ # @param [Hash] action
54
+ # @param [::Patch::Message] patch_message
55
+ # @return [::OSC::Message]
56
+ def get_osc_message(action, patch_message)
57
+ address = action[:osc][:address]
58
+ value = get_osc_value_from_action(patch_message.value, action)
59
+ ::OSC::Message.new(address, value)
60
+ end
61
+
62
+ # @param [Object] raw_osc
63
+ # @param [Hash] action
64
+ # @return [Array<Object>]
65
+ def get_patch_values_from_action(raw_osc, action)
66
+ from = action[:osc][:scale]
67
+ to = action[:default][:scale] unless action[:default].nil?
68
+ to ||= from
69
+ raw_osc.to_a.map { |value| get_value(value.to_f, from, to) }
70
+ end
71
+
72
+ # @param [Object] value
73
+ # @param [Hash] action
74
+ # @return [Object]
75
+ def get_osc_value_from_action(value, action)
76
+ to = action[:osc][:scale]
77
+ from = action[:default][:scale] unless action[:default].nil?
78
+ from ||= to
79
+ get_value(value, from, to)
80
+ end
81
+
82
+ # @param [Array<Hash>] actions
83
+ # @param [::Patch::Message] patch_message
84
+ # @return [Hash]
85
+ def get_osc_action(actions, patch_message)
86
+ action = actions.at(patch_message.index)
87
+ action unless action.nil? || action[:osc].nil?
88
+ end
89
+
90
+ # Translate a value
91
+ # @param [Fixnum] value
92
+ # @param [Range] from
93
+ # @param [Range] to
94
+ # @return [Fixnum]
95
+ def get_value(value, from, to)
96
+ if from == to
97
+ value
98
+ else
99
+ Scale.transform(value).from(from).to(to)
100
+ end
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+
107
+ end
108
+
109
+ end
@@ -0,0 +1,159 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ module OSC
6
+
7
+ # OSC server
8
+ class Server
9
+
10
+ attr_reader :id
11
+
12
+ # @param [Fixnum] id
13
+ # @param [Fixnum] port
14
+ # @param [Hash] options
15
+ # @option options [Hash] :echo
16
+ # @option options [Log] :log
17
+ def initialize(id, port, options = {})
18
+ @log = options[:log]
19
+ @server = nil
20
+ @active = false
21
+ @id = id
22
+ @is_failsafe = true
23
+
24
+ configure(port, options)
25
+ end
26
+
27
+ # Is the server active?
28
+ # @return [Boolean]
29
+ def active?
30
+ @active
31
+ end
32
+
33
+ # Start the server
34
+ # @return [Boolean] Whether the server was started
35
+ def start
36
+ @active = true
37
+ @connection = @server.run
38
+ true
39
+ end
40
+
41
+ # Stop the server
42
+ # @return [Boolean]
43
+ def stop
44
+ @active = false
45
+ true
46
+ end
47
+
48
+ # Disable the message handlers
49
+ # @return [Boolean]
50
+ def disable(patch)
51
+ addresses = get_addresses(patch)
52
+ addresses.select { |address| @server.remove_method(address) }.any?
53
+ end
54
+
55
+ # Listen for messages
56
+ # @param [::Patch::Patch] patch The patch to use for context
57
+ # @param [Proc] callback A callback to fire when messages are received
58
+ # @return [Boolean] Whether any actions were configured
59
+ def listen(patch, &callback)
60
+ addresses = get_addresses(patch)
61
+ addresses.select { |address| listen_for(address, patch, &callback) }.any?
62
+ end
63
+
64
+ protected
65
+
66
+ # @param [::Patch::Patch] patch The patch to use for context
67
+ # @return [Array<String, Regexp>]
68
+ def get_addresses(patch)
69
+ actions = ::Patch::IO::OSC::Action.osc_actions(patch.actions)
70
+ actions.map { |action| action[:osc][:address] }.compact.uniq
71
+ end
72
+
73
+ # Handle a new message
74
+ # @param [::Patch::Patch] patch A patch for context
75
+ # @param [OSC::Message] message The OSC message object
76
+ # @param [Proc] callback A callback to fire when a message or messages is received
77
+ # @return [Array<Patch::Message>]
78
+ def handle_message_received(patch, raw_osc, &callback)
79
+ messages = ::Patch::IO::OSC::Message.to_patch_messages(patch, raw_osc)
80
+ echo(patch, raw_osc) if echo?
81
+ # yield to custom behavior
82
+ yield(messages) if block_given?
83
+ messages
84
+ end
85
+
86
+ private
87
+
88
+ # @param [Fixnum] port
89
+ # @param [Hash] options
90
+ # @option options [Hash] :echo
91
+ # @return [Boolean]
92
+ def configure(port, options = {})
93
+ configure_server(port)
94
+ unless options[:echo].nil?
95
+ configure_echo(options[:echo][:host], options[:echo][:port])
96
+ end
97
+ true
98
+ end
99
+
100
+ # Listen for messages on the given address
101
+ # @param [::Patch::Patch] patch The patch to use for context
102
+ # @param [Proc] callback A callback to fire when messages are received
103
+ # @return [Boolean] Whether an action was configured
104
+ def listen_for(address, patch, &callback)
105
+ @server.add_method(address) do |message|
106
+ handle_message_received(patch, message, &callback)
107
+ end
108
+ true
109
+ end
110
+
111
+ # Configure the underlying server
112
+ # @param [Fixnum] port
113
+ # @return [::OSC::Server]
114
+ def configure_server(port)
115
+ @server = ::OSC::EMServer.new(port)
116
+ if @log
117
+ @server.add_method(/.*/) { |message| @log.puts("Received: #{message.address}") }
118
+ end
119
+ @server
120
+ end
121
+
122
+ # Will received messages be echoed back to the client?
123
+ # @return [Boolean]
124
+ def echo?
125
+ !@client.nil?
126
+ end
127
+
128
+ # Echo a message back to the client to update the UI or whatever
129
+ # @param [::Patch::Patch] patch
130
+ # @param [OSC::Message] osc_message
131
+ # @return [Boolean] Whether the echo occurred
132
+ def echo(patch, osc_message)
133
+ begin
134
+ @client.puts(patch, osc_message)
135
+ true
136
+ rescue Exception => exception # failsafe
137
+ @log.exception(exception) if @log
138
+ ::Thread.main.raise(exception) unless @is_failsafe
139
+ false
140
+ end
141
+ end
142
+
143
+ # Configure the echo client
144
+ # @param [String] host
145
+ # @param [Fixnum] echo
146
+ # @param [Hash] options
147
+ # @param [Log] :log
148
+ # @return [::Patch::IO::OSC::Client]
149
+ def configure_echo(host, port, options = {})
150
+ @client = Client.new(host, port, :log => options.fetch(:log, @log))
151
+ end
152
+
153
+ end
154
+
155
+ end
156
+
157
+ end
158
+
159
+ end
@@ -0,0 +1,43 @@
1
+ # Modules
2
+ require "patch/io/osc/action"
3
+ require "patch/io/osc/message"
4
+ # Classes
5
+ require "patch/io/osc/client"
6
+ require "patch/io/osc/server"
7
+
8
+ module Patch
9
+
10
+ module IO
11
+
12
+ # Receive OSC messages and do something with them
13
+ module OSC
14
+
15
+ # Key that will be used by Patch to identify the module
16
+ KEY = :osc
17
+ extend self
18
+ ::Patch::IO::Module.add(self)
19
+
20
+ # Instantiate an OSC server and/or client using the given config
21
+ # @param [Hash] config
22
+ # @param [Hash] options
23
+ # @option options [Action::Container] :actions
24
+ # @option options [Log] :log
25
+ # @return [::Patch::IO::OSC::Server]
26
+ def new_from_config(config, options = {})
27
+ instance_options = {
28
+ :log => options[:log]
29
+ }
30
+ if config[:server].nil?
31
+ unless config[:client].nil?
32
+ instance_options[:id] = config[:id]
33
+ Client.new(config[:client][:host], config[:client][:port], instance_options)
34
+ end
35
+ else
36
+ instance_options[:echo] = config[:client]
37
+ Server.new(config[:id], config[:server][:port], instance_options)
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end