require 'portmidi' require 'launchpad/errors' require 'launchpad/midi_codes' require 'launchpad/version' module Launchpad # This class is used to exchange data with the launchpad. # It provides methods to light LEDs and to get information about button presses/releases. # # Example: # # require 'rubygems' # require 'launchpad/device' # # device = Launchpad::Device.new # device.test_leds # sleep 1 # device.reset # sleep 1 # device.change :grid, :x => 4, :y => 4, :red => :high, :green => :low class Device include MidiCodes # Initializes the launchpad device. When output capabilities are requested, # the launchpad will be reset. # # Optional options hash: # # [:input] whether to use MIDI input for user interaction, # true/false, optional, defaults to +true+ # [:output] whether to use MIDI output for data display, # true/false, optional, defaults to +true+ # [: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" # # Errors raised: # # [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist # [Launchpad::DeviceBusyError] when device with ID or name specified is busy def initialize(opts = nil) opts = { :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. # # Errors raised: # # [Launchpad::NoOutputAllowedError] when output is not enabled def reset output(Status::CC, Status::NIL, Status::NIL) end # Lights all LEDs (for testing purposes). # # Parameters (see Launchpad for values): # # [+brightness+] brightness of both LEDs for all buttons # # Errors raised: # # [Launchpad::NoOutputAllowedError] when output is not enabled 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. # # Parameters (see Launchpad for values): # # [+type+] type of the button to change # # Optional options hash (see Launchpad for values): # # [:x] x coordinate # [:y] y coordinate # [:red] brightness of red LED # [:green] brightness of green LED # [:mode] button mode # # Errors raised: # # [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range # [Launchpad::NoOutputAllowedError] when output is not enabled 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 in batch mode. # # Parameters (see Launchpad for values): # # [+colors] an array of colors, each either being an integer or a Hash # * integer: calculated using the formula # color = 16 * green + red # * Hash: # [:red] brightness of red LED # [:green] brightness of green LED # [:mode] button mode # the array consists of 64 colors for the grid buttons, # 8 colors for the scene buttons (top to bottom) # and 8 colors for the top control buttons (left to right), # maximum 80 values - excessive values will be ignored, # missing values will be filled with 0 # # Errors raised: # # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range # [Launchpad::NoOutputAllowedError] when output is not enabled 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. # # Errors raised: # # [Launchpad::NoOutputAllowedError] when output is not enabled def flashing_on output(Status::CC, Status::NIL, Velocity::FLASHING_ON) end # Switches LEDs marked as flashing off when using custom timer for flashing. # # Errors raised: # # [Launchpad::NoOutputAllowedError] when output is not enabled def flashing_off output(Status::CC, Status::NIL, Velocity::FLASHING_OFF) end # Starts flashing LEDs marked as flashing automatically. # Stop flashing by calling flashing_on or flashing_off. # # Errors raised: # # [Launchpad::NoOutputAllowedError] when output is not enabled 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 haven't been handled yet. # # Returns: # # an array of hashes with (see Launchpad for values): # # [:timestamp] integer indicating the time when the action occured # [:state] state of the button after action # [:type] type of the button # [:x] x coordinate # [:y] y coordinate # # Errors raised: # # [Launchpad::NoInputAllowedError] when input is not enabled 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 # Creates input/output devices. # # Parameters: # # [+devices+] array of portmidi devices # [+device_type] class to instantiate (Portmidi::Input/Portmidi::Output) # # Options hash: # # [:id] id of the MIDI device to use # [:name] name of the MIDI device to use, # only used when :id is not specified, # defaults to "Launchpad" # # Returns: # # newly created device # # Errors raised: # # [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist # [Launchpad::DeviceBusyError] when device with ID or name specified is busy def device(devices, device_type, opts) id = opts[:id] if id.nil? name = opts[:name] || 'Launchpad' device = devices.select {|device| device.name == 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 # Reads input from the MIDI device. # # Returns: # # an array of hashes with: # # [:message] an array of # MIDI status code, # MIDI data 1 (note), # MIDI data 2 (velocity) # and a fourth value # [:timestamp] integer indicating the time when the MIDI message was created # # Errors raised: # # [Launchpad::NoInputAllowedError] when output is not enabled def input raise NoInputAllowedError if @input.nil? @input.read(16) end # Writes data to the MIDI device. # # Parameters: # # [+status+] MIDI status code # [+data1+] MIDI data 1 (note) # [+data2+] MIDI data 2 (velocity) # # Errors raised: # # [Launchpad::NoOutputAllowedError] when output is not enabled def output(status, data1, data2) raise NoOutputAllowedError if @output.nil? @output.write([{:message => [status, data1, data2], :timestamp => 0}]) nil end # Calculates the MIDI data 1 value (note) for a button. # # Parameters (see Launchpad for values): # # [+type+] type of the button # # Options hash: # # [:x] x coordinate # [:y] y coordinate # # Returns: # # integer to be used for MIDI data 1 # # Errors raised: # # [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range 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 # Calculates the MIDI data 2 value (velocity) for given brightness and mode values. # # Options hash: # # [:red] brightness of red LED # [:green] brightness of green LED # [:mode] button mode # # Returns: # # integer to be used for MIDI data 2 # # Errors raised: # # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range 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 # Calculates the integer brightness for given brightness values. # # Parameters (see Launchpad for values): # # [+brightness+] brightness # # Errors raised: # # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range 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