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.