mrjoy-launchpad 0.4.0

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.
@@ -0,0 +1,37 @@
1
+ module Launchpad
2
+
3
+ # Generic launchpad error.
4
+ class LaunchpadError < StandardError; end
5
+
6
+ # Error raised when the MIDI device specified doesn't exist.
7
+ class NoSuchDeviceError < LaunchpadError; end
8
+
9
+ # Error raised when the MIDI device specified is busy.
10
+ class DeviceBusyError < LaunchpadError; end
11
+
12
+ # Error raised when an input has been requested, although
13
+ # launchpad has been initialized without input.
14
+ class NoInputAllowedError < LaunchpadError; end
15
+
16
+ # Error raised when an output has been requested, although
17
+ # launchpad has been initialized without output.
18
+ class NoOutputAllowedError < LaunchpadError; end
19
+
20
+ # Error raised when <tt>x/y</tt> coordinates outside of the grid
21
+ # or none were specified.
22
+ class NoValidGridCoordinatesError < LaunchpadError; end
23
+
24
+ # Error raised when wrong brightness was specified.
25
+ class NoValidBrightnessError < LaunchpadError; end
26
+
27
+ # Error raised when anything fails while communicating
28
+ # with the launchpad.
29
+ class CommunicationError < LaunchpadError
30
+ attr_accessor :source
31
+ def initialize(e)
32
+ super(e.portmidi_error)
33
+ self.source = e
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,336 @@
1
+ require 'launchpad/device'
2
+ require 'launchpad/logging'
3
+
4
+ module Launchpad
5
+
6
+ # This class provides advanced interaction features.
7
+ #
8
+ # Example:
9
+ #
10
+ # require 'launchpad'
11
+ #
12
+ # interaction = Launchpad::Interaction.new
13
+ # interaction.response_to(:grid, :down) do |interaction, action|
14
+ # interaction.device.change(:grid, action.merge(:red => :high))
15
+ # end
16
+ # interaction.response_to(:mixer, :down) do |interaction, action|
17
+ # interaction.stop
18
+ # end
19
+ #
20
+ # interaction.start
21
+ class Interaction
22
+
23
+ include Logging
24
+
25
+ # Returns the Launchpad::Device the Launchpad::Interaction acts on.
26
+ attr_reader :device
27
+
28
+ # Returns whether the Launchpad::Interaction is active or not.
29
+ attr_reader :active
30
+
31
+ # Initializes the interaction.
32
+ #
33
+ # Optional options hash:
34
+ #
35
+ # [<tt>:device</tt>] Launchpad::Device to act on,
36
+ # optional, <tt>:input_device_id/:output_device_id</tt> will be used if omitted
37
+ # [<tt>:input_device_id</tt>] ID of the MIDI input device to use,
38
+ # optional, <tt>:device_name</tt> will be used if omitted
39
+ # [<tt>:output_device_id</tt>] ID of the MIDI output device to use,
40
+ # optional, <tt>:device_name</tt> will be used if omitted
41
+ # [<tt>:device_name</tt>] Name of the MIDI device to use,
42
+ # optional, defaults to "Launchpad"
43
+ # [<tt>:latency</tt>] delay (in s, fractions allowed) between MIDI pulls,
44
+ # optional, defaults to 0.001 (1ms)
45
+ # [<tt>:logger</tt>] [Logger] to be used by this interaction instance, can be changed afterwards
46
+ #
47
+ # Errors raised:
48
+ #
49
+ # [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
50
+ # [Launchpad::DeviceBusyError] when device with ID or name specified is busy
51
+ def initialize(opts = nil)
52
+ opts ||= {}
53
+
54
+ self.logger = opts[:logger]
55
+ logger.debug "initializing Launchpad::Interaction##{object_id} with #{opts.inspect}"
56
+
57
+ @device = opts[:device]
58
+ @use_threads = opts[:use_threads] || true
59
+ @device ||= Device.new(opts.merge(input: true,
60
+ output: true,
61
+ logger: opts[:logger]))
62
+ @latency = (opts[:latency] || 0.001).to_f.abs
63
+ @active = false
64
+
65
+ @action_threads = ThreadGroup.new
66
+ end
67
+
68
+ # Sets the logger to be used by the current instance and the device.
69
+ #
70
+ # [+logger+] the [Logger] instance
71
+ def logger=(logger)
72
+ @logger = logger
73
+ @device.logger = logger if @device
74
+ end
75
+
76
+ # Closes the interaction's device - nothing can be done with the interaction/device afterwards.
77
+ #
78
+ # Errors raised:
79
+ #
80
+ # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
81
+ # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the
82
+ def close
83
+ logger.debug "closing Launchpad::Interaction##{object_id}"
84
+ stop
85
+ @device.close
86
+ end
87
+
88
+ # Determines whether this interaction's device has been closed.
89
+ def closed?
90
+ @device.closed?
91
+ end
92
+
93
+ # Starts interacting with the launchpad. Resets the device when
94
+ # the interaction was properly stopped via stop or close.
95
+ #
96
+ # Optional options hash:
97
+ #
98
+ # [<tt>:detached</tt>] <tt>true/false</tt>,
99
+ # whether to detach the interaction, method is blocking when +false+,
100
+ # optional, defaults to +false+
101
+ #
102
+ # Errors raised:
103
+ #
104
+ # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
105
+ # [Launchpad::NoOutputAllowedError] when output is not enabled on the interaction's device
106
+ # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the launchpad
107
+ def start(opts = nil)
108
+ logger.debug "starting Launchpad::Interaction##{object_id}"
109
+
110
+ opts = {
111
+ :detached => false
112
+ }.merge(opts || {})
113
+
114
+ @active = true
115
+
116
+ @reader_thread ||= Thread.new do
117
+ begin
118
+ while @active do
119
+ @device.read_pending_actions.each do |action|
120
+ if @use_threads
121
+ action_thread = Thread.new(action) do |act|
122
+ respond_to_action(act)
123
+ end
124
+ @action_threads.add(action_thread)
125
+ else
126
+ respond_to_action(action)
127
+ end
128
+ end
129
+ sleep @latency# if @latency > 0.0
130
+ end
131
+ rescue Portmidi::DeviceError => e
132
+ logger.fatal "could not read from device, stopping to read actions"
133
+ raise CommunicationError.new(e)
134
+ rescue Exception => e
135
+ logger.fatal "error causing action reading to stop: #{e.inspect}"
136
+ raise e
137
+ ensure
138
+ @device.reset
139
+ end
140
+ end
141
+ @reader_thread.join unless opts[:detached]
142
+ end
143
+
144
+ # Stops interacting with the launchpad.
145
+ #
146
+ # Errors raised:
147
+ #
148
+ # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
149
+ # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the
150
+ def stop
151
+ logger.debug "stopping Launchpad::Interaction##{object_id}"
152
+ @active = false
153
+ if @reader_thread
154
+ # run (resume from sleep) and wait for @reader_thread to end
155
+ @reader_thread.run if @reader_thread.alive?
156
+ @reader_thread.join
157
+ @reader_thread = nil
158
+ end
159
+ ensure
160
+ @action_threads.list.each do |thread|
161
+ begin
162
+ thread.kill
163
+ thread.join
164
+ rescue Exception => e
165
+ logger.error "error when killing action thread: #{e.inspect}"
166
+ end
167
+ end
168
+ nil
169
+ end
170
+
171
+ # Registers a response to one or more actions.
172
+ #
173
+ # Parameters (see Launchpad for values):
174
+ #
175
+ # [+types+] one or an array of button types to respond to,
176
+ # additional value <tt>:all</tt> for all buttons
177
+ # [+state+] button state to respond to,
178
+ # additional value <tt>:both</tt>
179
+ #
180
+ # Optional options hash:
181
+ #
182
+ # [<tt>:exclusive</tt>] <tt>true/false</tt>,
183
+ # whether to deregister all other responses to the specified actions,
184
+ # optional, defaults to +false+
185
+ # [<tt>:x</tt>] x coordinate(s), can contain arrays and ranges, when specified
186
+ # without y coordinate, it's interpreted as a whole column
187
+ # [<tt>:y</tt>] y coordinate(s), can contain arrays and ranges, when specified
188
+ # without x coordinate, it's interpreted as a whole row
189
+ #
190
+ # Takes a block which will be called when an action matching the parameters occurs.
191
+ #
192
+ # Block parameters:
193
+ #
194
+ # [+interaction+] the interaction object that received the action
195
+ # [+action+] the action received from Launchpad::Device.read_pending_actions
196
+ def response_to(types = :all, state = :both, opts = nil, &block)
197
+ logger.debug "setting response to #{types.inspect} for state #{state.inspect} with #{opts.inspect}"
198
+ types = Array(types)
199
+ opts ||= {}
200
+ no_response_to(types, state) if opts[:exclusive] == true
201
+ Array(state == :both ? %w(down up) : state).each do |st|
202
+ types.each do |type|
203
+ combined_types(type, opts).each do |combined_type|
204
+ responses[combined_type][st.to_sym] << block
205
+ end
206
+ end
207
+ end
208
+ nil
209
+ end
210
+
211
+ # Deregisters all responses to one or more actions.
212
+ #
213
+ # Parameters (see Launchpad for values):
214
+ #
215
+ # [+types+] one or an array of button types to respond to,
216
+ # additional value <tt>:all</tt> for actions on all buttons
217
+ # (but not meaning "all responses"),
218
+ # optional, defaults to +nil+, meaning "all responses"
219
+ # [+state+] button state to respond to,
220
+ # additional value <tt>:both</tt>
221
+ #
222
+ # Optional options hash:
223
+ #
224
+ # [<tt>:x</tt>] x coordinate(s), can contain arrays and ranges, when specified
225
+ # without y coordinate, it's interpreted as a whole column
226
+ # [<tt>:y</tt>] y coordinate(s), can contain arrays and ranges, when specified
227
+ # without x coordinate, it's interpreted as a whole row
228
+ def no_response_to(types = nil, state = :both, opts = nil)
229
+ logger.debug "removing response to #{types.inspect} for state #{state.inspect}"
230
+ types = Array(types)
231
+ Array(state == :both ? %w(down up) : state).each do |state|
232
+ types.each do |type|
233
+ combined_types(type, opts).each do |combined_type|
234
+ responses[combined_type][state.to_sym].clear
235
+ end
236
+ end
237
+ end
238
+ nil
239
+ end
240
+
241
+ # Responds to an action by executing all matching responses, effectively simulating
242
+ # a button press/release.
243
+ #
244
+ # Parameters (see Launchpad for values):
245
+ #
246
+ # [+type+] type of the button to trigger
247
+ # [+state+] state of the button
248
+ #
249
+ # Optional options hash (see Launchpad for values):
250
+ #
251
+ # [<tt>:x</tt>] x coordinate
252
+ # [<tt>:y</tt>] y coordinate
253
+ def respond_to(type, state, opts = nil)
254
+ respond_to_action((opts || {}).merge(:type => type, :state => state))
255
+ end
256
+
257
+ private
258
+
259
+ # Returns the hash storing all responses. Keys are button types, values are
260
+ # hashes themselves, keys are <tt>:down/:up</tt>, values are arrays of responses.
261
+ def responses
262
+ @responses ||= Hash.new {|hash, key| hash[key] = {:down => [], :up => []}}
263
+ end
264
+
265
+ # Returns an array of grid positions for a range.
266
+ #
267
+ # Parameters:
268
+ #
269
+ # [+range+] the range definitions, can be
270
+ # * a Fixnum
271
+ # * a Range
272
+ # * an Array of Fixnum, Range or Array objects
273
+ def grid_range(range)
274
+ return nil if range.nil?
275
+ Array(range).flatten.map do |pos|
276
+ pos.respond_to?(:to_a) ? pos.to_a : pos
277
+ end.flatten.uniq
278
+ end
279
+
280
+ # Returns a list of combined types for the type and opts specified. Combined
281
+ # types are just the type, except for grid, where the opts are interpreted
282
+ # and all combinations of x and y coordinates are added as a position suffix.
283
+ #
284
+ # Example:
285
+ #
286
+ # combined_types(:grid, :x => 1..2, y => 2) => [:grid12, :grid22]
287
+ #
288
+ # Parameters (see Launchpad for values):
289
+ #
290
+ # [+type+] type of the button
291
+ #
292
+ # Optional options hash:
293
+ #
294
+ # [<tt>:x</tt>] x coordinate(s), can contain arrays and ranges, when specified
295
+ # without y coordinate, it's interpreted as a whole column
296
+ # [<tt>:y</tt>] y coordinate(s), can contain arrays and ranges, when specified
297
+ # without x coordinate, it's interpreted as a whole row
298
+ def combined_types(type, opts = nil)
299
+ if type.to_sym == :grid && opts
300
+ x = grid_range(opts[:x])
301
+ y = grid_range(opts[:y])
302
+ return [:grid] if x.nil? && y.nil? # whole grid
303
+ x ||= ['-'] # whole row
304
+ y ||= ['-'] # whole column
305
+ x.product(y).map {|x, y| :"grid#{x}#{y}"}
306
+ else
307
+ [type.to_sym]
308
+ end
309
+ end
310
+
311
+ # Reponds to an action by executing all matching responses.
312
+ #
313
+ # Parameters:
314
+ #
315
+ # [+action+] hash containing an action from Launchpad::Device.read_pending_actions
316
+ def respond_to_action(action)
317
+ type = action[:type].to_sym
318
+ state = action[:state].to_sym
319
+ actions = []
320
+ if type == :grid
321
+ actions += responses[:"grid#{action[:x]}#{action[:y]}"][state]
322
+ actions += responses[:"grid#{action[:x]}-"][state]
323
+ actions += responses[:"grid-#{action[:y]}"][state]
324
+ end
325
+ actions += responses[type][state]
326
+ actions += responses[:all][state]
327
+ actions.compact.each {|block| block.call(self, action)}
328
+ nil
329
+ rescue Exception => e
330
+ logger.error "error when responding to action #{action.inspect}: #{e.inspect}"
331
+ raise e
332
+ end
333
+
334
+ end
335
+
336
+ end
@@ -0,0 +1,27 @@
1
+ require 'logger'
2
+
3
+ module Launchpad
4
+
5
+ # This module provides logging facilities. Just include it to be able to log
6
+ # stuff.
7
+ module Logging
8
+
9
+ # Returns the logger to be used by the current instance.
10
+ #
11
+ # Returns:
12
+ #
13
+ # the logger set externally or a logger that swallows everything
14
+ def logger
15
+ @logger ||= Logger.new(nil)
16
+ end
17
+
18
+ # Sets the logger to be used by the current instance.
19
+ #
20
+ # [+logger+] the [Logger] instance
21
+ def logger=(logger)
22
+ @logger = logger
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,53 @@
1
+ module Launchpad
2
+
3
+ # Module defining constants for MIDI codes.
4
+ module MidiCodes
5
+
6
+ # Module defining MIDI status codes.
7
+ module Status
8
+ NIL = 0x00
9
+ OFF = 0x80
10
+ ON = 0x90
11
+ MULTI = 0x92
12
+ CC = 0xB0
13
+ end
14
+
15
+ # Module defininig MIDI data 1 (note) codes for control buttons.
16
+ module ControlButton
17
+ UP = 0x68
18
+ DOWN = 0x69
19
+ LEFT = 0x6A
20
+ RIGHT = 0x6B
21
+ SESSION = 0x6C
22
+ USER1 = 0x6D
23
+ USER2 = 0x6E
24
+ MIXER = 0x6F
25
+ end
26
+
27
+ # Module defininig MIDI data 1 (note) codes for scene buttons.
28
+ # TODO: Rename to match Mk2...
29
+ module SceneButton
30
+ SCENE1 = 0x59
31
+ SCENE2 = 0x4f
32
+ SCENE3 = 0x45
33
+ SCENE4 = 0x3b
34
+ SCENE5 = 0x31
35
+ SCENE6 = 0x27
36
+ SCENE7 = 0x1d
37
+ SCENE8 = 0x13
38
+ end
39
+
40
+ # Module defining MIDI data 2 (velocity) codes.
41
+ module Velocity
42
+ TEST_LEDS = 0x7C
43
+ end
44
+
45
+ # Module defining MIDI data 2 codes for selecting the grid layout.
46
+ module GridLayout
47
+ XY = 0x01
48
+ DRUM_RACK = 0x02
49
+ end
50
+
51
+ end
52
+
53
+ end