launchpad 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +0 -2
- data/README.rdoc +17 -2
- data/Rakefile +1 -0
- data/launchpad.gemspec +2 -2
- data/lib/launchpad.rb +32 -0
- data/lib/launchpad/device.rb +251 -60
- data/lib/launchpad/errors.rb +9 -9
- data/lib/launchpad/interaction.rb +111 -25
- data/lib/launchpad/midi_codes.rb +12 -7
- data/lib/launchpad/version.rb +1 -1
- data/test/helper.rb +4 -2
- data/test/test_device.rb +64 -1
- data/test/test_interaction.rb +29 -0
- metadata +2 -2
data/.document
CHANGED
data/README.rdoc
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
This gem provides an interface to access novation's launchpad programmatically. LEDs can be lighted and button presses can be responded to. Internally, launchpad's MIDI input/output is used to accomplish this.
|
4
4
|
|
5
|
-
The interfaces should be rather stable now, so experiment with them and comment on their usability. This still is work in progress. If you need anything or think the interfaces could be improved in any way, please contact me.
|
5
|
+
The interfaces should be rather stable now (sorry, I changed quite a bit since the last release), so experiment with them and comment on their usability. This still is work in progress. If you need anything or think the interfaces could be improved in any way, please contact me.
|
6
6
|
|
7
7
|
Sometimes, the launchpad won't react to anything or react to/light up the wrong LEDs. Don't despair, just dis- and reconnect the thing. It seems that some (unexpected) MIDI signals make it hickup.
|
8
8
|
|
@@ -53,6 +53,9 @@ This is an interaction example lighting all grid buttons in red when pressed and
|
|
53
53
|
interaction.response_to(:grid, :down) do |interaction, action|
|
54
54
|
interaction.device.change(:grid, action.merge(:red => :high))
|
55
55
|
end
|
56
|
+
interaction.response_to(:mixer, :down) do |interaction, action|
|
57
|
+
interaction.stop
|
58
|
+
end
|
56
59
|
|
57
60
|
interaction.start
|
58
61
|
|
@@ -62,12 +65,24 @@ For more details, see the examples. examples/color_picker.rb is the most complex
|
|
62
65
|
|
63
66
|
== Near future plans
|
64
67
|
|
65
|
-
* close devices properly as soon as halfbyte's pulled my changes to portmidi
|
66
68
|
* interaction responses for presses on single grid buttons/button areas
|
67
69
|
* double buffering
|
68
70
|
* bitmap rendering
|
69
71
|
|
70
72
|
|
73
|
+
== Changelog
|
74
|
+
|
75
|
+
=== v0.1.1
|
76
|
+
|
77
|
+
* ability to close device/interaction to free portmidi resources
|
78
|
+
* ability to initialize devices using device ids as well as device names
|
79
|
+
* complete documentation for http://rdoc.info/projects/thomasjachmann/launchpad
|
80
|
+
|
81
|
+
=== v0.1.0
|
82
|
+
|
83
|
+
* first feature complete version with kinda stable API
|
84
|
+
|
85
|
+
|
71
86
|
== Copyright
|
72
87
|
|
73
88
|
Copyright (c) 2009 Thomas Jachmann. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -13,6 +13,7 @@ begin
|
|
13
13
|
gem.homepage = 'http://github.com/thomasjachmann/launchpad'
|
14
14
|
gem.version = Launchpad::VERSION
|
15
15
|
gem.authors = ['Thomas Jachmann']
|
16
|
+
gem.has_rdoc = true
|
16
17
|
gem.add_dependency('portmidi')
|
17
18
|
gem.add_development_dependency('thoughtbot-shoulda', '>= 0')
|
18
19
|
gem.add_development_dependency('mocha')
|
data/launchpad.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{launchpad}
|
8
|
-
s.version = "0.1.
|
8
|
+
s.version = "0.1.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Thomas Jachmann"]
|
12
|
-
s.date = %q{2009-11-
|
12
|
+
s.date = %q{2009-11-18}
|
13
13
|
s.description = %q{This gem provides an interface to access novation's launchpad programmatically. LEDs can be lighted and button presses can be evaluated using launchpad's MIDI input/output.}
|
14
14
|
s.email = %q{tom.j@gmx.net}
|
15
15
|
s.extra_rdoc_files = [
|
data/lib/launchpad.rb
CHANGED
@@ -1,2 +1,34 @@
|
|
1
1
|
require 'launchpad/device'
|
2
2
|
require 'launchpad/interaction'
|
3
|
+
|
4
|
+
# All the fun of launchpad in one module!
|
5
|
+
#
|
6
|
+
# See Launchpad::Device for basic access to launchpad input/ouput
|
7
|
+
# and Launchpad::Interaction for advanced interaction features.
|
8
|
+
#
|
9
|
+
# The following parameters will be used throughout the library, so here are the ranges:
|
10
|
+
#
|
11
|
+
# [+type+] type of the button, one of
|
12
|
+
# <tt>
|
13
|
+
# :grid,
|
14
|
+
# :up, :down, :left, :right, :session, :user1, :user2, :mixer,
|
15
|
+
# :scene1 - :scene8
|
16
|
+
# </tt>
|
17
|
+
# [<tt>x/y</tt>] x/y coordinate (used when type is set to :grid),
|
18
|
+
# <tt>0-7</tt> (from left to right/top to bottom),
|
19
|
+
# mandatory when +type+ is set to <tt>:grid</tt>
|
20
|
+
# [<tt>red/green</tt>] brightness of the red/green LED,
|
21
|
+
# can be set to one of four levels:
|
22
|
+
# * off (<tt>:off, 0</tt>)
|
23
|
+
# * low brightness (<tt>:low, :lo, 1</tt>)
|
24
|
+
# * medium brightness (<tt>:medium, :med, 2</tt>)
|
25
|
+
# * full brightness (<tt>:high, :hi, 3</tt>)
|
26
|
+
# optional, defaults to <tt>:off</tt>
|
27
|
+
# [+mode+] button mode,
|
28
|
+
# one of
|
29
|
+
# * <tt>:normal</tt>
|
30
|
+
# * <tt>:flashing</tt> (LED is marked as flashing, see Launchpad::Device.flashing_on, Launchpad::Device.flashing_off and Launchpad::Device.flashing_auto)
|
31
|
+
# * <tt>:buffering</tt> (LED is written to buffer, see Launchpad::Device.start_buffering, Launchpad::Device.flush_buffer)
|
32
|
+
# optional, defaults to <tt>:normal</tt>
|
33
|
+
# [+state+] whether the button is pressed or released, <tt>:down/:up</tt>
|
34
|
+
module Launchpad; end
|
data/lib/launchpad/device.rb
CHANGED
@@ -6,54 +6,98 @@ require 'launchpad/version'
|
|
6
6
|
|
7
7
|
module Launchpad
|
8
8
|
|
9
|
+
# This class is used to exchange data with the launchpad.
|
10
|
+
# It provides methods to light LEDs and to get information about button presses/releases.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# require 'rubygems'
|
15
|
+
# require 'launchpad/device'
|
16
|
+
#
|
17
|
+
# device = Launchpad::Device.new
|
18
|
+
# device.test_leds
|
19
|
+
# sleep 1
|
20
|
+
# device.reset
|
21
|
+
# sleep 1
|
22
|
+
# device.change :grid, :x => 4, :y => 4, :red => :high, :green => :low
|
9
23
|
class Device
|
10
24
|
|
11
25
|
include MidiCodes
|
12
26
|
|
13
|
-
# Initializes the launchpad
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
27
|
+
# Initializes the launchpad device. When output capabilities are requested,
|
28
|
+
# the launchpad will be reset.
|
29
|
+
#
|
30
|
+
# Optional options hash:
|
31
|
+
#
|
32
|
+
# [<tt>:input</tt>] whether to use MIDI input for user interaction,
|
33
|
+
# <tt>true/false</tt>, optional, defaults to +true+
|
34
|
+
# [<tt>:output</tt>] whether to use MIDI output for data display,
|
35
|
+
# <tt>true/false</tt>, optional, defaults to +true+
|
36
|
+
# [<tt>:input_device_id</tt>] ID of the MIDI input device to use,
|
37
|
+
# optional, <tt>:device_name</tt> will be used if omitted
|
38
|
+
# [<tt>:output_device_id</tt>] ID of the MIDI output device to use,
|
39
|
+
# optional, <tt>:device_name</tt> will be used if omitted
|
40
|
+
# [<tt>:device_name</tt>] Name of the MIDI device to use,
|
41
|
+
# optional, defaults to "Launchpad"
|
42
|
+
#
|
43
|
+
# Errors raised:
|
44
|
+
#
|
45
|
+
# [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
|
46
|
+
# [Launchpad::DeviceBusyError] when device with ID or name specified is busy
|
19
47
|
def initialize(opts = nil)
|
20
48
|
opts = {
|
21
|
-
:device_name => 'Launchpad',
|
22
49
|
:input => true,
|
23
50
|
:output => true
|
24
51
|
}.merge(opts || {})
|
25
52
|
|
26
53
|
Portmidi.start
|
27
54
|
|
28
|
-
if opts[:input]
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
output_device = Portmidi.output_devices.select {|device| device.name == opts[:device_name]}.first
|
40
|
-
raise NoSuchDeviceError.new("MIDI output device #{opts[:device_name]} doesn't exist") if output_device.nil?
|
41
|
-
begin
|
42
|
-
@output = Portmidi::Output.new(output_device.device_id)
|
43
|
-
rescue RuntimeError => e
|
44
|
-
raise DeviceBusyError.new(e)
|
45
|
-
end
|
46
|
-
reset
|
47
|
-
end
|
55
|
+
@input = device(Portmidi.input_devices, Portmidi::Input, :id => opts[:input_device_id], :name => opts[:device_name]) if opts[:input]
|
56
|
+
@output = device(Portmidi.output_devices, Portmidi::Output, :id => opts[:output_device_id], :name => opts[:device_name]) if opts[:output]
|
57
|
+
reset if output_enabled?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Closes the device - nothing can be done with the device afterwards.
|
61
|
+
def close
|
62
|
+
@input.close unless @input.nil?
|
63
|
+
@input = nil
|
64
|
+
@output.close unless @output.nil?
|
65
|
+
@output = nil
|
48
66
|
end
|
49
67
|
|
50
|
-
#
|
68
|
+
# Determines whether this device has been closed.
|
69
|
+
def closed?
|
70
|
+
!(input_enabled? || output_enabled?)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Determines whether this device can be used to read input.
|
74
|
+
def input_enabled?
|
75
|
+
!@input.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
# Determines whether this device can be used to output data.
|
79
|
+
def output_enabled?
|
80
|
+
!@output.nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Resets the launchpad - all settings are reset and all LEDs are switched off.
|
84
|
+
#
|
85
|
+
# Errors raised:
|
86
|
+
#
|
87
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
51
88
|
def reset
|
52
89
|
output(Status::CC, Status::NIL, Status::NIL)
|
53
90
|
end
|
54
91
|
|
55
|
-
# Lights all LEDs (for testing purposes)
|
56
|
-
#
|
92
|
+
# Lights all LEDs (for testing purposes).
|
93
|
+
#
|
94
|
+
# Parameters (see Launchpad for values):
|
95
|
+
#
|
96
|
+
# [+brightness+] brightness of both LEDs for all buttons
|
97
|
+
#
|
98
|
+
# Errors raised:
|
99
|
+
#
|
100
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
57
101
|
def test_leds(brightness = :high)
|
58
102
|
brightness = brightness(brightness)
|
59
103
|
if brightness == 0
|
@@ -63,27 +107,52 @@ module Launchpad
|
|
63
107
|
end
|
64
108
|
end
|
65
109
|
|
66
|
-
# Changes a single LED
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
110
|
+
# Changes a single LED.
|
111
|
+
#
|
112
|
+
# Parameters (see Launchpad for values):
|
113
|
+
#
|
114
|
+
# [+type+] type of the button to change
|
115
|
+
#
|
116
|
+
# Optional options hash (see Launchpad for values):
|
117
|
+
#
|
118
|
+
# [<tt>:x</tt>] x coordinate
|
119
|
+
# [<tt>:y</tt>] y coordinate
|
120
|
+
# [<tt>:red</tt>] brightness of red LED
|
121
|
+
# [<tt>:green</tt>] brightness of green LED
|
122
|
+
# [<tt>:mode</tt>] button mode
|
123
|
+
#
|
124
|
+
# Errors raised:
|
125
|
+
#
|
126
|
+
# [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range
|
127
|
+
# [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
|
128
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
75
129
|
def change(type, opts = nil)
|
76
130
|
opts ||= {}
|
77
131
|
status = %w(up down left right session user1 user2 mixer).include?(type.to_s) ? Status::CC : Status::ON
|
78
132
|
output(status, note(type, opts), velocity(opts))
|
79
133
|
end
|
80
134
|
|
81
|
-
# Changes all LEDs
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
135
|
+
# Changes all LEDs in batch mode.
|
136
|
+
#
|
137
|
+
# Parameters (see Launchpad for values):
|
138
|
+
#
|
139
|
+
# [+colors] an array of colors, each either being an integer or a Hash
|
140
|
+
# * integer: calculated using the formula
|
141
|
+
# <tt>color = 16 * green + red</tt>
|
142
|
+
# * Hash:
|
143
|
+
# [<tt>:red</tt>] brightness of red LED
|
144
|
+
# [<tt>:green</tt>] brightness of green LED
|
145
|
+
# [<tt>:mode</tt>] button mode
|
146
|
+
# the array consists of 64 colors for the grid buttons,
|
147
|
+
# 8 colors for the scene buttons (top to bottom)
|
148
|
+
# and 8 colors for the top control buttons (left to right),
|
149
|
+
# maximum 80 values - excessive values will be ignored,
|
150
|
+
# missing values will be filled with 0
|
151
|
+
#
|
152
|
+
# Errors raised:
|
153
|
+
#
|
154
|
+
# [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
|
155
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
87
156
|
def change_all(*colors)
|
88
157
|
# ensure that colors is at least and most 80 elements long
|
89
158
|
colors = colors.flatten[0..79]
|
@@ -96,17 +165,30 @@ module Launchpad
|
|
96
165
|
end
|
97
166
|
end
|
98
167
|
|
99
|
-
# Switches LEDs marked as flashing on
|
168
|
+
# Switches LEDs marked as flashing on when using custom timer for flashing.
|
169
|
+
#
|
170
|
+
# Errors raised:
|
171
|
+
#
|
172
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
100
173
|
def flashing_on
|
101
174
|
output(Status::CC, Status::NIL, Velocity::FLASHING_ON)
|
102
175
|
end
|
103
176
|
|
104
|
-
# Switches LEDs marked as flashing off
|
177
|
+
# Switches LEDs marked as flashing off when using custom timer for flashing.
|
178
|
+
#
|
179
|
+
# Errors raised:
|
180
|
+
#
|
181
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
105
182
|
def flashing_off
|
106
183
|
output(Status::CC, Status::NIL, Velocity::FLASHING_OFF)
|
107
184
|
end
|
108
185
|
|
109
|
-
# Starts flashing LEDs marked as flashing automatically
|
186
|
+
# Starts flashing LEDs marked as flashing automatically.
|
187
|
+
# Stop flashing by calling flashing_on or flashing_off.
|
188
|
+
#
|
189
|
+
# Errors raised:
|
190
|
+
#
|
191
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
110
192
|
def flashing_auto
|
111
193
|
output(Status::CC, Status::NIL, Velocity::FLASHING_AUTO)
|
112
194
|
end
|
@@ -124,16 +206,21 @@ module Launchpad
|
|
124
206
|
# end
|
125
207
|
# end
|
126
208
|
|
127
|
-
# Reads user actions (button presses/releases) that
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
# ]
|
209
|
+
# Reads user actions (button presses/releases) that haven't been handled yet.
|
210
|
+
#
|
211
|
+
# Returns:
|
212
|
+
#
|
213
|
+
# an array of hashes with (see Launchpad for values):
|
214
|
+
#
|
215
|
+
# [<tt>:timestamp</tt>] integer indicating the time when the action occured
|
216
|
+
# [<tt>:state</tt>] state of the button after action
|
217
|
+
# [<tt>:type</tt>] type of the button
|
218
|
+
# [<tt>:x</tt>] x coordinate
|
219
|
+
# [<tt>:y</tt>] y coordinate
|
220
|
+
#
|
221
|
+
# Errors raised:
|
222
|
+
#
|
223
|
+
# [Launchpad::NoInputAllowedError] when input is not enabled
|
137
224
|
def read_pending_actions
|
138
225
|
Array(input).collect do |midi_message|
|
139
226
|
(code, note, velocity) = midi_message[:message]
|
@@ -175,17 +262,97 @@ module Launchpad
|
|
175
262
|
|
176
263
|
private
|
177
264
|
|
265
|
+
# Creates input/output devices.
|
266
|
+
#
|
267
|
+
# Parameters:
|
268
|
+
#
|
269
|
+
# [+devices+] array of portmidi devices
|
270
|
+
# [+device_type] class to instantiate (<tt>Portmidi::Input/Portmidi::Output</tt>)
|
271
|
+
#
|
272
|
+
# Options hash:
|
273
|
+
#
|
274
|
+
# [<tt>:id</tt>] id of the MIDI device to use
|
275
|
+
# [<tt>:name</tt>] name of the MIDI device to use,
|
276
|
+
# only used when <tt>:id</tt> is not specified,
|
277
|
+
# defaults to "Launchpad"
|
278
|
+
#
|
279
|
+
# Returns:
|
280
|
+
#
|
281
|
+
# newly created device
|
282
|
+
#
|
283
|
+
# Errors raised:
|
284
|
+
#
|
285
|
+
# [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
|
286
|
+
# [Launchpad::DeviceBusyError] when device with ID or name specified is busy
|
287
|
+
def device(devices, device_type, opts)
|
288
|
+
id = opts[:id]
|
289
|
+
if id.nil?
|
290
|
+
name = opts[:name] || 'Launchpad'
|
291
|
+
device = devices.select {|device| device.name == name}.first
|
292
|
+
id = device.device_id unless device.nil?
|
293
|
+
end
|
294
|
+
raise NoSuchDeviceError.new("MIDI device #{opts[:id] || opts[:name]} doesn't exist") if id.nil?
|
295
|
+
device_type.new(id)
|
296
|
+
rescue RuntimeError => e
|
297
|
+
raise DeviceBusyError.new(e)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Reads input from the MIDI device.
|
301
|
+
#
|
302
|
+
# Returns:
|
303
|
+
#
|
304
|
+
# an array of hashes with:
|
305
|
+
#
|
306
|
+
# [<tt>:message</tt>] an array of
|
307
|
+
# MIDI status code,
|
308
|
+
# MIDI data 1 (note),
|
309
|
+
# MIDI data 2 (velocity)
|
310
|
+
# and a fourth value
|
311
|
+
# [<tt>:timestamp</tt>] integer indicating the time when the MIDI message was created
|
312
|
+
#
|
313
|
+
# Errors raised:
|
314
|
+
#
|
315
|
+
# [Launchpad::NoInputAllowedError] when output is not enabled
|
178
316
|
def input
|
179
317
|
raise NoInputAllowedError if @input.nil?
|
180
318
|
@input.read(16)
|
181
319
|
end
|
182
320
|
|
183
|
-
|
321
|
+
# Writes data to the MIDI device.
|
322
|
+
#
|
323
|
+
# Parameters:
|
324
|
+
#
|
325
|
+
# [+status+] MIDI status code
|
326
|
+
# [+data1+] MIDI data 1 (note)
|
327
|
+
# [+data2+] MIDI data 2 (velocity)
|
328
|
+
#
|
329
|
+
# Errors raised:
|
330
|
+
#
|
331
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
332
|
+
def output(status, data1, data2)
|
184
333
|
raise NoOutputAllowedError if @output.nil?
|
185
|
-
@output.write([{:message =>
|
334
|
+
@output.write([{:message => [status, data1, data2], :timestamp => 0}])
|
186
335
|
nil
|
187
336
|
end
|
188
337
|
|
338
|
+
# Calculates the MIDI data 1 value (note) for a button.
|
339
|
+
#
|
340
|
+
# Parameters (see Launchpad for values):
|
341
|
+
#
|
342
|
+
# [+type+] type of the button
|
343
|
+
#
|
344
|
+
# Options hash:
|
345
|
+
#
|
346
|
+
# [<tt>:x</tt>] x coordinate
|
347
|
+
# [<tt>:y</tt>] y coordinate
|
348
|
+
#
|
349
|
+
# Returns:
|
350
|
+
#
|
351
|
+
# integer to be used for MIDI data 1
|
352
|
+
#
|
353
|
+
# Errors raised:
|
354
|
+
#
|
355
|
+
# [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range
|
189
356
|
def note(type, opts)
|
190
357
|
case type
|
191
358
|
when :up then ControlButton::UP
|
@@ -212,6 +379,21 @@ module Launchpad
|
|
212
379
|
end
|
213
380
|
end
|
214
381
|
|
382
|
+
# Calculates the MIDI data 2 value (velocity) for given brightness and mode values.
|
383
|
+
#
|
384
|
+
# Options hash:
|
385
|
+
#
|
386
|
+
# [<tt>:red</tt>] brightness of red LED
|
387
|
+
# [<tt>:green</tt>] brightness of green LED
|
388
|
+
# [<tt>:mode</tt>] button mode
|
389
|
+
#
|
390
|
+
# Returns:
|
391
|
+
#
|
392
|
+
# integer to be used for MIDI data 2
|
393
|
+
#
|
394
|
+
# Errors raised:
|
395
|
+
#
|
396
|
+
# [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
|
215
397
|
def velocity(opts)
|
216
398
|
color = if opts.is_a?(Hash)
|
217
399
|
red = brightness(opts[:red] || 0)
|
@@ -228,6 +410,15 @@ module Launchpad
|
|
228
410
|
color + flags
|
229
411
|
end
|
230
412
|
|
413
|
+
# Calculates the integer brightness for given brightness values.
|
414
|
+
#
|
415
|
+
# Parameters (see Launchpad for values):
|
416
|
+
#
|
417
|
+
# [+brightness+] brightness
|
418
|
+
#
|
419
|
+
# Errors raised:
|
420
|
+
#
|
421
|
+
# [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
|
231
422
|
def brightness(brightness)
|
232
423
|
case brightness
|
233
424
|
when 0, :off then 0
|
data/lib/launchpad/errors.rb
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
module Launchpad
|
2
2
|
|
3
|
-
# Generic launchpad error
|
3
|
+
# Generic launchpad error.
|
4
4
|
class LaunchpadError < StandardError; end
|
5
5
|
|
6
|
-
# Error raised when the MIDI device specified doesn't exist
|
6
|
+
# Error raised when the MIDI device specified doesn't exist.
|
7
7
|
class NoSuchDeviceError < LaunchpadError; end
|
8
8
|
|
9
|
-
# Error raised when the MIDI device specified is busy
|
9
|
+
# Error raised when the MIDI device specified is busy.
|
10
10
|
class DeviceBusyError < LaunchpadError; end
|
11
11
|
|
12
12
|
# Error raised when an input has been requested, although
|
13
|
-
# launchpad has been initialized without input
|
13
|
+
# launchpad has been initialized without input.
|
14
14
|
class NoInputAllowedError < LaunchpadError; end
|
15
15
|
|
16
16
|
# Error raised when an output has been requested, although
|
17
|
-
# launchpad has been initialized without output
|
17
|
+
# launchpad has been initialized without output.
|
18
18
|
class NoOutputAllowedError < LaunchpadError; end
|
19
19
|
|
20
|
-
# Error raised when x/y coordinates outside of the grid
|
21
|
-
# or none
|
20
|
+
# Error raised when <tt>x/y</tt> coordinates outside of the grid
|
21
|
+
# or none were specified.
|
22
22
|
class NoValidGridCoordinatesError < LaunchpadError; end
|
23
23
|
|
24
|
-
# Error raised when wrong brightness was specified
|
24
|
+
# Error raised when wrong brightness was specified.
|
25
25
|
class NoValidBrightnessError < LaunchpadError; end
|
26
26
|
|
27
27
|
# Error raised when anything fails while communicating
|
28
|
-
# with the launchpad
|
28
|
+
# with the launchpad.
|
29
29
|
class CommunicationError < LaunchpadError
|
30
30
|
attr_accessor :source
|
31
31
|
def initialize(e)
|
@@ -2,16 +2,49 @@ require 'launchpad/device'
|
|
2
2
|
|
3
3
|
module Launchpad
|
4
4
|
|
5
|
+
# This class provides advanced interaction features.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# require 'rubygems'
|
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
|
5
21
|
class Interaction
|
6
22
|
|
7
|
-
|
23
|
+
# Returns the Launchpad::Device the Launchpad::Interaction acts on.
|
24
|
+
attr_reader :device
|
8
25
|
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
26
|
+
# Returns whether the Launchpad::Interaction is active or not.
|
27
|
+
attr_reader :active
|
28
|
+
|
29
|
+
# Initializes the interaction.
|
30
|
+
#
|
31
|
+
# Optional options hash:
|
32
|
+
#
|
33
|
+
# [<tt>:device</tt>] Launchpad::Device to act on,
|
34
|
+
# optional, <tt>:input_device_id/:output_device_id</tt> will be used if omitted
|
35
|
+
# [<tt>:input_device_id</tt>] ID of the MIDI input device to use,
|
36
|
+
# optional, <tt>:device_name</tt> will be used if omitted
|
37
|
+
# [<tt>:output_device_id</tt>] ID of the MIDI output device to use,
|
38
|
+
# optional, <tt>:device_name</tt> will be used if omitted
|
39
|
+
# [<tt>:device_name</tt>] Name of the MIDI device to use,
|
40
|
+
# optional, defaults to "Launchpad"
|
41
|
+
# [<tt>:latency</tt>] delay (in s, fractions allowed) between MIDI pulls,
|
42
|
+
# optional, defaults to 0.001 (1ms)
|
43
|
+
#
|
44
|
+
# Errors raised:
|
45
|
+
#
|
46
|
+
# [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
|
47
|
+
# [Launchpad::DeviceBusyError] when device with ID or name specified is busy
|
15
48
|
def initialize(opts = nil)
|
16
49
|
opts ||= {}
|
17
50
|
@device = opts[:device] || Device.new(opts.merge(:input => true, :output => true))
|
@@ -19,7 +52,23 @@ module Launchpad
|
|
19
52
|
@active = false
|
20
53
|
end
|
21
54
|
|
22
|
-
#
|
55
|
+
# Closes the interaction's device - nothing can be done with the interaction/device afterwards.
|
56
|
+
def close
|
57
|
+
@device.close
|
58
|
+
end
|
59
|
+
|
60
|
+
# Determines whether this interaction's device has been closed.
|
61
|
+
def closed?
|
62
|
+
@device.closed?
|
63
|
+
end
|
64
|
+
|
65
|
+
# Starts interacting with the launchpad, blocking. Resets the device when
|
66
|
+
# the interaction was properly stopped via stop.
|
67
|
+
#
|
68
|
+
# Errors raised:
|
69
|
+
#
|
70
|
+
# [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
|
71
|
+
# [Launchpad::CommunicationError] when anything unexpected happens while communicating with the launchpad
|
23
72
|
def start
|
24
73
|
@active = true
|
25
74
|
while @active do
|
@@ -31,17 +80,32 @@ module Launchpad
|
|
31
80
|
raise CommunicationError.new(e)
|
32
81
|
end
|
33
82
|
|
34
|
-
# Stops interacting with the launchpad
|
83
|
+
# Stops interacting with the launchpad and resets it.
|
35
84
|
def stop
|
36
85
|
@active = false
|
37
86
|
end
|
38
87
|
|
39
|
-
# Registers a response to one or more actions
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
88
|
+
# Registers a response to one or more actions.
|
89
|
+
#
|
90
|
+
# Parameters (see Launchpad for values):
|
91
|
+
#
|
92
|
+
# [+types+] one or an array of button types to respond to,
|
93
|
+
# additional value <tt>:all</tt> for all buttons
|
94
|
+
# [+state+] button state to respond to,
|
95
|
+
# additional value <tt>:both</tt>
|
96
|
+
#
|
97
|
+
# Optional options hash:
|
98
|
+
#
|
99
|
+
# [<tt>:exclusive</tt>] <tt>true/false</tt>,
|
100
|
+
# whether to deregister all other responses to the specified actions,
|
101
|
+
# optional, defaults to +false+
|
102
|
+
#
|
103
|
+
# Takes a block which will be called when an action matching the parameters occurs.
|
104
|
+
#
|
105
|
+
# Block parameters:
|
106
|
+
#
|
107
|
+
# [+interaction+] the interaction object that received the action
|
108
|
+
# [+action+] the action received from Launchpad::Device.read_pending_actions
|
45
109
|
def response_to(types = :all, state = :both, opts = nil, &block)
|
46
110
|
types = Array(types)
|
47
111
|
opts ||= {}
|
@@ -49,39 +113,61 @@ module Launchpad
|
|
49
113
|
Array(state == :both ? %w(down up) : state).each do |state|
|
50
114
|
types.each {|type| responses[type.to_sym][state.to_sym] << block}
|
51
115
|
end
|
116
|
+
nil
|
52
117
|
end
|
53
118
|
|
54
|
-
# Deregisters all responses to one or more actions
|
55
|
-
#
|
56
|
-
#
|
119
|
+
# Deregisters all responses to one or more actions.
|
120
|
+
#
|
121
|
+
# Parameters (see Launchpad for values):
|
122
|
+
#
|
123
|
+
# [+types+] one or an array of button types to respond to,
|
124
|
+
# additional value <tt>:all</tt> for actions on all buttons
|
125
|
+
# (but not meaning "all responses"),
|
126
|
+
# optional, defaults to +nil+, meaning "all responses"
|
127
|
+
# [+state+] button state to respond to,
|
128
|
+
# additional value <tt>:both</tt>
|
57
129
|
def no_response_to(types = nil, state = :both)
|
58
130
|
types = Array(types)
|
59
131
|
Array(state == :both ? %w(down up) : state).each do |state|
|
60
132
|
types.each {|type| responses[type.to_sym][state.to_sym].clear}
|
61
133
|
end
|
134
|
+
nil
|
62
135
|
end
|
63
136
|
|
64
|
-
# Responds to an action by executing all matching responses
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
137
|
+
# Responds to an action by executing all matching responses, effectively simulating
|
138
|
+
# a button press/release.
|
139
|
+
#
|
140
|
+
# Parameters (see Launchpad for values):
|
141
|
+
#
|
142
|
+
# [+type+] type of the button to trigger
|
143
|
+
# [+state+] state of the button
|
144
|
+
#
|
145
|
+
# Optional options hash (see Launchpad for values):
|
146
|
+
#
|
147
|
+
# [<tt>:x</tt>] x coordinate
|
148
|
+
# [<tt>:y</tt>] y coordinate
|
71
149
|
def respond_to(type, state, opts = nil)
|
72
150
|
respond_to_action((opts || {}).merge(:type => type, :state => state))
|
73
151
|
end
|
74
152
|
|
75
153
|
private
|
76
154
|
|
155
|
+
# Returns the hash storing all responses. Keys are button types, values are
|
156
|
+
# hashes themselves, keys are <tt>:down/:up</tt>, values are arrays of responses.
|
77
157
|
def responses
|
78
158
|
@responses ||= Hash.new {|hash, key| hash[key] = {:down => [], :up => []}}
|
79
159
|
end
|
80
160
|
|
161
|
+
# Reponds to an action by executing all matching responses.
|
162
|
+
#
|
163
|
+
# Parameters:
|
164
|
+
#
|
165
|
+
# [+action+] hash containing an action from Launchpad::Device.read_pending_actions
|
81
166
|
def respond_to_action(action)
|
82
167
|
type = action[:type].to_sym
|
83
168
|
state = action[:state].to_sym
|
84
169
|
(responses[type][state] + responses[:all][state]).each {|block| block.call(self, action)}
|
170
|
+
nil
|
85
171
|
end
|
86
172
|
|
87
173
|
end
|
data/lib/launchpad/midi_codes.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
module Launchpad
|
2
2
|
|
3
|
+
# Module defining constants for MIDI codes.
|
3
4
|
module MidiCodes
|
4
5
|
|
6
|
+
# Module defining MIDI status codes.
|
5
7
|
module Status
|
6
8
|
NIL = 0x00
|
7
9
|
OFF = 0x80
|
@@ -10,13 +12,7 @@ module Launchpad
|
|
10
12
|
CC = 0xB0
|
11
13
|
end
|
12
14
|
|
13
|
-
|
14
|
-
FLASHING_ON = 0x20
|
15
|
-
FLASHING_OFF = 0x21
|
16
|
-
FLASHING_AUTO = 0x28
|
17
|
-
TEST_LEDS = 0x7C
|
18
|
-
end
|
19
|
-
|
15
|
+
# Module defininig MIDI data 1 (note) codes for control buttons.
|
20
16
|
module ControlButton
|
21
17
|
UP = 0x68
|
22
18
|
DOWN = 0x69
|
@@ -28,6 +24,7 @@ module Launchpad
|
|
28
24
|
MIXER = 0x6F
|
29
25
|
end
|
30
26
|
|
27
|
+
# Module defininig MIDI data 1 (note) codes for scene buttons.
|
31
28
|
module SceneButton
|
32
29
|
SCENE1 = 0x08
|
33
30
|
SCENE2 = 0x18
|
@@ -39,6 +36,14 @@ module Launchpad
|
|
39
36
|
SCENE8 = 0x78
|
40
37
|
end
|
41
38
|
|
39
|
+
# Module defining MIDI data 2 (velocity) codes.
|
40
|
+
module Velocity
|
41
|
+
FLASHING_ON = 0x20
|
42
|
+
FLASHING_OFF = 0x21
|
43
|
+
FLASHING_AUTO = 0x28
|
44
|
+
TEST_LEDS = 0x7C
|
45
|
+
end
|
46
|
+
|
42
47
|
end
|
43
48
|
|
44
49
|
end
|
data/lib/launchpad/version.rb
CHANGED
data/test/helper.rb
CHANGED
@@ -19,13 +19,13 @@ end
|
|
19
19
|
# mock Portmidi for tests
|
20
20
|
module Portmidi
|
21
21
|
|
22
|
-
class DeviceError < StandardError; end
|
23
|
-
|
24
22
|
class Input
|
25
23
|
attr_accessor :device_id
|
26
24
|
def initialize(device_id)
|
27
25
|
self.device_id = device_id
|
28
26
|
end
|
27
|
+
def read(*args); nil; end
|
28
|
+
def close; nil; end
|
29
29
|
end
|
30
30
|
|
31
31
|
class Output
|
@@ -33,6 +33,8 @@ module Portmidi
|
|
33
33
|
def initialize(device_id)
|
34
34
|
self.device_id = device_id
|
35
35
|
end
|
36
|
+
def write(*args); nil; end
|
37
|
+
def close; nil; end
|
36
38
|
end
|
37
39
|
|
38
40
|
def self.input_devices; mock_devices; end
|
data/test/test_device.rb
CHANGED
@@ -73,7 +73,7 @@ class TestDevice < Test::Unit::TestCase
|
|
73
73
|
assert_nil d.instance_variable_get('@output')
|
74
74
|
end
|
75
75
|
|
76
|
-
should 'initialize the correct input output devices' do
|
76
|
+
should 'initialize the correct input output devices when specified by name' do
|
77
77
|
Portmidi.stubs(:input_devices).returns(mock_devices(:id => 4, :name => 'Launchpad Name'))
|
78
78
|
Portmidi.stubs(:output_devices).returns(mock_devices(:id => 5, :name => 'Launchpad Name'))
|
79
79
|
d = Launchpad::Device.new(:device_name => 'Launchpad Name')
|
@@ -83,6 +83,16 @@ class TestDevice < Test::Unit::TestCase
|
|
83
83
|
assert_equal 5, output.device_id
|
84
84
|
end
|
85
85
|
|
86
|
+
should 'initialize the correct input output devices when specified by id' do
|
87
|
+
Portmidi.stubs(:input_devices).returns(mock_devices(:id => 4))
|
88
|
+
Portmidi.stubs(:output_devices).returns(mock_devices(:id => 5))
|
89
|
+
d = Launchpad::Device.new(:input_device_id => 4, :output_device_id => 5, :device_name => 'nonexistant')
|
90
|
+
assert_equal Portmidi::Input, (input = d.instance_variable_get('@input')).class
|
91
|
+
assert_equal 4, input.device_id
|
92
|
+
assert_equal Portmidi::Output, (output = d.instance_variable_get('@output')).class
|
93
|
+
assert_equal 5, output.device_id
|
94
|
+
end
|
95
|
+
|
86
96
|
should 'raise NoSuchDeviceError when requested input device does not exist' do
|
87
97
|
assert_raise Launchpad::NoSuchDeviceError do
|
88
98
|
Portmidi.stubs(:input_devices).returns(mock_devices(:name => 'Launchpad Input'))
|
@@ -113,6 +123,59 @@ class TestDevice < Test::Unit::TestCase
|
|
113
123
|
|
114
124
|
end
|
115
125
|
|
126
|
+
context 'close' do
|
127
|
+
|
128
|
+
should 'not fail when neither input nor output are there' do
|
129
|
+
Launchpad::Device.new(:input => false, :output => false).close
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'with input and output devices' do
|
133
|
+
|
134
|
+
setup do
|
135
|
+
Portmidi::Input.stubs(:new).returns(@input = mock('input'))
|
136
|
+
Portmidi::Output.stubs(:new).returns(@output = mock('output', :write => nil))
|
137
|
+
@device = Launchpad::Device.new
|
138
|
+
end
|
139
|
+
|
140
|
+
should 'close input/output and raise NoInputAllowedError/NoOutputAllowedError on subsequent read/write accesses' do
|
141
|
+
@input.expects(:close)
|
142
|
+
@output.expects(:close)
|
143
|
+
@device.close
|
144
|
+
assert_raise Launchpad::NoInputAllowedError do
|
145
|
+
@device.read_pending_actions
|
146
|
+
end
|
147
|
+
assert_raise Launchpad::NoOutputAllowedError do
|
148
|
+
@device.change(:session)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
context 'closed?' do
|
157
|
+
|
158
|
+
should 'return true when neither input nor output are there' do
|
159
|
+
assert Launchpad::Device.new(:input => false, :output => false).closed?
|
160
|
+
end
|
161
|
+
|
162
|
+
should 'return false when initialized with input' do
|
163
|
+
assert !Launchpad::Device.new(:input => true, :output => false).closed?
|
164
|
+
end
|
165
|
+
|
166
|
+
should 'return false when initialized with output' do
|
167
|
+
assert !Launchpad::Device.new(:input => false, :output => true).closed?
|
168
|
+
end
|
169
|
+
|
170
|
+
should 'return false when initialized with both but true after calling close' do
|
171
|
+
d = Launchpad::Device.new
|
172
|
+
assert !d.closed?
|
173
|
+
d.close
|
174
|
+
assert d.closed?
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
116
179
|
{
|
117
180
|
:reset => [0xB0, 0x00, 0x00],
|
118
181
|
:flashing_on => [0xB0, 0x00, 0x20],
|
data/test/test_interaction.rb
CHANGED
@@ -26,6 +26,35 @@ class TestInteraction < Test::Unit::TestCase
|
|
26
26
|
|
27
27
|
end
|
28
28
|
|
29
|
+
context 'close' do
|
30
|
+
|
31
|
+
should 'close device' do
|
32
|
+
interaction = Launchpad::Interaction.new(:device => device = Launchpad::Device.new)
|
33
|
+
device.expects(:close)
|
34
|
+
interaction.close
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'craise NoInputAllowedError on subsequent accesses' do
|
38
|
+
interaction = Launchpad::Interaction.new(:device => device = Launchpad::Device.new)
|
39
|
+
interaction.close
|
40
|
+
assert_raise Launchpad::NoInputAllowedError do
|
41
|
+
interaction.start
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'closed?' do
|
48
|
+
|
49
|
+
should 'return false on a newly created interaction, but true after closing' do
|
50
|
+
interaction = Launchpad::Interaction.new
|
51
|
+
assert !interaction.closed?
|
52
|
+
interaction.close
|
53
|
+
assert interaction.closed?
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
29
58
|
context 'start' do
|
30
59
|
|
31
60
|
# this is kinda greybox tested, since I couldn't come up with another way to test a loop [thomas, 2009-11-11]
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: launchpad
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Jachmann
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-11-
|
12
|
+
date: 2009-11-18 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|