lib/surface_master/launchpad/device.rb in surface_master-0.2.0 vs lib/surface_master/launchpad/device.rb in surface_master-0.2.1
- old
+ new
@@ -1,7 +1,8 @@
module SurfaceMaster
module Launchpad
+ # Low-level interface to Novation Launchpad Mark 2 control surface.
class Device < SurfaceMaster::Device
include MIDICodes
# TODO: Rename scenes to match Mk2
CODE_NOTE_TO_TYPE = Hash.new { |*_| :grid }
@@ -52,108 +53,119 @@
end
# TODO: Support more of the LaunchPad Mark 2's functionality.
def change(opts = nil)
+ fail NoOutputAllowedError unless output_enabled?
+
opts ||= {}
command, payload = color_payload(opts)
sysex!(command, payload[:led], payload[:color])
+ nil
end
def changes(values)
- msg_by_command = {}
- values.each do |value|
- command, payload = color_payload(value)
- (msg_by_command[command] ||= []) << payload
- end
- msg_by_command.each do |command, payloads|
- # The documented batch size for RGB LED updates is 80. The docs lie, at least on my current
- # firmware version -- anything above 62 crashes the device hard.
+ fail NoOutputAllowedError unless output_enabled?
+
+ organize_commands(values).each do |command, payloads|
+ # The documented batch size for RGB LED updates is 80. The docs lie, at least on my
+ # current firmware version -- anything above 62 crashes the device hard.
while (slice = payloads.shift(62)).length > 0
- sysex!(command, *slice.map { |payload| [payload[:led], payload[:color]] })
+ messages = slice.map { |payload| [payload[:led], payload[:color]] }
+ sysex!(command, *messages)
end
end
+ nil
end
def read
+ fail NoInputAllowedError unless input_enabled?
super.collect do |input|
- note = input[:note]
- input[:type] = CODE_NOTE_TO_TYPE[[input[:code], note]] || :grid
- if input[:type] == :grid
- note = note - 11
- input[:x] = note % 10
- input[:y] = note / 10
- end
+ note = input.delete(:note)
+ input[:type] = CODE_NOTE_TO_TYPE[[input.delete(:code), note]] || :grid
+ input[:x], input[:y] = decode_grid_coord(note) if input[:type] == :grid
+ input.delete(:velocity)
input
end
end
protected
+ def organize_commands(values)
+ msg_by_command = {}
+ values.each do |value|
+ command, payload = color_payload(value)
+ (msg_by_command[command] ||= []) << payload
+ end
+ msg_by_command
+ end
+
+ def decode_grid_coord(note)
+ note -= 11
+ x = note % 10
+ y = note / 10
+ [x, y]
+ end
+
def layout!(mode); sysex!(0x22, mode); end
def sysex_prefix; @sysex_prefix ||= super + [0x00, 0x20, 0x29, 0x02, 0x18]; end
def decode_led(opts)
case
- when opts[:cc]
- [:cc, TYPE_TO_NOTE[opts[:cc]]]
- when opts[:grid]
- if opts[:grid] == :all
- [:all, nil]
- else
- [:grid, (opts[:grid][1] * 10) + opts[:grid][0] + 11]
- end
- when opts[:column]
- [:column, opts[:column]]
- when opts[:row]
- [:row, opts[:row]]
+ when opts[:cc] then [:cc, TYPE_TO_NOTE[opts[:cc]]]
+ when opts[:grid] then decode_grid_led(opts)
+ when opts[:column] then [:column, opts[:column]]
+ when opts[:row] then [:row, opts[:row]]
end
+ rescue
+ raise SurfaceMaster::Launchpad::NoValidGridCoordinatesError
end
- TYPE_TO_COMMAND = { cc: 0x0B,
- grid: 0x0B,
- column: 0x0C,
- row: 0x0D,
- all: 0x0E }.freeze
- def color_payload(opts)
- type, led = decode_led(opts)
- command = TYPE_TO_COMMAND[type]
- case type
- when :cc, :grid
- color = [opts[:red] || 0x00, opts[:green] || 0x00, opts[:blue] || 0x00]
- when :column, :row, :all
- color = opts[:color] || 0x00
+ def decode_grid_led(opts)
+ if opts[:grid] == :all
+ [:all, nil]
+ else
+ check_xy_values!(opts[:grid])
+ [:grid, (opts[:grid][1] * 10) + opts[:grid][0] + 11]
end
- [command, { led: led, color: color }]
end
+ def check_xy_values!(xy_pair)
+ x = xy_pair[0]
+ y = xy_pair[1]
+ return unless xy_pair.length != 2 ||
+ !coord_in_range?(x) ||
+ !coord_in_range?(y)
+
+ fail SurfaceMaster::Launchpad::NoValidGridCoordinatesError
+ end
+
+ def coord_in_range?(val); val && val >= 0 && val <= 7; end
+
+ def color_payload(opts)
+ # Hard-coded to single-LED RGB update right now.
+ # For paletted changes, commands available include:
+ # 0x0C -> Column
+ # 0x0D -> Row
+ # 0x0E -> All LEDs
+ [0x0B,
+ { led: decode_led(opts),
+ color: [opts[:red] || 0x00, opts[:green] || 0x00, opts[:blue] || 0x00] }]
+ end
+
def output!(status, data1, data2)
outputs!(message(status, data1, data2))
end
def outputs!(*messages)
messages = Array(messages)
if @output.nil?
- logger.error "trying to write to device that's not been initialized for output"
- raise SurfaceMaster::NoOutputAllowedError
+ logger.error "trying to write to device not open for output"
+ fail SurfaceMaster::NoOutputAllowedError
end
logger.debug "writing messages to launchpad:\n #{messages.join("\n ")}" if logger.debug?
@output.write(messages)
nil
- end
-
- def note(type, opts)
- note = TYPE_TO_NOTE[type]
- if note.nil?
- x = (opts[:x] || -1).to_i
- y = (opts[:y] || -1).to_i
- if x < 0 || x > 7 || y < 0 || y > 7
- logger.error "wrong coordinates specified: x=#{x}, y=#{y}"
- raise NoValidGridCoordinatesError.new("you need to specify valid coordinates (x/y, 0-7, from top left), you specified: x=#{x}, y=#{y}")
- end
- note = y * 10 + x
- end
- note
end
end
end
end