require 'launchpad/device' module Launchpad # This class provides advanced interaction features. # # Example: # # require 'rubygems' # require 'launchpad' # # interaction = Launchpad::Interaction.new # interaction.response_to(:grid, :down) do |interaction, action| # interaction.device.change(:grid, action.merge(:red => :high)) # end # interaction.response_to(:mixer, :down) do |interaction, action| # interaction.stop # end # # interaction.start class Interaction # Returns the Launchpad::Device the Launchpad::Interaction acts on. attr_reader :device # Returns whether the Launchpad::Interaction is active or not. attr_reader :active # Initializes the interaction. # # Optional options hash: # # [:device] Launchpad::Device to act on, # optional, :input_device_id/:output_device_id will be used if omitted # [: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" # [:latency] delay (in s, fractions allowed) between MIDI pulls, # optional, defaults to 0.001 (1ms) # # 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 = opts[:device] || Device.new(opts.merge(:input => true, :output => true)) @latency = (opts[:latency] || 0.001).to_f.abs @active = false end # Closes the interaction's device - nothing can be done with the interaction/device afterwards. # # Errors raised: # # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the def close stop @device.close end # Determines whether this interaction's device has been closed. def closed? @device.closed? end # Starts interacting with the launchpad. Resets the device when # the interaction was properly stopped via stop or close. # # Optional options hash: # # [:detached] true/false, # whether to detach the interaction, method is blocking when +false+, # optional, defaults to +false+ # # Errors raised: # # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device # [Launchpad::NoOutputAllowedError] when output is not enabled on the interaction's device # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the launchpad def start(opts = nil) opts = { :detached => false }.merge(opts || {}) @active = true @reader_thread ||= Thread.new do begin while @active do @device.read_pending_actions.each {|action| Thread.new {respond_to_action(action)}} sleep @latency unless @latency <= 0 end rescue Portmidi::DeviceError => e raise CommunicationError.new(e) ensure @device.reset end end @reader_thread.join unless opts[:detached] end # Stops interacting with the launchpad. # # Errors raised: # # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the def stop @active = false if @reader_thread # run (resume from sleep) and wait for @reader_thread to end @reader_thread.run if @reader_thread.alive? @reader_thread.join @reader_thread = nil end nil end # Registers a response to one or more actions. # # Parameters (see Launchpad for values): # # [+types+] one or an array of button types to respond to, # additional value :all for all buttons # [+state+] button state to respond to, # additional value :both # # Optional options hash: # # [:exclusive] true/false, # whether to deregister all other responses to the specified actions, # optional, defaults to +false+ # # Takes a block which will be called when an action matching the parameters occurs. # # Block parameters: # # [+interaction+] the interaction object that received the action # [+action+] the action received from Launchpad::Device.read_pending_actions def response_to(types = :all, state = :both, opts = nil, &block) types = Array(types) opts ||= {} no_response_to(types, state) if opts[:exclusive] == true Array(state == :both ? %w(down up) : state).each do |state| types.each {|type| responses[type.to_sym][state.to_sym] << block} end nil end # Deregisters all responses to one or more actions. # # Parameters (see Launchpad for values): # # [+types+] one or an array of button types to respond to, # additional value :all for actions on all buttons # (but not meaning "all responses"), # optional, defaults to +nil+, meaning "all responses" # [+state+] button state to respond to, # additional value :both def no_response_to(types = nil, state = :both) types = Array(types) Array(state == :both ? %w(down up) : state).each do |state| types.each {|type| responses[type.to_sym][state.to_sym].clear} end nil end # Responds to an action by executing all matching responses, effectively simulating # a button press/release. # # Parameters (see Launchpad for values): # # [+type+] type of the button to trigger # [+state+] state of the button # # Optional options hash (see Launchpad for values): # # [:x] x coordinate # [:y] y coordinate def respond_to(type, state, opts = nil) respond_to_action((opts || {}).merge(:type => type, :state => state)) end private # Returns the hash storing all responses. Keys are button types, values are # hashes themselves, keys are :down/:up, values are arrays of responses. def responses @responses ||= Hash.new {|hash, key| hash[key] = {:down => [], :up => []}} end # Reponds to an action by executing all matching responses. # # Parameters: # # [+action+] hash containing an action from Launchpad::Device.read_pending_actions def respond_to_action(action) type = action[:type].to_sym state = action[:state].to_sym (responses[type][state] + responses[:all][state]).each {|block| block.call(self, action)} nil end end end