-
Notifications
You must be signed in to change notification settings - Fork 12
/
device.rb
264 lines (235 loc) · 9.11 KB
/
device.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
require 'portmidi'
require 'launchpad/errors'
require 'launchpad/midi_codes'
require 'launchpad/version'
module Launchpad
class Device
include MidiCodes
# Initializes the launchpad
# {
# :input_device_id => ID of the MIDI input device to use, optional, :device_name will be used if omitted
# :output_device_id => ID of the MIDI output device to use, optional, :device_name will be used if omitted
# :device_name => Name of the MIDI device to use, optional, defaults to Launchpad
# :input => true/false, whether to use MIDI input for user interaction, optional, defaults to true
# :output => true/false, whether to use MIDI output for data display, optional, defaults to true
# }
def initialize(opts = nil)
opts = {
:device_name => 'Launchpad',
:input => true,
:output => true
}.merge(opts || {})
Portmidi.start
@input = device(Portmidi.input_devices, Portmidi::Input, :id => opts[:input_device_id], :name => opts[:device_name]) if opts[:input]
@output = device(Portmidi.output_devices, Portmidi::Output, :id => opts[:output_device_id], :name => opts[:device_name]) if opts[:output]
reset if output_enabled?
end
# Closes the device - nothing can be done with the device afterwards
def close
@input.close unless @input.nil?
@input = nil
@output.close unless @output.nil?
@output = nil
end
# Determines whether this device has been closed
def closed?
!(input_enabled? || output_enabled?)
end
# Determines whether this device can be used to read input
def input_enabled?
!@input.nil?
end
# Determines whether this device can be used to output data
def output_enabled?
!@output.nil?
end
# Resets the launchpad - all settings are reset and all LEDs are switched off
def reset
output(Status::CC, Status::NIL, Status::NIL)
end
# Lights all LEDs (for testing purposes)
# takes an optional parameter brightness (:off/:low/:medium/:high, defaults to :high)
def test_leds(brightness = :high)
brightness = brightness(brightness)
if brightness == 0
reset
else
output(Status::CC, Status::NIL, Velocity::TEST_LEDS + brightness)
end
end
# Changes a single LED
# type => one of :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8
# opts => {
# :x => x coordinate (0 based from top left, mandatory if type is :grid)
# :y => y coordinate (0 based from top left, mandatory if type is :grid)
# :red => brightness of red LED (0-3, optional, defaults to 0)
# :green => brightness of red LED (0-3, optional, defaults to 0)
# :mode => button behaviour (:normal, :flashing, :buffering, optional, defaults to :normal)
# }
def change(type, opts = nil)
opts ||= {}
status = %w(up down left right session user1 user2 mixer).include?(type.to_s) ? Status::CC : Status::ON
output(status, note(type, opts), velocity(opts))
end
# Changes all LEDs at once
# velocities is an array of arrays, each containing a
# color value calculated using the formula
# color = 16 * green + red
# with green and red each ranging from 0-3
# first the grid, then the scene buttons (top to bottom), then the top control buttons (left to right), maximum 80 values
def change_all(*colors)
# ensure that colors is at least and most 80 elements long
colors = colors.flatten[0..79]
colors += [0] * (80 - colors.size) if colors.size < 80
# HACK switch off first grid LED to reset rapid LED change pointer
output(Status::ON, 0, 0)
# send colors in slices of 2
colors.each_slice(2) do |c1, c2|
output(Status::MULTI, velocity(c1), velocity(c2))
end
end
# Switches LEDs marked as flashing on (when using custom timer for flashing)
def flashing_on
output(Status::CC, Status::NIL, Velocity::FLASHING_ON)
end
# Switches LEDs marked as flashing off (when using custom timer for flashing)
def flashing_off
output(Status::CC, Status::NIL, Velocity::FLASHING_OFF)
end
# Starts flashing LEDs marked as flashing automatically (stop by calling #flashing_on or #flashing_off)
def flashing_auto
output(Status::CC, Status::NIL, Velocity::FLASHING_AUTO)
end
# def start_buffering
# output(CC, 0x00, 0x31)
# @buffering = true
# end
#
# def flush_buffer(end_buffering = true)
# output(CC, 0x00, 0x34)
# if end_buffering
# output(CC, 0x00, 0x30)
# @buffering = false
# end
# end
# Reads user actions (button presses/releases) that aren't handled yet
# [
# {
# :timestamp => integer indicating the time when the action occured
# :state => :down/:up, whether the button has been pressed or released
# :type => which button has been pressed, one of :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8
# :x => x coordinate (0-7), only set when :type is :grid
# :y => y coordinate (0-7), only set when :type is :grid
# }, ...
# ]
def read_pending_actions
Array(input).collect do |midi_message|
(code, note, velocity) = midi_message[:message]
data = {
:timestamp => midi_message[:timestamp],
:state => (velocity == 127 ? :down : :up)
}
data[:type] = case code
when Status::ON
case note
when SceneButton::SCENE1 then :scene1
when SceneButton::SCENE2 then :scene2
when SceneButton::SCENE3 then :scene3
when SceneButton::SCENE4 then :scene4
when SceneButton::SCENE5 then :scene5
when SceneButton::SCENE6 then :scene6
when SceneButton::SCENE7 then :scene7
when SceneButton::SCENE8 then :scene8
else
data[:x] = note % 16
data[:y] = note / 16
:grid
end
when Status::CC
case note
when ControlButton::UP then :up
when ControlButton::DOWN then :down
when ControlButton::LEFT then :left
when ControlButton::RIGHT then :right
when ControlButton::SESSION then :session
when ControlButton::USER1 then :user1
when ControlButton::USER2 then :user2
when ControlButton::MIXER then :mixer
end
end
data
end
end
private
def device(devices, device_type, opts)
id = opts[:id]
if id.nil?
device = devices.select {|device| device.name == opts[:name]}.first
id = device.device_id unless device.nil?
end
raise NoSuchDeviceError.new("MIDI device #{opts[:id] || opts[:name]} doesn't exist") if id.nil?
device_type.new(id)
rescue RuntimeError => e
raise DeviceBusyError.new(e)
end
def input
raise NoInputAllowedError if @input.nil?
@input.read(16)
end
def output(*args)
raise NoOutputAllowedError if @output.nil?
@output.write([{:message => args, :timestamp => 0}])
nil
end
def note(type, opts)
case type
when :up then ControlButton::UP
when :down then ControlButton::DOWN
when :left then ControlButton::LEFT
when :right then ControlButton::RIGHT
when :session then ControlButton::SESSION
when :user1 then ControlButton::USER1
when :user2 then ControlButton::USER2
when :mixer then ControlButton::MIXER
when :scene1 then SceneButton::SCENE1
when :scene2 then SceneButton::SCENE2
when :scene3 then SceneButton::SCENE3
when :scene4 then SceneButton::SCENE4
when :scene5 then SceneButton::SCENE5
when :scene6 then SceneButton::SCENE6
when :scene7 then SceneButton::SCENE7
when :scene8 then SceneButton::SCENE8
else
x = (opts[:x] || -1).to_i
y = (opts[:y] || -1).to_i
raise NoValidGridCoordinatesError.new("you need to specify valid coordinates (x/y, 0-7, from top left), you specified: x=#{x}, y=#{y}") if x < 0 || x > 7 || y < 0 || y > 7
y * 16 + x
end
end
def velocity(opts)
color = if opts.is_a?(Hash)
red = brightness(opts[:red] || 0)
green = brightness(opts[:green] || 0)
16 * green + red
else
opts.to_i
end
flags = case opts[:mode]
when :flashing then 8
when :buffering then 0
else 12
end
color + flags
end
def brightness(brightness)
case brightness
when 0, :off then 0
when 1, :low, :lo then 1
when 2, :medium, :med then 2
when 3, :high, :hi then 3
else
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}")
end
end
end
end