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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +8 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +136 -0
- data/Rakefile +10 -0
- data/examples/binary_clock.rb +29 -0
- data/examples/color_picker.rb +96 -0
- data/examples/colors.rb +21 -0
- data/examples/doodle.rb +68 -0
- data/examples/double_buffering.rb +104 -0
- data/examples/drawing_board.rb +25 -0
- data/examples/feedback.rb +34 -0
- data/examples/reset.rb +6 -0
- data/launchpad.gemspec +34 -0
- data/lib/launchpad.rb +34 -0
- data/lib/launchpad/device.rb +574 -0
- data/lib/launchpad/errors.rb +37 -0
- data/lib/launchpad/interaction.rb +336 -0
- data/lib/launchpad/logging.rb +27 -0
- data/lib/launchpad/midi_codes.rb +53 -0
- data/lib/launchpad/version.rb +3 -0
- data/monitor.rb +88 -0
- data/test/helper.rb +44 -0
- data/test/test_device.rb +530 -0
- data/test/test_interaction.rb +456 -0
- data/testbed.rb +48 -0
- metadata +146 -0
@@ -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
|