launchpad_mk2 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ require 'launchpad_mk2'
2
+
3
+ device = Launchpad::Device.new(:input => false, :output => true)
4
+
5
+ device.reset_all()
6
+ # sleep 0.5
7
+ device.lightn_column([0, 3,6], 45)
8
+ # sleep 0.5
9
+ device.lightn_row([7,6, 2], 22)
10
+ # sleep 0.5
11
+ # device.light_all(63)
12
+ # sleep 0.5
13
+ # device.reset()
14
+ # sleep 0.5
15
+ # device.light_all(79)
16
+ # sleep 0.5
17
+ # device.reset()
18
+ # sleep 0.5
19
+ # device.flash1(4, 4, 63)
20
+ # sleep 2
21
+ device.flashn([[1, 1],[2, 2],[3, 3],[4, 4]], 63)
22
+ device.pulsen([[4, 1],[4, 2],[4, 3],[4, 4]], 63)
23
+ sleep 2
24
+ # device.pulse(3, 3, 45)
25
+ # sleep 2
26
+ device.reset_all()
27
+ # device.pulse(3, 3, 63)
28
+ # sleep 2
29
+ # device.scroll_once(45, "Oscar is a snot monster")
30
+ # sleep 10
31
+ # device.scroll_stop()
32
+
33
+ # sleep so that the messages can be sent before the program terminates
34
+ sleep 0.1
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "launchpad_mk2/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "launchpad_mk2"
7
+ s.version = LaunchpadMk2::VERSION
8
+ s.authors = ["Andy Marks"]
9
+ s.email = ["vampwillow@gmail.com"]
10
+ s.homepage = "https://github.com/andeemarks/launchpad"
11
+ s.summary = %q{A Ruby gem for programmatically controlling the Novation Launchpad MK2.}
12
+ s.description = %q{This gem provides programmatic access to the Novation Launchpad MK2. LEDs can be lighted and button presses can be evaluated using launchpad's MIDI input/output.}
13
+ s.license = 'MIT'
14
+ s.rubyforge_project = "launchpad_mk2"
15
+
16
+ s.add_dependency "portmidi", "= 0.0.6"
17
+ s.add_dependency "ffi", "= 1.9.18"
18
+ s.add_development_dependency "rake", "~> 12"
19
+ if RUBY_VERSION < "1.9"
20
+ s.add_development_dependency "minitest"
21
+ else
22
+ s.add_development_dependency "minitest-reporters", "~> 1.1", ">= 1.1.14"
23
+ end
24
+ s.add_development_dependency "mocha", "~> 0.14", ">= 0.14.0"
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
+ s.require_paths = ["lib"]
30
+ end
@@ -0,0 +1,23 @@
1
+ require 'launchpad_mk2/interaction'
2
+
3
+ # All the fun of launchpad in one module!
4
+ #
5
+ # See Launchpad::Device for basic access to launchpad input/ouput
6
+ # and Launchpad::Interaction for advanced interaction features.
7
+ #
8
+ # The following parameters will be used throughout the library, so here are the ranges:
9
+ #
10
+ # [+type+] type of the button, one of
11
+ # <tt>
12
+ # :grid,
13
+ # :up, :down, :left, :right, :session, :user1, :user2, :mixer,
14
+ # :scene1 - :scene8
15
+ # </tt>
16
+ # [<tt>x/y</tt>] x/y coordinate (used when type is set to :grid),
17
+ # <tt>0-7</tt> (from left to right/bottom to top),
18
+ # mandatory when +type+ is set to <tt>:grid</tt>
19
+ # [<tt>color</tt>] color of the LED (value between 0 and 127 inclusive)
20
+ # optional, defaults to <tt>:off</tt>
21
+ # [+state+] whether the button is pressed or released, <tt>:down/:up</tt>
22
+ module LaunchpadMk2
23
+ end
@@ -0,0 +1,466 @@
1
+ require 'portmidi'
2
+
3
+ require 'launchpad_mk2/errors'
4
+ require 'launchpad_mk2/logging'
5
+ require 'launchpad_mk2/midi_codes'
6
+ require 'launchpad_mk2/version'
7
+
8
+ module LaunchpadMk2
9
+
10
+ # This class is used to exchange data with the launchpad.
11
+ # It provides methods to light LEDs and to get information about button presses/releases.
12
+ #
13
+ # Example:
14
+ #
15
+ # require 'launchpad_mk2/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
23
+ class Device
24
+
25
+ include Logging
26
+ include MidiCodes
27
+
28
+ MK2_DEVICE_NAME = 'Launchpad MK2 MIDI 1'
29
+
30
+ CODE_NOTE_TO_DATA_TYPE = {
31
+ [Status::ON, SceneButton::SCENE1] => :scene1,
32
+ [Status::ON, SceneButton::SCENE2] => :scene2,
33
+ [Status::ON, SceneButton::SCENE3] => :scene3,
34
+ [Status::ON, SceneButton::SCENE4] => :scene4,
35
+ [Status::ON, SceneButton::SCENE5] => :scene5,
36
+ [Status::ON, SceneButton::SCENE6] => :scene6,
37
+ [Status::ON, SceneButton::SCENE7] => :scene7,
38
+ [Status::ON, SceneButton::SCENE8] => :scene8,
39
+ [Status::CC, ControlButton::UP] => :up,
40
+ [Status::CC, ControlButton::DOWN] => :down,
41
+ [Status::CC, ControlButton::LEFT] => :left,
42
+ [Status::CC, ControlButton::RIGHT] => :right,
43
+ [Status::CC, ControlButton::SESSION] => :session,
44
+ [Status::CC, ControlButton::USER1] => :user1,
45
+ [Status::CC, ControlButton::USER2] => :user2,
46
+ [Status::CC, ControlButton::MIXER] => :mixer
47
+ }.freeze
48
+
49
+ TYPE_TO_NOTE = {
50
+ :up => ControlButton::UP,
51
+ :down => ControlButton::DOWN,
52
+ :left => ControlButton::LEFT,
53
+ :right => ControlButton::RIGHT,
54
+ :session => ControlButton::SESSION,
55
+ :user1 => ControlButton::USER1,
56
+ :user2 => ControlButton::USER2,
57
+ :mixer => ControlButton::MIXER,
58
+ :scene1 => SceneButton::SCENE1,
59
+ :scene2 => SceneButton::SCENE2,
60
+ :scene3 => SceneButton::SCENE3,
61
+ :scene4 => SceneButton::SCENE4,
62
+ :scene5 => SceneButton::SCENE5,
63
+ :scene6 => SceneButton::SCENE6,
64
+ :scene7 => SceneButton::SCENE7,
65
+ :scene8 => SceneButton::SCENE8
66
+ }.freeze
67
+
68
+ # Initializes the launchpad device. When output capabilities are requested,
69
+ # the launchpad will be reset.
70
+ #
71
+ # Optional options hash:
72
+ #
73
+ # [<tt>:input</tt>] whether to use MIDI input for user interaction,
74
+ # <tt>true/false</tt>, optional, defaults to +true+
75
+ # [<tt>:output</tt>] whether to use MIDI output for data display,
76
+ # <tt>true/false</tt>, optional, defaults to +true+
77
+ # [<tt>:input_device_id</tt>] ID of the MIDI input device to use,
78
+ # optional, <tt>:device_name</tt> will be used if omitted
79
+ # [<tt>:output_device_id</tt>] ID of the MIDI output device to use,
80
+ # optional, <tt>:device_name</tt> will be used if omitted
81
+ # [<tt>:device_name</tt>] Name of the MIDI device to use,
82
+ # optional, defaults to "Launchpad"
83
+ # [<tt>:logger</tt>] [Logger] to be used by this device instance, can be changed afterwards
84
+ #
85
+ # Errors raised:
86
+ #
87
+ # [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
88
+ # [Launchpad::DeviceBusyError] when device with ID or name specified is busy
89
+ def initialize(opts = nil)
90
+ @input = nil
91
+ @output = nil
92
+ opts = {
93
+ :input => true,
94
+ :output => true
95
+ }.merge(opts || {})
96
+
97
+ self.logger = opts[:logger]
98
+ logger.debug "initializing Launchpad::Device##{object_id} with #{opts.inspect}"
99
+
100
+ Portmidi.start
101
+
102
+ @input = create_device!(Portmidi.input_devices, Portmidi::Input,
103
+ :id => opts[:input_device_id],
104
+ :name => opts[:device_name]
105
+ ) if opts[:input]
106
+ @output = create_device!(Portmidi.output_devices, Portmidi::Output,
107
+ :id => opts[:output_device_id],
108
+ :name => opts[:device_name]
109
+ ) if opts[:output]
110
+ end
111
+
112
+ # Closes the device - nothing can be done with the device afterwards.
113
+ def close
114
+ logger.debug "closing Launchpad::Device##{object_id}"
115
+ @input.close unless @input.nil?
116
+ @input = nil
117
+ @output.close unless @output.nil?
118
+ @output = nil
119
+ end
120
+
121
+ # Determines whether this device has been closed.
122
+ def closed?
123
+ !(input_enabled? || output_enabled?)
124
+ end
125
+
126
+ # Determines whether this device can be used to read input.
127
+ def input_enabled?
128
+ !@input.nil?
129
+ end
130
+
131
+ # Determines whether this device can be used to output data.
132
+ def output_enabled?
133
+ !@output.nil?
134
+ end
135
+
136
+ # Changes a single LED.
137
+ #
138
+ # Parameters (see Launchpad for values):
139
+ #
140
+ # [+type+] type of the button to change
141
+ #
142
+ # Optional options hash (see Launchpad for values):
143
+ #
144
+ # [<tt>:x</tt>] x coordinate
145
+ # [<tt>:y</tt>] y coordinate
146
+ # [<tt>color</tt>] color of the LED (value between 0 and 127 inclusive)
147
+ # optional, defaults to <tt>:off</tt>
148
+ #
149
+ # Errors raised:
150
+ #
151
+ # [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range
152
+ # [Launchpad::NoValidColorError] when color value isn't within the valid range
153
+ # [Launchpad::NoOutputAllowedError] when output is not enabled
154
+ def change(type, opts = nil)
155
+ opts ||= {}
156
+ status = %w(up down left right session user1 user2 mixer).include?(type.to_s) ? Status::CC : Status::ON
157
+ output(status, note(type, opts), velocity(opts))
158
+ end
159
+
160
+ SYSEX_HEADER = [240, 0, 32, 41, 2, 24]
161
+ SYSEX_FOOTER = [247]
162
+
163
+ def pulse1(x, y, color_key)
164
+ note = note(:grid, {:x => x, :y => y})
165
+ output_sysex(SYSEX_HEADER + [40, 0, note, color_key] + SYSEX_FOOTER)
166
+ end
167
+
168
+ def pulsen(notes, color_key)
169
+ notes.each { |coord|
170
+ note = note(:grid, {:x => coord[0], :y => coord[1]})
171
+ output_sysex(SYSEX_HEADER + [40, 0, note, color_key] + SYSEX_FOOTER)
172
+ }
173
+ end
174
+
175
+ def flash1(x, y, color_key)
176
+ note = note(:grid, {:x => x, :y => y})
177
+ output_sysex(SYSEX_HEADER + [35, 0, note, color_key] + SYSEX_FOOTER)
178
+ end
179
+
180
+ def flashn(notes, color_key)
181
+ notes.each { |coord|
182
+ note = note(:grid, {:x => coord[0], :y => coord[1]})
183
+ output_sysex(SYSEX_HEADER + [35, 0, note, color_key, 0] + SYSEX_FOOTER)
184
+ }
185
+ end
186
+
187
+ def scroll(color_key, text, mode)
188
+ output_sysex(SYSEX_HEADER + [20, color_key, mode] + text.chars.map(&:ord) + SYSEX_FOOTER)
189
+ end
190
+
191
+ def scroll_forever(color_key, text)
192
+ scroll(color_key, text, 1)
193
+ end
194
+
195
+ def scroll_once(color_key, text)
196
+ scroll(color_key, text, 0)
197
+ end
198
+
199
+ def scroll_stop()
200
+ output_sysex(SYSEX_HEADER + [20] + SYSEX_FOOTER)
201
+ end
202
+
203
+ def light_all(color_key)
204
+ output_sysex(SYSEX_HEADER + [14, color_key] + SYSEX_FOOTER)
205
+ end
206
+
207
+ def reset_all()
208
+ light_all(0)
209
+ end
210
+
211
+ def lightn_column(column_keys, color_key)
212
+ column_keys.each { |column_key|
213
+ output_sysex(SYSEX_HEADER + [12, column_key, color_key] + SYSEX_FOOTER)
214
+ }
215
+ end
216
+
217
+ def light1_column(column_key, color_key)
218
+ output_sysex(SYSEX_HEADER + [12, column_key, color_key] + SYSEX_FOOTER)
219
+ end
220
+
221
+ def lightn_row(rows_keys, color_key)
222
+ rows_keys.each { |row_key|
223
+ output_sysex(SYSEX_HEADER + [13, row_key, color_key] + SYSEX_FOOTER)
224
+ }
225
+ end
226
+
227
+ def light1_row(row_key, color_key)
228
+ output_sysex(SYSEX_HEADER + [13, row_key, color_key] + SYSEX_FOOTER)
229
+ end
230
+
231
+ # Reads user actions (button presses/releases) that haven't been handled yet.
232
+ # This is non-blocking, so when nothing happend yet you'll get an empty array.
233
+ #
234
+ # Returns:
235
+ #
236
+ # an array of hashes with (see Launchpad for values):
237
+ #
238
+ # [<tt>:timestamp</tt>] integer indicating the time when the action occured
239
+ # [<tt>:state</tt>] state of the button after action
240
+ # [<tt>:type</tt>] type of the button
241
+ # [<tt>:x</tt>] x coordinate
242
+ # [<tt>:y</tt>] y coordinate
243
+ #
244
+ # Errors raised:
245
+ #
246
+ # [Launchpad::NoInputAllowedError] when input is not enabled
247
+ def read_pending_actions
248
+ Array(input).collect do |midi_message|
249
+ (code, note, velocity) = midi_message[:message]
250
+ data = {
251
+ :timestamp => midi_message[:timestamp],
252
+ :state => (velocity == 127 ? :down : :up)
253
+ }
254
+ data[:type] = CODE_NOTE_TO_DATA_TYPE[[code, note]] || :grid
255
+ if data[:type] == :grid
256
+ data[:x] = (note % 10) - 1
257
+ data[:y] = (note / 10) - 1
258
+ end
259
+ data
260
+ end
261
+ end
262
+
263
+ private
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 create_device!(devices, device_type, opts)
288
+ logger.debug "creating #{device_type} with #{opts.inspect}, choosing from portmidi devices #{devices.inspect}"
289
+ id = opts[:id]
290
+ if id.nil?
291
+ name = opts[:name] || MK2_DEVICE_NAME
292
+ device = devices.select {|current_device| current_device.name == name}.first
293
+ id = device.device_id unless device.nil?
294
+ end
295
+ if id.nil?
296
+ message = "MIDI device #{opts[:id] || opts[:name]} doesn't exist"
297
+ logger.fatal message
298
+ raise NoSuchDeviceError.new(message)
299
+ end
300
+ device_type.new(id)
301
+ rescue RuntimeError => e
302
+ logger.fatal "error creating #{device_type}: #{e.inspect}"
303
+ raise DeviceBusyError.new(e)
304
+ end
305
+
306
+ # Reads input from the MIDI device.
307
+ #
308
+ # Returns:
309
+ #
310
+ # an array of hashes with:
311
+ #
312
+ # [<tt>:message</tt>] an array of
313
+ # MIDI status code,
314
+ # MIDI data 1 (note),
315
+ # MIDI data 2 (velocity)
316
+ # and a fourth value
317
+ # [<tt>:timestamp</tt>] integer indicating the time when the MIDI message was created
318
+ #
319
+ # Errors raised:
320
+ #
321
+ # [Launchpad::NoInputAllowedError] when output is not enabled
322
+ def input
323
+ if @input.nil?
324
+ logger.error "trying to read from device that's not been initialized for input"
325
+ raise NoInputAllowedError
326
+ end
327
+ @input.read(16)
328
+ end
329
+
330
+ # Writes data to the MIDI device.
331
+ #
332
+ # Parameters:
333
+ #
334
+ # [+status+] MIDI status code
335
+ # [+data1+] MIDI data 1 (note)
336
+ # [+data2+] MIDI data 2 (velocity)
337
+ #
338
+ # Errors raised:
339
+ #
340
+ # [Launchpad::NoOutputAllowedError] when output is not enabled
341
+ def output(status, data1, data2)
342
+ output_messages([message(status, data1, data2)])
343
+ end
344
+
345
+ # Writes several messages to the MIDI device.
346
+ #
347
+ # Parameters:
348
+ #
349
+ # [+messages+] an array of hashes (usually created with message) with:
350
+ # [<tt>:message</tt>] an array of
351
+ # MIDI status code,
352
+ # MIDI data 1 (note),
353
+ # MIDI data 2 (velocity)
354
+ # [<tt>:timestamp</tt>] integer indicating the time when the MIDI message was created
355
+ def output_messages(messages)
356
+ if @output.nil?
357
+ logger.error "trying to write to device that's not been initialized for output"
358
+ raise NoOutputAllowedError
359
+ end
360
+ logger.debug "writing messages to launchpad:\n #{messages.join("\n ")}" if logger.debug?
361
+ @output.write(messages)
362
+ nil
363
+ end
364
+
365
+ def output_sysex(messages)
366
+ if @output.nil?
367
+ logger.error "trying to write to device that's not been initialized for output"
368
+ raise NoOutputAllowedError
369
+ end
370
+ logger.debug "writing sysex to launchpad:\n #{messages.join("\n ")}" if logger.debug?
371
+ @output.write_sysex(messages)
372
+ nil
373
+ end
374
+
375
+ # Calculates the MIDI data 1 value (note) for a button.
376
+ #
377
+ # Parameters (see Launchpad for values):
378
+ #
379
+ # [+type+] type of the button
380
+ #
381
+ # Options hash:
382
+ #
383
+ # [<tt>:x</tt>] x coordinate
384
+ # [<tt>:y</tt>] y coordinate
385
+ #
386
+ # Returns:
387
+ #
388
+ # integer to be used for MIDI data 1
389
+ #
390
+ # Errors raised:
391
+ #
392
+ # [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range
393
+ def note(type, opts)
394
+ note = TYPE_TO_NOTE[type]
395
+ if note.nil?
396
+ x = (opts[:x] || -1).to_i
397
+ y = (opts[:y] || -1).to_i
398
+ if x < 0 || x > 7 || y < 0 || y > 7
399
+ logger.error "wrong coordinates specified: x=#{x}, y=#{y}"
400
+ raise NoValidGridCoordinatesError.new("you need to specify valid coordinates (x/y, 0-7, from top left), you specified: x=#{x}, y=#{y}")
401
+ end
402
+ note = (y + 1) * 10 + (x + 1)
403
+ end
404
+ note
405
+ end
406
+
407
+ # Calculates the MIDI data 2 value (velocity) for given brightness and mode values.
408
+ #
409
+ # Options hash:
410
+ #
411
+ # [<tt>color</tt>] color of the LED (value between 0 and 127 inclusive)
412
+ # optional, defaults to <tt>:off</tt>
413
+ #
414
+ # Returns:
415
+ #
416
+ # integer to be used for MIDI data 2
417
+ def velocity(opts)
418
+ if opts.is_a?(Hash)
419
+ color = color(opts[:color]) || 0
420
+ else
421
+ opts.to_i + 12
422
+ end
423
+ end
424
+
425
+ def color(color_key)
426
+ if color_key.nil?
427
+ 0
428
+ else
429
+ if (not (color_key.is_a? Integer))
430
+ logger.error "wrong color specified: color_key=#{color_key}"
431
+ raise NoValidColorError.new("you need to specify a valid color (0-127), you specified: color_key=#{color_key}")
432
+ end
433
+
434
+ if color_key < 0 || color_key > 127
435
+ logger.error "wrong color specified: color_key=#{color_key}"
436
+ raise NoValidColorError.new("you need to specify a valid color (0-127), you specified: color_key=#{color_key}")
437
+ end
438
+
439
+ color_key
440
+ end
441
+ end
442
+
443
+ # Creates a MIDI message.
444
+ #
445
+ # Parameters:
446
+ #
447
+ # [+status+] MIDI status code
448
+ # [+data1+] MIDI data 1 (note)
449
+ # [+data2+] MIDI data 2 (velocity)
450
+ #
451
+ # Returns:
452
+ #
453
+ # an array with:
454
+ #
455
+ # [<tt>:message</tt>] an array of
456
+ # MIDI status code,
457
+ # MIDI data 1 (note),
458
+ # MIDI data 2 (velocity)
459
+ # [<tt>:timestamp</tt>] integer indicating the time when the MIDI message was created, in this case 0
460
+ def message(status, data1, data2)
461
+ {:message => [status, data1, data2], :timestamp => 0}
462
+ end
463
+
464
+ end
465
+
466
+ end