lib/launchpad/device.rb in launchpad-0.1.0 vs lib/launchpad/device.rb in launchpad-0.1.1
- old
+ new
@@ -4,88 +4,157 @@
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_name => Name of the MIDI device to use, optional, defaults to Launchpad
- # :input => true/false, whether to use MIDI input for user interaction, optional, defaults to true
- # :output => true/false, whether to use MIDI output for data display, optional, defaults to true
- # }
+ # Initializes the launchpad device. When output capabilities are requested,
+ # the launchpad will be reset.
+ #
+ # Optional options hash:
+ #
+ # [<tt>:input</tt>] whether to use MIDI input for user interaction,
+ # <tt>true/false</tt>, optional, defaults to +true+
+ # [<tt>:output</tt>] whether to use MIDI output for data display,
+ # <tt>true/false</tt>, optional, defaults to +true+
+ # [<tt>:input_device_id</tt>] ID of the MIDI input device to use,
+ # 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"
+ #
+ # 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 = {
- :device_name => 'Launchpad',
:input => true,
:output => true
}.merge(opts || {})
Portmidi.start
- if opts[:input]
- input_device = Portmidi.input_devices.select {|device| device.name == opts[:device_name]}.first
- raise NoSuchDeviceError.new("MIDI input device #{opts[:device_name]} doesn't exist") if input_device.nil?
- begin
- @input = Portmidi::Input.new(input_device.device_id)
- rescue RuntimeError => e
- raise DeviceBusyError.new(e)
- end
- end
-
- if opts[:output]
- output_device = Portmidi.output_devices.select {|device| device.name == opts[:device_name]}.first
- raise NoSuchDeviceError.new("MIDI output device #{opts[:device_name]} doesn't exist") if output_device.nil?
- begin
- @output = Portmidi::Output.new(output_device.device_id)
- rescue RuntimeError => e
- raise DeviceBusyError.new(e)
- end
- reset
- end
+ @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
- # Resets the launchpad - all settings are reset and all LEDs are switched off
+ # 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)
- # takes an optional parameter brightness (:off/:low/:medium/:high, defaults to :high)
+ # 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
- # type => one of :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8
- # opts => {
- # :x => x coordinate (0 based from top left, mandatory if type is :grid)
- # :y => y coordinate (0 based from top left, mandatory if type is :grid)
- # :red => brightness of red LED (0-3, optional, defaults to 0)
- # :green => brightness of red LED (0-3, optional, defaults to 0)
- # :mode => button behaviour (:normal, :flashing, :buffering, optional, defaults to :normal)
- # }
+ # Changes a single LED.
+ #
+ # Parameters (see Launchpad for values):
+ #
+ # [+type+] type of the button to change
+ #
+ # Optional options hash (see Launchpad for values):
+ #
+ # [<tt>:x</tt>] x coordinate
+ # [<tt>:y</tt>] y coordinate
+ # [<tt>:red</tt>] brightness of red LED
+ # [<tt>:green</tt>] brightness of green LED
+ # [<tt>:mode</tt>] 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 at once
- # velocities is an array of arrays, each containing a
- # color value calculated using the formula
- # color = 16 * green + red
- # with green and red each ranging from 0-3
- # first the grid, then the scene buttons (top to bottom), then the top control buttons (left to right), maximum 80 values
+ # 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
+ # <tt>color = 16 * green + red</tt>
+ # * Hash:
+ # [<tt>:red</tt>] brightness of red LED
+ # [<tt>:green</tt>] brightness of green LED
+ # [<tt>:mode</tt>] 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
@@ -94,21 +163,34 @@
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)
+ # 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)
+ # 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 by calling #flashing_on or #flashing_off)
+ # 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
@@ -122,20 +204,25 @@
# output(CC, 0x00, 0x30)
# @buffering = false
# end
# end
- # Reads user actions (button presses/releases) that aren't handled yet
- # [
- # {
- # :timestamp => integer indicating the time when the action occured
- # :state => :down/:up, whether the button has been pressed or released
- # :type => which button has been pressed, one of :grid, :up, :down, :left, :right, :session, :user1, :user2, :mixer, :scene1 - :scene8
- # :x => x coordinate (0-7), only set when :type is :grid
- # :y => y coordinate (0-7), only set when :type is :grid
- # }, ...
- # ]
+ # Reads user actions (button presses/releases) that haven't been handled yet.
+ #
+ # Returns:
+ #
+ # an array of hashes with (see Launchpad for values):
+ #
+ # [<tt>:timestamp</tt>] integer indicating the time when the action occured
+ # [<tt>:state</tt>] state of the button after action
+ # [<tt>:type</tt>] type of the button
+ # [<tt>:x</tt>] x coordinate
+ # [<tt>:y</tt>] 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],
@@ -173,21 +260,101 @@
end
end
private
+ # Creates input/output devices.
+ #
+ # Parameters:
+ #
+ # [+devices+] array of portmidi devices
+ # [+device_type] class to instantiate (<tt>Portmidi::Input/Portmidi::Output</tt>)
+ #
+ # Options hash:
+ #
+ # [<tt>:id</tt>] id of the MIDI device to use
+ # [<tt>:name</tt>] name of the MIDI device to use,
+ # only used when <tt>:id</tt> 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:
+ #
+ # [<tt>:message</tt>] an array of
+ # MIDI status code,
+ # MIDI data 1 (note),
+ # MIDI data 2 (velocity)
+ # and a fourth value
+ # [<tt>:timestamp</tt>] 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
- def output(*args)
+ # 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 => args, :timestamp => 0}])
+ @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:
+ #
+ # [<tt>:x</tt>] x coordinate
+ # [<tt>:y</tt>] 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
@@ -210,10 +377,25 @@
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:
+ #
+ # [<tt>:red</tt>] brightness of red LED
+ # [<tt>:green</tt>] brightness of green LED
+ # [<tt>:mode</tt>] 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
@@ -226,9 +408,18 @@
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