mrjoy-launchpad 0.4.0
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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +8 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +136 -0
- data/Rakefile +10 -0
- data/examples/binary_clock.rb +29 -0
- data/examples/color_picker.rb +96 -0
- data/examples/colors.rb +21 -0
- data/examples/doodle.rb +68 -0
- data/examples/double_buffering.rb +104 -0
- data/examples/drawing_board.rb +25 -0
- data/examples/feedback.rb +34 -0
- data/examples/reset.rb +6 -0
- data/launchpad.gemspec +34 -0
- data/lib/launchpad.rb +34 -0
- data/lib/launchpad/device.rb +574 -0
- data/lib/launchpad/errors.rb +37 -0
- data/lib/launchpad/interaction.rb +336 -0
- data/lib/launchpad/logging.rb +27 -0
- data/lib/launchpad/midi_codes.rb +53 -0
- data/lib/launchpad/version.rb +3 -0
- data/monitor.rb +88 -0
- data/test/helper.rb +44 -0
- data/test/test_device.rb +530 -0
- data/test/test_interaction.rb +456 -0
- data/testbed.rb +48 -0
- metadata +146 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'launchpad'
|
2
|
+
|
3
|
+
interaction = Launchpad::Interaction.new
|
4
|
+
|
5
|
+
flags = Hash.new(false)
|
6
|
+
|
7
|
+
# yellow feedback for grid buttons
|
8
|
+
interaction.response_to(:grid, :down) do |interaction, action|
|
9
|
+
coord = 16 * action[:y] + action[:x]
|
10
|
+
brightness = flags[coord] ? :off : :hi
|
11
|
+
flags[coord] = !flags[coord]
|
12
|
+
interaction.device.change(:grid, action.merge(:red => brightness, :green => brightness))
|
13
|
+
end
|
14
|
+
|
15
|
+
# mixer button terminates interaction on button up
|
16
|
+
interaction.response_to(:mixer) do |interaction, action|
|
17
|
+
interaction.device.change(:mixer, :red => action[:state] == :down ? :hi : :off)
|
18
|
+
interaction.stop if action[:state] == :up
|
19
|
+
end
|
20
|
+
|
21
|
+
# start interacting
|
22
|
+
interaction.start
|
23
|
+
|
24
|
+
# sleep so that the messages can be sent before the program terminates
|
25
|
+
sleep 0.1
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'launchpad'
|
2
|
+
|
3
|
+
interaction = Launchpad::Interaction.new
|
4
|
+
|
5
|
+
def brightness(action)
|
6
|
+
action[:state] == :down ? :hi : :off
|
7
|
+
end
|
8
|
+
|
9
|
+
# yellow feedback for grid buttons
|
10
|
+
interaction.response_to(:grid) do |interaction, action|
|
11
|
+
b = brightness(action)
|
12
|
+
interaction.device.change(:grid, action.merge(:red => b, :green => b))
|
13
|
+
end
|
14
|
+
|
15
|
+
# red feedback for top control buttons
|
16
|
+
interaction.response_to([:up, :down, :left, :right, :session, :user1, :user2, :mixer]) do |interaction, action|
|
17
|
+
interaction.device.change(action[:type], :red => brightness(action))
|
18
|
+
end
|
19
|
+
|
20
|
+
# green feedback for scene buttons
|
21
|
+
interaction.response_to([:scene1, :scene2, :scene3, :scene4, :scene5, :scene6, :scene7, :scene8]) do |interaction, action|
|
22
|
+
interaction.device.change(action[:type], :green => brightness(action))
|
23
|
+
end
|
24
|
+
|
25
|
+
# mixer button terminates interaction on button up
|
26
|
+
interaction.response_to(:mixer, :up) do |interaction, action|
|
27
|
+
interaction.stop
|
28
|
+
end
|
29
|
+
|
30
|
+
# start interacting
|
31
|
+
interaction.start
|
32
|
+
|
33
|
+
# sleep so that the messages can be sent before the program terminates
|
34
|
+
sleep 0.1
|
data/examples/reset.rb
ADDED
data/launchpad.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "launchpad/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "mrjoy-launchpad"
|
7
|
+
s.version = Launchpad::VERSION
|
8
|
+
s.authors = ["Jon Frisby", "Thomas Jachmann"]
|
9
|
+
s.email = ["jfrisby@mrjoy.com", "self@thomasjachmann.com"]
|
10
|
+
s.homepage = "https://github.com/MrJoy/launchpad"
|
11
|
+
s.summary = %q{A gem for accessing Novation's LaunchPad programmatically and easily.}
|
12
|
+
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.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "launchpad"
|
15
|
+
|
16
|
+
s.add_dependency "portmidi", ">= 0.0.6"
|
17
|
+
s.add_dependency "ffi"
|
18
|
+
s.add_development_dependency "rake"
|
19
|
+
if RUBY_VERSION < "1.9"
|
20
|
+
s.add_development_dependency "minitest"
|
21
|
+
# s.add_development_dependency "ruby-debug"
|
22
|
+
else
|
23
|
+
s.add_development_dependency "minitest-reporters"
|
24
|
+
# s.add_development_dependency "debugger"
|
25
|
+
end
|
26
|
+
s.add_development_dependency "mocha"
|
27
|
+
|
28
|
+
# s.has_rdoc = true
|
29
|
+
|
30
|
+
s.files = `git ls-files`.split("\n")
|
31
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
32
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
end
|
data/lib/launchpad.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'launchpad/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/top to bottom),
|
18
|
+
# mandatory when +type+ is set to <tt>:grid</tt>
|
19
|
+
# [<tt>red/green</tt>] brightness of the red/green LED,
|
20
|
+
# can be set to one of four levels:
|
21
|
+
# * off (<tt>:off, 0</tt>)
|
22
|
+
# * low brightness (<tt>:low, :lo, 1</tt>)
|
23
|
+
# * medium brightness (<tt>:medium, :med, 2</tt>)
|
24
|
+
# * full brightness (<tt>:high, :hi, 3</tt>)
|
25
|
+
# optional, defaults to <tt>:off</tt>
|
26
|
+
# [+mode+] button mode,
|
27
|
+
# one of
|
28
|
+
# * <tt>:normal</tt>
|
29
|
+
# * <tt>:flashing</tt> (LED is marked as flashing, see Launchpad::Device.flashing_on, Launchpad::Device.flashing_off and Launchpad::Device.flashing_auto)
|
30
|
+
# * <tt>:buffering</tt> (LED is written to buffer, see Launchpad::Device.start_buffering, Launchpad::Device.flush_buffer)
|
31
|
+
# optional, defaults to <tt>:normal</tt>
|
32
|
+
# [+state+] whether the button is pressed or released, <tt>:down/:up</tt>
|
33
|
+
module Launchpad
|
34
|
+
end
|
@@ -0,0 +1,574 @@
|
|
1
|
+
require 'portmidi'
|
2
|
+
|
3
|
+
require 'launchpad/errors'
|
4
|
+
require 'launchpad/logging'
|
5
|
+
require 'launchpad/midi_codes'
|
6
|
+
require 'launchpad/version'
|
7
|
+
|
8
|
+
module Launchpad
|
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/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
|
+
CODE_NOTE_TO_DATA_TYPE = {
|
29
|
+
[Status::ON, SceneButton::SCENE1] => :scene1,
|
30
|
+
[Status::ON, SceneButton::SCENE2] => :scene2,
|
31
|
+
[Status::ON, SceneButton::SCENE3] => :scene3,
|
32
|
+
[Status::ON, SceneButton::SCENE4] => :scene4,
|
33
|
+
[Status::ON, SceneButton::SCENE5] => :scene5,
|
34
|
+
[Status::ON, SceneButton::SCENE6] => :scene6,
|
35
|
+
[Status::ON, SceneButton::SCENE7] => :scene7,
|
36
|
+
[Status::ON, SceneButton::SCENE8] => :scene8,
|
37
|
+
[Status::CC, ControlButton::UP] => :up,
|
38
|
+
[Status::CC, ControlButton::DOWN] => :down,
|
39
|
+
[Status::CC, ControlButton::LEFT] => :left,
|
40
|
+
[Status::CC, ControlButton::RIGHT] => :right,
|
41
|
+
[Status::CC, ControlButton::SESSION] => :session,
|
42
|
+
[Status::CC, ControlButton::USER1] => :user1,
|
43
|
+
[Status::CC, ControlButton::USER2] => :user2,
|
44
|
+
[Status::CC, ControlButton::MIXER] => :mixer
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
# TODO: Rename scenes to match Mk2
|
48
|
+
TYPE_TO_NOTE = {
|
49
|
+
:up => ControlButton::UP,
|
50
|
+
:down => ControlButton::DOWN,
|
51
|
+
:left => ControlButton::LEFT,
|
52
|
+
:right => ControlButton::RIGHT,
|
53
|
+
:session => ControlButton::SESSION,
|
54
|
+
:user1 => ControlButton::USER1,
|
55
|
+
:user2 => ControlButton::USER2,
|
56
|
+
:mixer => ControlButton::MIXER,
|
57
|
+
:scene1 => SceneButton::SCENE1,
|
58
|
+
:scene2 => SceneButton::SCENE2,
|
59
|
+
:scene3 => SceneButton::SCENE3,
|
60
|
+
:scene4 => SceneButton::SCENE4,
|
61
|
+
:scene5 => SceneButton::SCENE5,
|
62
|
+
:scene6 => SceneButton::SCENE6,
|
63
|
+
:scene7 => SceneButton::SCENE7,
|
64
|
+
:scene8 => SceneButton::SCENE8
|
65
|
+
}.freeze
|
66
|
+
|
67
|
+
# Initializes the launchpad device. When output capabilities are requested,
|
68
|
+
# the launchpad will be reset.
|
69
|
+
#
|
70
|
+
# Optional options hash:
|
71
|
+
#
|
72
|
+
# [<tt>:input</tt>] whether to use MIDI input for user interaction,
|
73
|
+
# <tt>true/false</tt>, optional, defaults to +true+
|
74
|
+
# [<tt>:output</tt>] whether to use MIDI output for data display,
|
75
|
+
# <tt>true/false</tt>, optional, defaults to +true+
|
76
|
+
# [<tt>:input_device_id</tt>] ID of the MIDI input device to use,
|
77
|
+
# optional, <tt>:device_name</tt> will be used if omitted
|
78
|
+
# [<tt>:output_device_id</tt>] ID of the MIDI output device to use,
|
79
|
+
# optional, <tt>:device_name</tt> will be used if omitted
|
80
|
+
# [<tt>:device_name</tt>] Name of the MIDI device to use,
|
81
|
+
# optional, defaults to "Launchpad"
|
82
|
+
# [<tt>:logger</tt>] [Logger] to be used by this device instance, can be changed afterwards
|
83
|
+
#
|
84
|
+
# Errors raised:
|
85
|
+
#
|
86
|
+
# [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
|
87
|
+
# [Launchpad::DeviceBusyError] when device with ID or name specified is busy
|
88
|
+
def initialize(opts = nil)
|
89
|
+
opts = {
|
90
|
+
:input => true,
|
91
|
+
:output => true
|
92
|
+
}.merge(opts || {})
|
93
|
+
|
94
|
+
self.logger = opts[:logger]
|
95
|
+
logger.debug "initializing Launchpad::Device##{object_id} with #{opts.inspect}"
|
96
|
+
|
97
|
+
Portmidi.start
|
98
|
+
|
99
|
+
@input = create_device!(Portmidi.input_devices, Portmidi::Input,
|
100
|
+
:id => opts[:input_device_id],
|
101
|
+
:name => opts[:device_name]
|
102
|
+
) if opts[:input]
|
103
|
+
@output = create_device!(Portmidi.output_devices, Portmidi::Output,
|
104
|
+
:id => opts[:output_device_id],
|
105
|
+
:name => opts[:device_name]
|
106
|
+
) if opts[:output]
|
107
|
+
|
108
|
+
reset if output_enabled?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Closes the device - nothing can be done with the device afterwards.
|
112
|
+
def close
|
113
|
+
logger.debug "closing Launchpad::Device##{object_id}"
|
114
|
+
@input.close unless @input.nil?
|
115
|
+
@input = nil
|
116
|
+
@output.close unless @output.nil?
|
117
|
+
@output = nil
|
118
|
+
end
|
119
|
+
|
120
|
+
# Determines whether this device has been closed.
|
121
|
+
def closed?
|
122
|
+
!(input_enabled? || output_enabled?)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Determines whether this device can be used to read input.
|
126
|
+
def input_enabled?
|
127
|
+
!@input.nil?
|
128
|
+
end
|
129
|
+
|
130
|
+
# Determines whether this device can be used to output data.
|
131
|
+
def output_enabled?
|
132
|
+
!@output.nil?
|
133
|
+
end
|
134
|
+
|
135
|
+
# Resets the launchpad - all settings are reset and all LEDs are switched off.
|
136
|
+
#
|
137
|
+
# Errors raised:
|
138
|
+
#
|
139
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
140
|
+
def reset
|
141
|
+
output(Status::CC, Status::NIL, Status::NIL)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Lights all LEDs (for testing purposes).
|
145
|
+
#
|
146
|
+
# Parameters (see Launchpad for values):
|
147
|
+
#
|
148
|
+
# [+brightness+] brightness of both LEDs for all buttons
|
149
|
+
#
|
150
|
+
# Errors raised:
|
151
|
+
#
|
152
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
153
|
+
# def test_leds(brightness = :high)
|
154
|
+
# brightness = brightness(brightness)
|
155
|
+
# if brightness == 0
|
156
|
+
# reset
|
157
|
+
# else
|
158
|
+
# output(Status::CC, Status::NIL, Velocity::TEST_LEDS + brightness)
|
159
|
+
# end
|
160
|
+
# end
|
161
|
+
|
162
|
+
# Changes a single LED.
|
163
|
+
#
|
164
|
+
# Parameters (see Launchpad for values):
|
165
|
+
#
|
166
|
+
# [+type+] type of the button to change
|
167
|
+
#
|
168
|
+
# Optional options hash (see Launchpad for values):
|
169
|
+
#
|
170
|
+
# [<tt>:x</tt>] x coordinate
|
171
|
+
# [<tt>:y</tt>] y coordinate
|
172
|
+
# [<tt>:red</tt>] brightness of red LED
|
173
|
+
# [<tt>:green</tt>] brightness of green LED
|
174
|
+
# [<tt>:mode</tt>] button mode, defaults to <tt>:normal</tt>, one of:
|
175
|
+
# [<tt>:normal/tt>] updates the LED for all circumstances (the new value will be written to both buffers)
|
176
|
+
# [<tt>:flashing/tt>] updates the LED for flashing (the new value will be written to buffer 0 while the LED will be off in buffer 1, see buffering_mode)
|
177
|
+
# [<tt>:buffering/tt>] updates the LED for the current update_buffer only
|
178
|
+
#
|
179
|
+
# Errors raised:
|
180
|
+
#
|
181
|
+
# [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range
|
182
|
+
# [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
|
183
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
184
|
+
# def change(type, opts = nil)
|
185
|
+
# opts ||= {}
|
186
|
+
# status = %w(up down left right session user1 user2 mixer).include?(type.to_s) ? Status::CC : Status::ON
|
187
|
+
# output(status, note(type, opts), velocity(opts))
|
188
|
+
# end
|
189
|
+
def change_grid(x, y, r, g, b)
|
190
|
+
led = (y * 10) + x + 11
|
191
|
+
@output.write_sysex([
|
192
|
+
# SysEx Begin:
|
193
|
+
0xF0,
|
194
|
+
# Manufacturer/Device:
|
195
|
+
0x00,
|
196
|
+
0x20,
|
197
|
+
0x29,
|
198
|
+
0x02,
|
199
|
+
0x18,
|
200
|
+
# Command:
|
201
|
+
0x0B,
|
202
|
+
# LED:
|
203
|
+
led,
|
204
|
+
# Red, Green, Blue:
|
205
|
+
r,
|
206
|
+
g,
|
207
|
+
b,
|
208
|
+
# SysEx End:
|
209
|
+
0xF7,
|
210
|
+
])
|
211
|
+
end
|
212
|
+
|
213
|
+
def change_command(position, r, g, b)
|
214
|
+
led = TYPE_TO_NOTE[position]
|
215
|
+
@output.write_sysex([
|
216
|
+
# SysEx Begin:
|
217
|
+
0xF0,
|
218
|
+
# Manufacturer/Device:
|
219
|
+
0x00,
|
220
|
+
0x20,
|
221
|
+
0x29,
|
222
|
+
0x02,
|
223
|
+
0x18,
|
224
|
+
# Command:
|
225
|
+
0x0B,
|
226
|
+
# LED:
|
227
|
+
led,
|
228
|
+
# Red, Green, Blue:
|
229
|
+
r,
|
230
|
+
g,
|
231
|
+
b,
|
232
|
+
# SysEx End:
|
233
|
+
0xF7,
|
234
|
+
])
|
235
|
+
end
|
236
|
+
|
237
|
+
# Changes all LEDs in batch mode.
|
238
|
+
#
|
239
|
+
# Parameters (see Launchpad for values):
|
240
|
+
#
|
241
|
+
# [+colors] an array of colors, each either being an integer or a Hash
|
242
|
+
# * integer: calculated using the formula
|
243
|
+
# <tt>color = 16 * green + red</tt>
|
244
|
+
# * Hash:
|
245
|
+
# [<tt>:red</tt>] brightness of red LED
|
246
|
+
# [<tt>:green</tt>] brightness of green LED
|
247
|
+
# [<tt>:mode</tt>] button mode, defaults to <tt>:normal</tt>, one of:
|
248
|
+
# [<tt>:normal/tt>] updates the LEDs for all circumstances (the new value will be written to both buffers)
|
249
|
+
# [<tt>:flashing/tt>] updates the LEDs for flashing (the new values will be written to buffer 0 while the LEDs will be off in buffer 1, see buffering_mode)
|
250
|
+
# [<tt>:buffering/tt>] updates the LEDs for the current update_buffer only
|
251
|
+
# the array consists of 64 colors for the grid buttons,
|
252
|
+
# 8 colors for the scene buttons (top to bottom)
|
253
|
+
# and 8 colors for the top control buttons (left to right),
|
254
|
+
# maximum 80 values - excessive values will be ignored,
|
255
|
+
# missing values will be filled with 0
|
256
|
+
#
|
257
|
+
# Errors raised:
|
258
|
+
#
|
259
|
+
# [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
|
260
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
261
|
+
# def change_all(*colors)
|
262
|
+
# # ensure that colors is at least and most 80 elements long
|
263
|
+
# colors = colors.flatten[0..79]
|
264
|
+
# colors += [0] * (80 - colors.size) if colors.size < 80
|
265
|
+
# # send normal MIDI message to reset rapid LED change pointer
|
266
|
+
# # in this case, set mapping mode to x-y layout (the default)
|
267
|
+
# output(Status::CC, Status::NIL, GridLayout::XY)
|
268
|
+
# # send colors in slices of 2
|
269
|
+
# messages = []
|
270
|
+
# colors.each_slice(2) do |c1, c2|
|
271
|
+
# messages << message(Status::MULTI, velocity(c1), velocity(c2))
|
272
|
+
# end
|
273
|
+
# output_messages(messages)
|
274
|
+
# end
|
275
|
+
|
276
|
+
# Switches LEDs marked as flashing on when using custom timer for flashing.
|
277
|
+
#
|
278
|
+
# Errors raised:
|
279
|
+
#
|
280
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
281
|
+
# def flashing_on
|
282
|
+
# buffering_mode(:display_buffer => 0)
|
283
|
+
# end
|
284
|
+
|
285
|
+
# Switches LEDs marked as flashing off when using custom timer for flashing.
|
286
|
+
#
|
287
|
+
# Errors raised:
|
288
|
+
#
|
289
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
290
|
+
# def flashing_off
|
291
|
+
# buffering_mode(:display_buffer => 1)
|
292
|
+
# end
|
293
|
+
|
294
|
+
# Starts flashing LEDs marked as flashing automatically.
|
295
|
+
# Stop flashing by calling flashing_on or flashing_off.
|
296
|
+
#
|
297
|
+
# Errors raised:
|
298
|
+
#
|
299
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
300
|
+
# def flashing_auto
|
301
|
+
# buffering_mode(:flashing => true)
|
302
|
+
# end
|
303
|
+
|
304
|
+
# Controls the two buffers.
|
305
|
+
#
|
306
|
+
# Optional options hash:
|
307
|
+
#
|
308
|
+
# [<tt>:display_buffer</tt>] which buffer to use for display, defaults to +0+
|
309
|
+
# [<tt>:update_buffer</tt>] which buffer to use for updates when <tt>:mode</tt> is set to <tt>:buffering</tt>, defaults to +0+ (see change)
|
310
|
+
# [<tt>:copy</tt>] whether to copy the LEDs states from the new display_buffer over to the new update_buffer, <tt>true/false</tt>, defaults to <tt>false</tt>
|
311
|
+
# [<tt>:flashing</tt>] whether to start flashing by automatically switching between the two buffers for display, <tt>true/false</tt>, defaults to <tt>false</tt>
|
312
|
+
#
|
313
|
+
# Errors raised:
|
314
|
+
#
|
315
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
316
|
+
# def buffering_mode(opts = nil)
|
317
|
+
# opts = {
|
318
|
+
# :display_buffer => 0,
|
319
|
+
# :update_buffer => 0,
|
320
|
+
# :copy => false,
|
321
|
+
# :flashing => false
|
322
|
+
# }.merge(opts || {})
|
323
|
+
# data = opts[:display_buffer] + 4 * opts[:update_buffer] + 32
|
324
|
+
# data += 16 if opts[:copy]
|
325
|
+
# data += 8 if opts[:flashing]
|
326
|
+
# output(Status::CC, Status::NIL, data)
|
327
|
+
# end
|
328
|
+
|
329
|
+
# Reads user actions (button presses/releases) that haven't been handled yet.
|
330
|
+
# This is non-blocking, so when nothing happend yet you'll get an empty array.
|
331
|
+
#
|
332
|
+
# Returns:
|
333
|
+
#
|
334
|
+
# an array of hashes with (see Launchpad for values):
|
335
|
+
#
|
336
|
+
# [<tt>:timestamp</tt>] integer indicating the time when the action occured
|
337
|
+
# [<tt>:state</tt>] state of the button after action
|
338
|
+
# [<tt>:type</tt>] type of the button
|
339
|
+
# [<tt>:x</tt>] x coordinate
|
340
|
+
# [<tt>:y</tt>] y coordinate
|
341
|
+
#
|
342
|
+
# Errors raised:
|
343
|
+
#
|
344
|
+
# [Launchpad::NoInputAllowedError] when input is not enabled
|
345
|
+
def read_pending_actions
|
346
|
+
Array(input).collect do |midi_message|
|
347
|
+
(code, note, velocity) = midi_message[:message]
|
348
|
+
data = {
|
349
|
+
:timestamp => midi_message[:timestamp],
|
350
|
+
:state => (velocity == 127 ? :down : :up)
|
351
|
+
}
|
352
|
+
data[:type] = CODE_NOTE_TO_DATA_TYPE[[code, note]] || :grid
|
353
|
+
if data[:type] == :grid
|
354
|
+
note = note - 11
|
355
|
+
data[:x] = note % 10
|
356
|
+
data[:y] = note / 10
|
357
|
+
end
|
358
|
+
data
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
private
|
363
|
+
|
364
|
+
# Creates input/output devices.
|
365
|
+
#
|
366
|
+
# Parameters:
|
367
|
+
#
|
368
|
+
# [+devices+] array of portmidi devices
|
369
|
+
# [+device_type] class to instantiate (<tt>Portmidi::Input/Portmidi::Output</tt>)
|
370
|
+
#
|
371
|
+
# Options hash:
|
372
|
+
#
|
373
|
+
# [<tt>:id</tt>] id of the MIDI device to use
|
374
|
+
# [<tt>:name</tt>] name of the MIDI device to use,
|
375
|
+
# only used when <tt>:id</tt> is not specified,
|
376
|
+
# defaults to "Launchpad"
|
377
|
+
#
|
378
|
+
# Returns:
|
379
|
+
#
|
380
|
+
# newly created device
|
381
|
+
#
|
382
|
+
# Errors raised:
|
383
|
+
#
|
384
|
+
# [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
|
385
|
+
# [Launchpad::DeviceBusyError] when device with ID or name specified is busy
|
386
|
+
def create_device!(devices, device_type, opts)
|
387
|
+
logger.debug "creating #{device_type} with #{opts.inspect}, choosing from portmidi devices #{devices.inspect}"
|
388
|
+
id = opts[:id]
|
389
|
+
if id.nil?
|
390
|
+
name = opts[:name] || "Launchpad MK2"
|
391
|
+
device = devices.select {|dev| dev.name == name}.first
|
392
|
+
id = device.device_id unless device.nil?
|
393
|
+
end
|
394
|
+
if id.nil?
|
395
|
+
message = "MIDI device #{opts[:id] || opts[:name]} doesn't exist"
|
396
|
+
logger.fatal message
|
397
|
+
raise NoSuchDeviceError.new(message)
|
398
|
+
end
|
399
|
+
device_type.new(id)
|
400
|
+
rescue RuntimeError => e
|
401
|
+
logger.fatal "error creating #{device_type}: #{e.inspect}"
|
402
|
+
raise DeviceBusyError.new(e)
|
403
|
+
end
|
404
|
+
|
405
|
+
# Reads input from the MIDI device.
|
406
|
+
#
|
407
|
+
# Returns:
|
408
|
+
#
|
409
|
+
# an array of hashes with:
|
410
|
+
#
|
411
|
+
# [<tt>:message</tt>] an array of
|
412
|
+
# MIDI status code,
|
413
|
+
# MIDI data 1 (note),
|
414
|
+
# MIDI data 2 (velocity)
|
415
|
+
# and a fourth value
|
416
|
+
# [<tt>:timestamp</tt>] integer indicating the time when the MIDI message was created
|
417
|
+
#
|
418
|
+
# Errors raised:
|
419
|
+
#
|
420
|
+
# [Launchpad::NoInputAllowedError] when output is not enabled
|
421
|
+
def input
|
422
|
+
if @input.nil?
|
423
|
+
logger.error "trying to read from device that's not been initialized for input"
|
424
|
+
raise NoInputAllowedError
|
425
|
+
end
|
426
|
+
@input.read(16)
|
427
|
+
end
|
428
|
+
|
429
|
+
# Writes data to the MIDI device.
|
430
|
+
#
|
431
|
+
# Parameters:
|
432
|
+
#
|
433
|
+
# [+status+] MIDI status code
|
434
|
+
# [+data1+] MIDI data 1 (note)
|
435
|
+
# [+data2+] MIDI data 2 (velocity)
|
436
|
+
#
|
437
|
+
# Errors raised:
|
438
|
+
#
|
439
|
+
# [Launchpad::NoOutputAllowedError] when output is not enabled
|
440
|
+
def output(status, data1, data2)
|
441
|
+
output_messages([message(status, data1, data2)])
|
442
|
+
end
|
443
|
+
|
444
|
+
# Writes several messages to the MIDI device.
|
445
|
+
#
|
446
|
+
# Parameters:
|
447
|
+
#
|
448
|
+
# [+messages+] an array of hashes (usually created with message) with:
|
449
|
+
# [<tt>:message</tt>] an array of
|
450
|
+
# MIDI status code,
|
451
|
+
# MIDI data 1 (note),
|
452
|
+
# MIDI data 2 (velocity)
|
453
|
+
# [<tt>:timestamp</tt>] integer indicating the time when the MIDI message was created
|
454
|
+
def output_messages(messages)
|
455
|
+
if @output.nil?
|
456
|
+
logger.error "trying to write to device that's not been initialized for output"
|
457
|
+
raise NoOutputAllowedError
|
458
|
+
end
|
459
|
+
logger.debug "writing messages to launchpad:\n #{messages.join("\n ")}" if logger.debug?
|
460
|
+
@output.write(messages)
|
461
|
+
nil
|
462
|
+
end
|
463
|
+
|
464
|
+
# Calculates the MIDI data 1 value (note) for a button.
|
465
|
+
#
|
466
|
+
# Parameters (see Launchpad for values):
|
467
|
+
#
|
468
|
+
# [+type+] type of the button
|
469
|
+
#
|
470
|
+
# Options hash:
|
471
|
+
#
|
472
|
+
# [<tt>:x</tt>] x coordinate
|
473
|
+
# [<tt>:y</tt>] y coordinate
|
474
|
+
#
|
475
|
+
# Returns:
|
476
|
+
#
|
477
|
+
# integer to be used for MIDI data 1
|
478
|
+
#
|
479
|
+
# Errors raised:
|
480
|
+
#
|
481
|
+
# [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range
|
482
|
+
def note(type, opts)
|
483
|
+
note = TYPE_TO_NOTE[type]
|
484
|
+
if note.nil?
|
485
|
+
x = (opts[:x] || -1).to_i
|
486
|
+
y = (opts[:y] || -1).to_i
|
487
|
+
if x < 0 || x > 7 || y < 0 || y > 7
|
488
|
+
logger.error "wrong coordinates specified: x=#{x}, y=#{y}"
|
489
|
+
raise NoValidGridCoordinatesError.new("you need to specify valid coordinates (x/y, 0-7, from top left), you specified: x=#{x}, y=#{y}")
|
490
|
+
end
|
491
|
+
note = y * 10 + x
|
492
|
+
end
|
493
|
+
note
|
494
|
+
end
|
495
|
+
|
496
|
+
# Calculates the MIDI data 2 value (velocity) for given brightness and mode values.
|
497
|
+
#
|
498
|
+
# Options hash:
|
499
|
+
#
|
500
|
+
# [<tt>:red</tt>] brightness of red LED
|
501
|
+
# [<tt>:green</tt>] brightness of green LED
|
502
|
+
# [<tt>:mode</tt>] button mode, defaults to <tt>:normal</tt>, one of:
|
503
|
+
# [<tt>:normal/tt>] updates the LED for all circumstances (the new value will be written to both buffers)
|
504
|
+
# [<tt>:flashing/tt>] updates the LED for flashing (the new value will be written to buffer 0 while in buffer 1, the value will be :off, see )
|
505
|
+
# [<tt>:buffering/tt>] updates the LED for the current update_buffer only
|
506
|
+
#
|
507
|
+
# Returns:
|
508
|
+
#
|
509
|
+
# integer to be used for MIDI data 2
|
510
|
+
#
|
511
|
+
# Errors raised:
|
512
|
+
#
|
513
|
+
# [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
|
514
|
+
# def velocity(opts)
|
515
|
+
# if opts.is_a?(Hash)
|
516
|
+
# red = brightness(opts[:red] || 0)
|
517
|
+
# green = brightness(opts[:green] || 0)
|
518
|
+
# color = 16 * green + red
|
519
|
+
# flags = case opts[:mode]
|
520
|
+
# when :flashing then 8
|
521
|
+
# when :buffering then 0
|
522
|
+
# else 12
|
523
|
+
# end
|
524
|
+
# color + flags
|
525
|
+
# else
|
526
|
+
# opts.to_i + 12
|
527
|
+
# end
|
528
|
+
# end
|
529
|
+
|
530
|
+
# Calculates the integer brightness for given brightness values.
|
531
|
+
#
|
532
|
+
# Parameters (see Launchpad for values):
|
533
|
+
#
|
534
|
+
# [+brightness+] brightness
|
535
|
+
#
|
536
|
+
# Errors raised:
|
537
|
+
#
|
538
|
+
# [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
|
539
|
+
# def brightness(brightness)
|
540
|
+
# case brightness
|
541
|
+
# when 0, :off then 0
|
542
|
+
# when 1, :low, :lo then 1
|
543
|
+
# when 2, :medium, :med then 2
|
544
|
+
# when 3, :high, :hi then 3
|
545
|
+
# else
|
546
|
+
# logger.error "wrong brightness specified: #{brightness}"
|
547
|
+
# raise NoValidBrightnessError.new("you need to specify the brightness as 0/1/2/3, :off/:low/:medium/:high or :off/:lo/:hi, you specified: #{brightness}")
|
548
|
+
# end
|
549
|
+
# end
|
550
|
+
|
551
|
+
# Creates a MIDI message.
|
552
|
+
#
|
553
|
+
# Parameters:
|
554
|
+
#
|
555
|
+
# [+status+] MIDI status code
|
556
|
+
# [+data1+] MIDI data 1 (note)
|
557
|
+
# [+data2+] MIDI data 2 (velocity)
|
558
|
+
#
|
559
|
+
# Returns:
|
560
|
+
#
|
561
|
+
# an array with:
|
562
|
+
#
|
563
|
+
# [<tt>:message</tt>] an array of
|
564
|
+
# MIDI status code,
|
565
|
+
# MIDI data 1 (note),
|
566
|
+
# MIDI data 2 (velocity)
|
567
|
+
# [<tt>:timestamp</tt>] integer indicating the time when the MIDI message was created, in this case 0
|
568
|
+
def message(status, data1, data2)
|
569
|
+
{:message => [status, data1, data2], :timestamp => 0}
|
570
|
+
end
|
571
|
+
|
572
|
+
end
|
573
|
+
|
574
|
+
end
|