mrjoy-launchpad 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|