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