lib/launchpad/device.rb in launchpad-0.2.2 vs lib/launchpad/device.rb in launchpad-0.3.0

- old
+ new

@@ -1,31 +1,70 @@ require 'portmidi' require 'launchpad/errors' +require 'launchpad/logging' 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 Logging include MidiCodes + CODE_NOTE_TO_DATA_TYPE = { + [Status::ON, SceneButton::SCENE1] => :scene1, + [Status::ON, SceneButton::SCENE2] => :scene2, + [Status::ON, SceneButton::SCENE3] => :scene3, + [Status::ON, SceneButton::SCENE4] => :scene4, + [Status::ON, SceneButton::SCENE5] => :scene5, + [Status::ON, SceneButton::SCENE6] => :scene6, + [Status::ON, SceneButton::SCENE7] => :scene7, + [Status::ON, SceneButton::SCENE8] => :scene8, + [Status::CC, ControlButton::UP] => :up, + [Status::CC, ControlButton::DOWN] => :down, + [Status::CC, ControlButton::LEFT] => :left, + [Status::CC, ControlButton::RIGHT] => :right, + [Status::CC, ControlButton::SESSION] => :session, + [Status::CC, ControlButton::USER1] => :user1, + [Status::CC, ControlButton::USER2] => :user2, + [Status::CC, ControlButton::MIXER] => :mixer + }.freeze + + TYPE_TO_NOTE = { + :up => ControlButton::UP, + :down => ControlButton::DOWN, + :left => ControlButton::LEFT, + :right => ControlButton::RIGHT, + :session => ControlButton::SESSION, + :user1 => ControlButton::USER1, + :user2 => ControlButton::USER2, + :mixer => ControlButton::MIXER, + :scene1 => SceneButton::SCENE1, + :scene2 => SceneButton::SCENE2, + :scene3 => SceneButton::SCENE3, + :scene4 => SceneButton::SCENE4, + :scene5 => SceneButton::SCENE5, + :scene6 => SceneButton::SCENE6, + :scene7 => SceneButton::SCENE7, + :scene8 => SceneButton::SCENE8 + }.freeze + # Initializes the launchpad device. When output capabilities are requested, # the launchpad will be reset. # # Optional options hash: # @@ -37,10 +76,11 @@ # optional, <tt>:device_name</tt> will be used if omitted # [<tt>:output_device_id</tt>] ID of the MIDI output device to use, # optional, <tt>:device_name</tt> will be used if omitted # [<tt>:device_name</tt>] Name of the MIDI device to use, # optional, defaults to "Launchpad" + # [<tt>:logger</tt>] [Logger] to be used by this device instance, can be changed afterwards # # 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 @@ -48,19 +88,30 @@ opts = { :input => true, :output => true }.merge(opts || {}) + self.logger = opts[:logger] + logger.debug "initializing Launchpad::Device##{object_id} with #{opts.inspect}" + 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] + @input = create_device!(Portmidi.input_devices, Portmidi::Input, + :id => opts[:input_device_id], + :name => opts[:device_name] + ) if opts[:input] + @output = create_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 + logger.debug "closing Launchpad::Device##{object_id}" @input.close unless @input.nil? @input = nil @output.close unless @output.nil? @output = nil end @@ -226,10 +277,11 @@ data += 8 if opts[:flashing] output(Status::CC, Status::NIL, data) end # Reads user actions (button presses/releases) that haven't been handled yet. + # This is non-blocking, so when nothing happend yet you'll get an empty array. # # Returns: # # an array of hashes with (see Launchpad for values): # @@ -247,37 +299,14 @@ (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 + data[:type] = CODE_NOTE_TO_DATA_TYPE[[code, note]] || :grid + if data[:type] == :grid + data[:x] = note % 16 + data[:y] = note / 16 end data end end @@ -303,20 +332,26 @@ # # 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) + def create_device!(devices, device_type, opts) + logger.debug "creating #{device_type} with #{opts.inspect}, choosing from portmidi devices #{devices.inspect}" 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? + if id.nil? + message = "MIDI device #{opts[:id] || opts[:name]} doesn't exist" + logger.fatal message + raise NoSuchDeviceError.new(message) + end device_type.new(id) rescue RuntimeError => e + logger.fatal "error creating #{device_type}: #{e.inspect}" raise DeviceBusyError.new(e) end # Reads input from the MIDI device. # @@ -333,11 +368,14 @@ # # Errors raised: # # [Launchpad::NoInputAllowedError] when output is not enabled def input - raise NoInputAllowedError if @input.nil? + if @input.nil? + logger.error "trying to read from device that's not been initialized for input" + raise NoInputAllowedError + end @input.read(16) end # Writes data to the MIDI device. # @@ -363,11 +401,15 @@ # MIDI status code, # MIDI data 1 (note), # MIDI data 2 (velocity) # [<tt>:timestamp</tt>] integer indicating the time when the MIDI message was created def output_messages(messages) - raise NoOutputAllowedError if @output.nil? + if @output.nil? + logger.error "trying to write to device that's not been initialized for output" + raise NoOutputAllowedError + end + logger.debug "writing messages to launchpad:\n #{messages.join("\n ")}" if logger.debug? @output.write(messages) nil end # Calculates the MIDI data 1 value (note) for a button. @@ -387,33 +429,21 @@ # # 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 + note = TYPE_TO_NOTE[type] + if note.nil? 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 + 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 * 16 + x end + note end # Calculates the MIDI data 2 value (velocity) for given brightness and mode values. # # Options hash: @@ -431,23 +461,23 @@ # # Errors raised: # # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range def velocity(opts) - color = if opts.is_a?(Hash) + if opts.is_a?(Hash) red = brightness(opts[:red] || 0) green = brightness(opts[:green] || 0) - 16 * green + red + color = 16 * green + red + flags = case opts[:mode] + when :flashing then 8 + when :buffering then 0 + else 12 + end + color + flags else - opts.to_i + opts.to_i + 12 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): @@ -462,9 +492,10 @@ when 0, :off then 0 when 1, :low, :lo then 1 when 2, :medium, :med then 2 when 3, :high, :hi then 3 else + logger.error "wrong brightness specified: #{brightness}" 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 # Creates a MIDI message.