launchpad_mk2 0.0.1

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