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 CHANGED
@@ -1,5 +1,3 @@
1
1
  README.rdoc
2
2
  lib/**/*.rb
3
- bin/*
4
- features/**/*.feature
5
3
  LICENSE
@@ -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')
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{launchpad}
8
- s.version = "0.1.0"
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-13}
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 = [
@@ -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
@@ -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
- # :device_name => Name of the MIDI device to use, optional, defaults to Launchpad
16
- # :input => true/false, whether to use MIDI input for user interaction, optional, defaults to true
17
- # :output => true/false, whether to use MIDI output for data display, optional, defaults to true
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
- input_device = Portmidi.input_devices.select {|device| device.name == opts[:device_name]}.first
30
- raise NoSuchDeviceError.new("MIDI input device #{opts[:device_name]} doesn't exist") if input_device.nil?
31
- begin
32
- @input = Portmidi::Input.new(input_device.device_id)
33
- rescue RuntimeError => e
34
- raise DeviceBusyError.new(e)
35
- end
36
- end
37
-
38
- if opts[:output]
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
- # Resets the launchpad - all settings are reset and all LEDs are switched off
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
- # takes an optional parameter brightness (:off/:low/:medium/:high, defaults to :high)
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
- # type => one of :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8
68
- # opts => {
69
- # :x => x coordinate (0 based from top left, mandatory if type is :grid)
70
- # :y => y coordinate (0 based from top left, mandatory if type is :grid)
71
- # :red => brightness of red LED (0-3, optional, defaults to 0)
72
- # :green => brightness of red LED (0-3, optional, defaults to 0)
73
- # :mode => button behaviour (:normal, :flashing, :buffering, optional, defaults to :normal)
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 at once
82
- # velocities is an array of arrays, each containing a
83
- # color value calculated using the formula
84
- # color = 16 * green + red
85
- # with green and red each ranging from 0-3
86
- # first the grid, then the scene buttons (top to bottom), then the top control buttons (left to right), maximum 80 values
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 (when using custom timer for flashing)
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 (when using custom timer for flashing)
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 (stop by calling #flashing_on or #flashing_off)
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 aren't handled yet
128
- # [
129
- # {
130
- # :timestamp => integer indicating the time when the action occured
131
- # :state => :down/:up, whether the button has been pressed or released
132
- # :type => which button has been pressed, one of :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8
133
- # :x => x coordinate (0-7), only set when :type is :grid
134
- # :y => y coordinate (0-7), only set when :type is :grid
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
- def output(*args)
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 => args, :timestamp => 0}])
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
@@ -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 at all were specified
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
- attr_reader :device, :active
23
+ # Returns the Launchpad::Device the Launchpad::Interaction acts on.
24
+ attr_reader :device
8
25
 
9
- # Initializes the launchpad interaction
10
- # {
11
- # :device => Launchpad::Device instance, optional
12
- # :device_name => Name of the MIDI device to use, optional, defaults to Launchpad, ignored when :device is specified
13
- # :latency => delay (in s, fractions allowed) between MIDI pulls, optional, defaults to 0.001
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
- # Starts interacting with the launchpad, blocking
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
- # types => the type of action to respond to, one or more of :all, :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8, optional, defaults to :all
41
- # state => which state transition to respond to, one of :down, :up, :both, optional, defaults to :both
42
- # opts => {
43
- # :exclusive => whether all other responses to the given types shall be deregistered first
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
- # type => the type of response to clear, one or more of :all (not meaning "all responses" but "responses registered for type :all"), :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8, optional, defaults to nil (meaning "all responses")
56
- # state => which state transition to not respond to, one of :down, :up, :both, optional, defaults to :both
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
- # type => the type of action to respond to, one of :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8
66
- # state => which state transition to respond to, one of :down, :up
67
- # opts => {
68
- # :x => x coordinate (0 based from top left)
69
- # :y => y coordinate (0 based from top left)
70
- # }, unused unless type is :grid
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
@@ -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
- module Velocity
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
@@ -1,3 +1,3 @@
1
1
  module Launchpad
2
- VERSION = '0.1.0'
2
+ VERSION = '0.1.1'
3
3
  end
@@ -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
@@ -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],
@@ -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.0
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-13 00:00:00 +01:00
12
+ date: 2009-11-18 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency