├── .gitignore
├── .travis.yml
├── Gemfile
├── LICENSE
├── README.rdoc
├── Rakefile
├── examples
├── binary_clock.rb
├── color_picker.rb
├── colors.rb
├── doodle.rb
├── double_buffering.rb
├── drawing_board.rb
├── feedback.rb
└── reset.rb
├── launchpad.gemspec
├── lib
├── launchpad.rb
└── launchpad
│ ├── device.rb
│ ├── errors.rb
│ ├── interaction.rb
│ ├── logging.rb
│ ├── midi_codes.rb
│ └── version.rb
└── test
├── helper.rb
├── test_device.rb
└── test_interaction.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 1.8.7
4 | - 1.9.3
5 | - 2.0.0
6 | before_install:
7 | - sudo apt-get update
8 | - sudo apt-get install libportmidi-dev
9 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in launchpad.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009 Thomas Jachmann
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = launchpad
2 |
3 | {
}[https://travis-ci.org/thomasjachmann/launchpad]
4 |
5 | This gem provides a ruby interface to access novation's launchpad programmatically. LEDs can be lighted and button presses can be responded to. Internally, launchpad's MIDI input/output is used to accomplish this.
6 |
7 | The interfaces should be rather stable now (sorry, I changed quite a bit since the last release), so experiment with them and comment on their usability. This still is work in progress. If you need anything or think the interfaces could be improved in any way, please contact me.
8 |
9 | Sometimes, the launchpad won't react to anything or react to/light up the wrong LEDs. Don't despair, just dis- and reconnect the thing. It seems that some (unexpected) MIDI signals make it hickup.
10 |
11 |
12 | == More Info
13 |
14 | If you don't know what launchpad is, visit:
15 |
16 | * Novation's site at http://de.novationmusic.com/products/midi_controller/launchpad
17 | * my demo videos for this library at http://www.youtube.com/thomasjachmann
18 | * other demos on youtube http://www.youtube.com/results?search_query=novation+launchpad
19 |
20 | If you're into other languages or want to know what goes on behind the scenes MIDI wise, have a look at:
21 |
22 | * Novation's MIDI programmer's reference at {www.novationmusic.com/support/launchpad}[http://www.novationmusic.com/support/launchpad/] (bottom of the page)
23 | * Tobi Tobes' port of my gem to processing at http://github.com/rngtng/launchpad
24 |
25 |
26 | == Requirements
27 |
28 | * Roger B. Dannenberg's {portmidi library}[http://sourceforge.net/projects/portmedia/]
29 | * Jan Krutisch's {portmidi gem}[http://github.com/halfbyte/portmidi]
30 |
31 |
32 | == Compatibility
33 |
34 | The gem is known to be compatible with the following ruby versions:
35 |
36 | * MRI 1.8.7
37 | * MRI 1.9.3
38 | * MRI 2.0.0
39 |
40 |
41 | == Installation
42 |
43 | The gem is hosted on RubyGems[https://rubygems.org/], so in order to use it, you're gonna gem install it:
44 |
45 | gem install launchpad
46 |
47 |
48 | == Usage
49 |
50 | There are two main entry points:
51 |
52 | * require 'launchpad/device', providing Launchpad::Device, which handles all the basic input/output stuff
53 | * require 'launchpad/interaction' or just 'launchpad', additionally providing Launchpad::Interaction, which lets you respond to actions (button presses/releases)
54 |
55 | This is a simple example (only requiring the device for output) that switches on all LEDs (for testing), resets the launchpad again and then lights the grid button at position 4/4 (from top left).
56 |
57 | require 'launchpad/device'
58 |
59 | device = Launchpad::Device.new
60 | device.test_leds
61 | sleep 1
62 | device.reset
63 | sleep 1
64 | device.change :grid, :x => 4, :y => 4, :red => :high, :green => :low
65 |
66 | This is an interaction example lighting all grid buttons in red when pressed and keeping them lit.
67 |
68 | require 'launchpad'
69 |
70 | interaction = Launchpad::Interaction.new
71 | interaction.response_to(:grid, :down) do |interaction, action|
72 | interaction.device.change(:grid, action.merge(:red => :high))
73 | end
74 | interaction.response_to(:mixer, :down) do |interaction, action|
75 | interaction.stop
76 | end
77 |
78 | interaction.start
79 |
80 |
81 | For more details, see the examples. examples/color_picker.rb is the most complex example with interaction.
82 |
83 |
84 | == Future plans
85 |
86 | * bitmap rendering
87 | * internal tracking of LED states for both buffers
88 |
89 |
90 | == Changelog
91 |
92 | === v.0.3.0
93 |
94 | * logging
95 | * reworked multi threading for action handling
96 | * compatibility with ruby 1.8.7 and 2.0.0
97 | * interaction responses for presses on single grid buttons/button areas/columns/rows
98 |
99 | === v.0.2.2
100 |
101 | * single threading fix: prevent ThreadError when Launchpad::Interaction#stop is called within an action response
102 |
103 | === v.0.2.1
104 |
105 | * Launchpad::Interaction#close now properly stops interaction first
106 | * multi threading: Launchpad::Interaction#start method can be called with :detached => true to allow calling thread to continue
107 |
108 | === v.0.2.0
109 |
110 | * double buffering (see Launchpad::Device#buffering_mode)
111 | * don't update grid button 0,0 before change_all (in order to reset rapid update pointer), use MIDI message without visual effect
112 | * (at least) doubled the speed of change_all by not sending each message individually but sending them in one go (as an array)
113 |
114 | === v0.1.1
115 |
116 | * ability to close device/interaction to free portmidi resources
117 | * ability to initialize devices using device ids as well as device names
118 | * complete documentation for http://rdoc.info/projects/thomasjachmann/launchpad
119 |
120 | === v0.1.0
121 |
122 | * first feature complete version with kinda stable API
123 |
124 |
125 | == Copyright
126 |
127 | Copyright (c) 2009 Thomas Jachmann. See LICENSE for details.
128 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 |
3 | require 'rake/testtask'
4 | Rake::TestTask.new(:test) do |test|
5 | test.libs << 'lib' << 'test'
6 | test.pattern = 'test/**/test_*.rb'
7 | test.verbose = true
8 | end
9 |
10 | task :default => :test
11 |
--------------------------------------------------------------------------------
/examples/binary_clock.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad'
2 |
3 | device = Launchpad::Device.new
4 |
5 | on = { :red => :high, :green => :off }
6 | off = { :red => :off, :green => :lo }
7 |
8 | digit_map = [
9 | [off, off, off, off],
10 | [on , off, off, off],
11 | [off, on , off, off],
12 | [on , on , off, off],
13 | [off, off, on , off],
14 | [on , off, on , off],
15 | [off, on , on , off],
16 | [on , on , on , off],
17 | [off, off, off, on ],
18 | [on , off, off, on ]
19 | ]
20 |
21 | while true do
22 | Time.now.strftime('%H%M%S').split('').each_with_index do |digit, x|
23 | digit_map[digit.to_i].each_with_index do |color, y|
24 | device.change :grid, color.merge(:x => x, :y => (7 - y))
25 | end
26 | end
27 |
28 | sleep 0.25
29 | end
30 |
--------------------------------------------------------------------------------
/examples/color_picker.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad'
2 |
3 | interaction = Launchpad::Interaction.new
4 |
5 | # build color arrays for color display views
6 | colors_single = [
7 | [ 0, 1, 2, 3, 0, 0, 0, 0],
8 | [16, 17, 18, 19, 0, 0, 0, 0],
9 | [32, 33, 34, 35, 0, 0, 0, 0],
10 | [48, 49, 50, 51, 0, 0, 0, 0],
11 | [0] * 8,
12 | [0] * 8,
13 | [0] * 8,
14 | [0] * 8,
15 | [0] * 8
16 | ]
17 | colors_double = [
18 | [ 0, 0, 1, 1, 2, 2, 3, 3],
19 | [ 0, 0, 1, 1, 2, 2, 3, 3],
20 | [16, 16, 17, 17, 18, 18, 19, 19],
21 | [16, 16, 17, 17, 18, 18, 19, 19],
22 | [32, 32, 33, 33, 34, 34, 35, 35],
23 | [32, 32, 33, 33, 34, 34, 35, 35],
24 | [48, 48, 49, 49, 50, 50, 51, 51],
25 | [48, 48, 49, 49, 50, 50, 51, 51],
26 | [0] * 8
27 | ]
28 | colors_mirrored = [
29 | [ 0, 1, 2, 3, 3, 2, 1, 0],
30 | [16, 17, 18, 19, 19, 18, 17, 16],
31 | [32, 33, 34, 35, 35, 34, 33, 32],
32 | [48, 49, 50, 51, 51, 50, 49, 48],
33 | [48, 49, 50, 51, 51, 50, 49, 48],
34 | [32, 33, 34, 35, 35, 34, 33, 32],
35 | [16, 17, 18, 19, 19, 18, 17, 16],
36 | [ 0, 1, 2, 3, 3, 2, 1, 0],
37 | [0] * 8
38 | ]
39 |
40 | # setup color display views
41 | def display_color_view(colors)
42 | lambda do |interaction, action|
43 | # set color
44 | interaction.device.change_all(colors)
45 | # register mute interactor on scene buttons
46 | interaction.response_to(%w(scene1 scene2 scene3 scene4 scene5 scene6 scene7 scene8), :down, :exclusive => true, &@mute)
47 | end
48 | end
49 | interaction.response_to(:up, :down, &display_color_view(colors_single + [48, 16, 16, 16]))
50 | interaction.response_to(:down, :down, &display_color_view(colors_double + [16, 48, 16, 16]))
51 | interaction.response_to(:left, :down, &display_color_view(colors_mirrored + [16, 16, 48, 16]))
52 |
53 | # setup color picker view
54 | def display_color(opts)
55 | lambda do |interaction, action|
56 | @red = opts[:red] if opts[:red]
57 | @green = opts[:green] if opts[:green]
58 | colors = [(@green * 16 + @red)] * 64
59 | scenes = [@red == 3 ? 51 : 3, @red == 2 ? 51 : 2, @red == 1 ? 51 : 1, @red == 0 ? 51 : 0, @green == 3 ? 51 : 48, @green == 2 ? 51 : 32, @green == 1 ? 51 : 16, @green == 0 ? 51 : 0]
60 | interaction.device.change_all(colors + scenes + [16, 16, 16, 48])
61 | end
62 | end
63 | interaction.response_to(:right, :down) do |interaction, action|
64 | @red = 0
65 | @green = 0
66 | # register color picker interactors on scene buttons
67 | interaction.response_to(:scene1, :down, :exclusive => true, &display_color(:red => 3))
68 | interaction.response_to(:scene2, :down, :exclusive => true, &display_color(:red => 2))
69 | interaction.response_to(:scene3, :down, :exclusive => true, &display_color(:red => 1))
70 | interaction.response_to(:scene4, :down, :exclusive => true, &display_color(:red => 0))
71 | interaction.response_to(:scene5, :down, :exclusive => true, &display_color(:green => 3))
72 | interaction.response_to(:scene6, :down, :exclusive => true, &display_color(:green => 2))
73 | interaction.response_to(:scene7, :down, :exclusive => true, &display_color(:green => 1))
74 | interaction.response_to(:scene8, :down, :exclusive => true, &display_color(:green => 0))
75 | # display color
76 | interaction.respond_to(:scene8, :down)
77 | end
78 |
79 | # mixer button terminates interaction on button up
80 | interaction.response_to(:mixer) do |interaction, action|
81 | interaction.device.change(:mixer, :red => action[:state] == :down ? :hi : :off)
82 | interaction.stop if action[:state] == :up
83 | end
84 |
85 | # setup mute display interactors on all unused buttons
86 | @mute = display_color_view([0] * 72 + [16, 16, 16, 16])
87 | interaction.response_to(%w(session user1 user2 grid), :down, &@mute)
88 |
89 | # display mute view
90 | interaction.respond_to(:session, :down)
91 |
92 | # start interacting
93 | interaction.start
94 |
95 | # sleep so that the messages can be sent before the program terminates
96 | sleep 0.1
97 |
--------------------------------------------------------------------------------
/examples/colors.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad'
2 |
3 | device = Launchpad::Device.new(:input => false, :output => true)
4 |
5 | pos_x = pos_y = 0
6 | 4.times do |red|
7 | 4.times do |green|
8 | device.change :grid, :x => pos_x, :y => pos_y, :red => red, :green => green
9 | device.change :grid, :x => 7 - pos_x, :y => pos_y, :red => red, :green => green
10 | device.change :grid, :x => pos_x, :y => 7 - pos_y, :red => red, :green => green
11 | device.change :grid, :x => 7 - pos_x, :y => 7 - pos_y, :red => red, :green => green
12 | pos_y += 1
13 | # sleep, otherwise the connection drops some messages - WTF?
14 | sleep 0.01
15 | end
16 | pos_x += 1
17 | pos_y = 0
18 | end
19 |
20 | # sleep so that the messages can be sent before the program terminates
21 | sleep 0.1
22 |
--------------------------------------------------------------------------------
/examples/doodle.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad'
2 |
3 | interaction = Launchpad::Interaction.new
4 |
5 | current_color = {
6 | :red => :hi,
7 | :green => :hi,
8 | :mode => :normal
9 | }
10 |
11 | def update_scene_buttons(d, color)
12 | on = {:red => :hi, :green => :hi}
13 | d.change(:scene1, color[:red] == :hi ? on : {:red => :hi})
14 | d.change(:scene2, color[:red] == :med ? on : {:red => :med})
15 | d.change(:scene3, color[:red] == :lo ? on : {:red => :lo})
16 | d.change(:scene4, color[:red] == :off ? on : {:red => :off})
17 | d.change(:scene5, color[:green] == :hi ? on : {:green => :hi})
18 | d.change(:scene6, color[:green] == :med ? on : {:green => :med})
19 | d.change(:scene7, color[:green] == :lo ? on : {:green => :lo})
20 | d.change(:scene8, color[:green] == :off ? on : {:green => :off})
21 | d.change(:user1, :green => color[:mode] == :normal ? :lo : :hi, :mode => :flashing)
22 | d.change(:user2, :green => color[:mode] == :normal ? :hi : :lo)
23 | end
24 |
25 | def choose_color(color, opts)
26 | lambda do |interaction, action|
27 | color.update(opts)
28 | update_scene_buttons(interaction.device, color)
29 | end
30 | end
31 |
32 | # register color picker interactors on scene buttons
33 | interaction.response_to(:scene1, :down, &choose_color(current_color, :red => :hi))
34 | interaction.response_to(:scene2, :down, &choose_color(current_color, :red => :med))
35 | interaction.response_to(:scene3, :down, &choose_color(current_color, :red => :lo))
36 | interaction.response_to(:scene4, :down, &choose_color(current_color, :red => :off))
37 | interaction.response_to(:scene5, :down, &choose_color(current_color, :green => :hi))
38 | interaction.response_to(:scene6, :down, &choose_color(current_color, :green => :med))
39 | interaction.response_to(:scene7, :down, &choose_color(current_color, :green => :lo))
40 | interaction.response_to(:scene8, :down, &choose_color(current_color, :green => :off))
41 |
42 | # register mode picker interactors on user buttons
43 | interaction.response_to(:user1, :down, &choose_color(current_color, :mode => :flashing))
44 | interaction.response_to(:user2, :down, &choose_color(current_color, :mode => :normal))
45 |
46 | # update scene buttons and start flashing
47 | update_scene_buttons(interaction.device, current_color)
48 | interaction.device.flashing_auto
49 |
50 | # feedback for grid buttons
51 | interaction.response_to(:grid, :down) do |interaction, action|
52 | #coord = 16 * action[:y] + action[:x]
53 | #brightness = flags[coord] ? :off : :hi
54 | #flags[coord] = !flags[coord]
55 | interaction.device.change(:grid, action.merge(current_color))
56 | end
57 |
58 | # mixer button terminates interaction on button up
59 | interaction.response_to(:mixer) do |interaction, action|
60 | interaction.device.change(:mixer, :red => action[:state] == :down ? :hi : :off)
61 | interaction.stop if action[:state] == :up
62 | end
63 |
64 | # start interacting
65 | interaction.start
66 |
67 | # sleep so that the messages can be sent before the program terminates
68 | sleep 0.1
69 |
--------------------------------------------------------------------------------
/examples/double_buffering.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad'
2 |
3 | interaction = Launchpad::Interaction.new
4 |
5 | # store and change button states, ugly but well...
6 | @button_states = [
7 | [false, false, false, false, false, false, false, false],
8 | [false, false, false, false, false, false, false, false],
9 | [[false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false]],
10 | [[false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false]],
11 | [[false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false]],
12 | [[false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false]],
13 | [[false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false]],
14 | [[false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false], [false, false]]
15 | ]
16 | def change_button_state(action)
17 | if action[:y] > 1
18 | which = @active_buffer_button == :user2 ? 1 : 0
19 | @button_states[action[:y]][action[:x]][which] = !@button_states[action[:y]][action[:x]][which]
20 | else
21 | @button_states[action[:y]][action[:x]] = !@button_states[action[:y]][action[:x]]
22 | end
23 | end
24 |
25 | # setup grid buttons to:
26 | # * set LEDs in normal mode on the first row
27 | # * set LEDs in flashing mode on the second row
28 | # * set LEDs in buffering mode on all other rows
29 | interaction.response_to(:grid, :down) do |interaction, action|
30 | color = change_button_state(action) ? @color : {}
31 | case action[:y]
32 | when 0
33 | interaction.device.change(:grid, action.merge(color))
34 | when 1
35 | interaction.device.buffering_mode(:flashing => false, :display_buffer => 1, :update_buffer => 0)
36 | interaction.device.change(:grid, action.merge(color).merge(:mode => :flashing))
37 | interaction.respond_to(@active_buffer_button, :down)
38 | else
39 | interaction.device.change(:grid, action.merge(color).merge(:mode => :buffering))
40 | end
41 | end
42 |
43 | # green feedback for buffer buttons
44 | interaction.response_to([:session, :user1, :user2], :down) do |interaction, action|
45 | case @active_buffer_button = action[:type]
46 | when :session
47 | interaction.device.buffering_mode(:flashing => true)
48 | when :user1
49 | interaction.device.buffering_mode(:display_buffer => 0, :update_buffer => 0)
50 | when :user2
51 | interaction.device.buffering_mode(:display_buffer => 1, :update_buffer => 1)
52 | end
53 | interaction.device.change(:session, :red => @active_buffer_button == :session ? :hi : :lo, :green => @active_buffer_button == :session ? :hi : :lo)
54 | interaction.device.change(:user1, :red => @active_buffer_button == :user1 ? :hi : :lo, :green => @active_buffer_button == :user1 ? :hi : :lo)
55 | interaction.device.change(:user2, :red => @active_buffer_button == :user2 ? :hi : :lo, :green => @active_buffer_button == :user2 ? :hi : :lo)
56 | end
57 |
58 | # setup color picker
59 | def display_color(opts)
60 | lambda do |interaction, action|
61 | @red = opts[:red] if opts[:red]
62 | @green = opts[:green] if opts[:green]
63 | if @red == 0 && @green == 0
64 | @red = 1 if opts[:red]
65 | @green = 1 if opts[:green]
66 | end
67 | @color = {:red => @red, :green => @green}
68 | on = {:red => 3, :green => 3}
69 | interaction.device.change(:scene1, @red == 3 ? on : {:red => 3})
70 | interaction.device.change(:scene2, @red == 2 ? on : {:red => 2})
71 | interaction.device.change(:scene3, @red == 1 ? on : {:red => 1})
72 | interaction.device.change(:scene4, @red == 0 ? on : {:red => 0})
73 | interaction.device.change(:scene5, @green == 3 ? on : {:green => 3})
74 | interaction.device.change(:scene6, @green == 2 ? on : {:green => 2})
75 | interaction.device.change(:scene7, @green == 1 ? on : {:green => 1})
76 | interaction.device.change(:scene8, @green == 0 ? on : {:green => 0})
77 | end
78 | end
79 | # register color picker interactors on scene buttons
80 | interaction.response_to(:scene1, :down, :exclusive => true, &display_color(:red => 3))
81 | interaction.response_to(:scene2, :down, :exclusive => true, &display_color(:red => 2))
82 | interaction.response_to(:scene3, :down, :exclusive => true, &display_color(:red => 1))
83 | interaction.response_to(:scene4, :down, :exclusive => true, &display_color(:red => 0))
84 | interaction.response_to(:scene5, :down, :exclusive => true, &display_color(:green => 3))
85 | interaction.response_to(:scene6, :down, :exclusive => true, &display_color(:green => 2))
86 | interaction.response_to(:scene7, :down, :exclusive => true, &display_color(:green => 1))
87 | interaction.response_to(:scene8, :down, :exclusive => true, &display_color(:green => 0))
88 | # pick green
89 | interaction.respond_to(:scene5, :down)
90 |
91 | # mixer button terminates interaction on button up
92 | interaction.response_to(:mixer) do |interaction, action|
93 | interaction.device.change(:mixer, :red => action[:state] == :down ? :hi : :off)
94 | interaction.stop if action[:state] == :up
95 | end
96 |
97 | # start in auto flashing mode
98 | interaction.respond_to(:session, :down)
99 |
100 | # start interacting
101 | interaction.start
102 |
103 | # sleep so that the messages can be sent before the program terminates
104 | sleep 0.1
105 |
--------------------------------------------------------------------------------
/examples/drawing_board.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad'
2 |
3 | interaction = Launchpad::Interaction.new
4 |
5 | flags = Hash.new(false)
6 |
7 | # yellow feedback for grid buttons
8 | interaction.response_to(:grid, :down) do |interaction, action|
9 | coord = 16 * action[:y] + action[:x]
10 | brightness = flags[coord] ? :off : :hi
11 | flags[coord] = !flags[coord]
12 | interaction.device.change(:grid, action.merge(:red => brightness, :green => brightness))
13 | end
14 |
15 | # mixer button terminates interaction on button up
16 | interaction.response_to(:mixer) do |interaction, action|
17 | interaction.device.change(:mixer, :red => action[:state] == :down ? :hi : :off)
18 | interaction.stop if action[:state] == :up
19 | end
20 |
21 | # start interacting
22 | interaction.start
23 |
24 | # sleep so that the messages can be sent before the program terminates
25 | sleep 0.1
26 |
--------------------------------------------------------------------------------
/examples/feedback.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad'
2 |
3 | interaction = Launchpad::Interaction.new
4 |
5 | def brightness(action)
6 | action[:state] == :down ? :hi : :off
7 | end
8 |
9 | # yellow feedback for grid buttons
10 | interaction.response_to(:grid) do |interaction, action|
11 | b = brightness(action)
12 | interaction.device.change(:grid, action.merge(:red => b, :green => b))
13 | end
14 |
15 | # red feedback for top control buttons
16 | interaction.response_to([:up, :down, :left, :right, :session, :user1, :user2, :mixer]) do |interaction, action|
17 | interaction.device.change(action[:type], :red => brightness(action))
18 | end
19 |
20 | # green feedback for scene buttons
21 | interaction.response_to([:scene1, :scene2, :scene3, :scene4, :scene5, :scene6, :scene7, :scene8]) do |interaction, action|
22 | interaction.device.change(action[:type], :green => brightness(action))
23 | end
24 |
25 | # mixer button terminates interaction on button up
26 | interaction.response_to(:mixer, :up) do |interaction, action|
27 | interaction.stop
28 | end
29 |
30 | # start interacting
31 | interaction.start
32 |
33 | # sleep so that the messages can be sent before the program terminates
34 | sleep 0.1
35 |
--------------------------------------------------------------------------------
/examples/reset.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad'
2 |
3 | Launchpad::Device.new.reset
4 |
5 | # sleep so that the messages can be sent before the program terminates
6 | sleep 0.1
7 |
--------------------------------------------------------------------------------
/launchpad.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "launchpad/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "launchpad"
7 | s.version = Launchpad::VERSION
8 | s.authors = ["Thomas Jachmann"]
9 | s.email = ["self@thomasjachmann.com"]
10 | s.homepage = "https://github.com/thomasjachmann/launchpad"
11 | s.summary = %q{A gem for accessing novation's launchpad programmatically and easily.}
12 | s.description = %q{This gem provides an interface to access novation's launchpad programmatically. LEDs can be lighted and button presses can be evaluated using launchpad's MIDI input/output.}
13 |
14 | s.rubyforge_project = "launchpad"
15 |
16 | s.add_dependency "portmidi", ">= 0.0.6"
17 | s.add_dependency "ffi"
18 | s.add_development_dependency "rake"
19 | if RUBY_VERSION < "1.9"
20 | s.add_development_dependency "minitest"
21 | # s.add_development_dependency "ruby-debug"
22 | else
23 | s.add_development_dependency "minitest-reporters"
24 | # s.add_development_dependency "debugger"
25 | end
26 | s.add_development_dependency "mocha"
27 |
28 | # s.has_rdoc = true
29 |
30 | s.files = `git ls-files`.split("\n")
31 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
32 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
33 | s.require_paths = ["lib"]
34 | end
35 |
--------------------------------------------------------------------------------
/lib/launchpad.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad/interaction'
2 |
3 | # All the fun of launchpad in one module!
4 | #
5 | # See Launchpad::Device for basic access to launchpad input/ouput
6 | # and Launchpad::Interaction for advanced interaction features.
7 | #
8 | # The following parameters will be used throughout the library, so here are the ranges:
9 | #
10 | # [+type+] type of the button, one of
11 | #
12 | # :grid,
13 | # :up, :down, :left, :right, :session, :user1, :user2, :mixer,
14 | # :scene1 - :scene8
15 | #
16 | # [x/y] x/y coordinate (used when type is set to :grid),
17 | # 0-7 (from left to right/top to bottom),
18 | # mandatory when +type+ is set to :grid
19 | # [red/green] brightness of the red/green LED,
20 | # can be set to one of four levels:
21 | # * off (:off, 0)
22 | # * low brightness (:low, :lo, 1)
23 | # * medium brightness (:medium, :med, 2)
24 | # * full brightness (:high, :hi, 3)
25 | # optional, defaults to :off
26 | # [+mode+] button mode,
27 | # one of
28 | # * :normal
29 | # * :flashing (LED is marked as flashing, see Launchpad::Device.flashing_on, Launchpad::Device.flashing_off and Launchpad::Device.flashing_auto)
30 | # * :buffering (LED is written to buffer, see Launchpad::Device.start_buffering, Launchpad::Device.flush_buffer)
31 | # optional, defaults to :normal
32 | # [+state+] whether the button is pressed or released, :down/:up
33 | module Launchpad
34 | end
35 |
--------------------------------------------------------------------------------
/lib/launchpad/device.rb:
--------------------------------------------------------------------------------
1 | require 'portmidi'
2 |
3 | require 'launchpad/errors'
4 | require 'launchpad/logging'
5 | require 'launchpad/midi_codes'
6 | require 'launchpad/version'
7 |
8 | module Launchpad
9 |
10 | # This class is used to exchange data with the launchpad.
11 | # It provides methods to light LEDs and to get information about button presses/releases.
12 | #
13 | # Example:
14 | #
15 | # require 'launchpad/device'
16 | #
17 | # device = Launchpad::Device.new
18 | # device.test_leds
19 | # sleep 1
20 | # device.reset
21 | # sleep 1
22 | # device.change :grid, :x => 4, :y => 4, :red => :high, :green => :low
23 | class Device
24 |
25 | include Logging
26 | include MidiCodes
27 |
28 | CODE_NOTE_TO_DATA_TYPE = {
29 | [Status::ON, SceneButton::SCENE1] => :scene1,
30 | [Status::ON, SceneButton::SCENE2] => :scene2,
31 | [Status::ON, SceneButton::SCENE3] => :scene3,
32 | [Status::ON, SceneButton::SCENE4] => :scene4,
33 | [Status::ON, SceneButton::SCENE5] => :scene5,
34 | [Status::ON, SceneButton::SCENE6] => :scene6,
35 | [Status::ON, SceneButton::SCENE7] => :scene7,
36 | [Status::ON, SceneButton::SCENE8] => :scene8,
37 | [Status::CC, ControlButton::UP] => :up,
38 | [Status::CC, ControlButton::DOWN] => :down,
39 | [Status::CC, ControlButton::LEFT] => :left,
40 | [Status::CC, ControlButton::RIGHT] => :right,
41 | [Status::CC, ControlButton::SESSION] => :session,
42 | [Status::CC, ControlButton::USER1] => :user1,
43 | [Status::CC, ControlButton::USER2] => :user2,
44 | [Status::CC, ControlButton::MIXER] => :mixer
45 | }.freeze
46 |
47 | TYPE_TO_NOTE = {
48 | :up => ControlButton::UP,
49 | :down => ControlButton::DOWN,
50 | :left => ControlButton::LEFT,
51 | :right => ControlButton::RIGHT,
52 | :session => ControlButton::SESSION,
53 | :user1 => ControlButton::USER1,
54 | :user2 => ControlButton::USER2,
55 | :mixer => ControlButton::MIXER,
56 | :scene1 => SceneButton::SCENE1,
57 | :scene2 => SceneButton::SCENE2,
58 | :scene3 => SceneButton::SCENE3,
59 | :scene4 => SceneButton::SCENE4,
60 | :scene5 => SceneButton::SCENE5,
61 | :scene6 => SceneButton::SCENE6,
62 | :scene7 => SceneButton::SCENE7,
63 | :scene8 => SceneButton::SCENE8
64 | }.freeze
65 |
66 | # Initializes the launchpad device. When output capabilities are requested,
67 | # the launchpad will be reset.
68 | #
69 | # Optional options hash:
70 | #
71 | # [:input] whether to use MIDI input for user interaction,
72 | # true/false, optional, defaults to +true+
73 | # [:output] whether to use MIDI output for data display,
74 | # true/false, optional, defaults to +true+
75 | # [:input_device_id] ID of the MIDI input device to use,
76 | # optional, :device_name will be used if omitted
77 | # [:output_device_id] ID of the MIDI output device to use,
78 | # optional, :device_name will be used if omitted
79 | # [:device_name] Name of the MIDI device to use,
80 | # optional, defaults to "Launchpad"
81 | # [:logger] [Logger] to be used by this device instance, can be changed afterwards
82 | #
83 | # Errors raised:
84 | #
85 | # [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
86 | # [Launchpad::DeviceBusyError] when device with ID or name specified is busy
87 | def initialize(opts = nil)
88 | opts = {
89 | :input => true,
90 | :output => true
91 | }.merge(opts || {})
92 |
93 | self.logger = opts[:logger]
94 | logger.debug "initializing Launchpad::Device##{object_id} with #{opts.inspect}"
95 |
96 | Portmidi.start
97 |
98 | @input = create_device!(Portmidi.input_devices, Portmidi::Input,
99 | :id => opts[:input_device_id],
100 | :name => opts[:device_name]
101 | ) if opts[:input]
102 | @output = create_device!(Portmidi.output_devices, Portmidi::Output,
103 | :id => opts[:output_device_id],
104 | :name => opts[:device_name]
105 | ) if opts[:output]
106 |
107 | reset if output_enabled?
108 | end
109 |
110 | # Closes the device - nothing can be done with the device afterwards.
111 | def close
112 | logger.debug "closing Launchpad::Device##{object_id}"
113 | @input.close unless @input.nil?
114 | @input = nil
115 | @output.close unless @output.nil?
116 | @output = nil
117 | end
118 |
119 | # Determines whether this device has been closed.
120 | def closed?
121 | !(input_enabled? || output_enabled?)
122 | end
123 |
124 | # Determines whether this device can be used to read input.
125 | def input_enabled?
126 | !@input.nil?
127 | end
128 |
129 | # Determines whether this device can be used to output data.
130 | def output_enabled?
131 | !@output.nil?
132 | end
133 |
134 | # Resets the launchpad - all settings are reset and all LEDs are switched off.
135 | #
136 | # Errors raised:
137 | #
138 | # [Launchpad::NoOutputAllowedError] when output is not enabled
139 | def reset
140 | output(Status::CC, Status::NIL, Status::NIL)
141 | end
142 |
143 | # Lights all LEDs (for testing purposes).
144 | #
145 | # Parameters (see Launchpad for values):
146 | #
147 | # [+brightness+] brightness of both LEDs for all buttons
148 | #
149 | # Errors raised:
150 | #
151 | # [Launchpad::NoOutputAllowedError] when output is not enabled
152 | def test_leds(brightness = :high)
153 | brightness = brightness(brightness)
154 | if brightness == 0
155 | reset
156 | else
157 | output(Status::CC, Status::NIL, Velocity::TEST_LEDS + brightness)
158 | end
159 | end
160 |
161 | # Changes a single LED.
162 | #
163 | # Parameters (see Launchpad for values):
164 | #
165 | # [+type+] type of the button to change
166 | #
167 | # Optional options hash (see Launchpad for values):
168 | #
169 | # [:x] x coordinate
170 | # [:y] y coordinate
171 | # [:red] brightness of red LED
172 | # [:green] brightness of green LED
173 | # [:mode] button mode, defaults to :normal, one of:
174 | # [:normal/tt>] updates the LED for all circumstances (the new value will be written to both buffers)
175 | # [:flashing/tt>] updates the LED for flashing (the new value will be written to buffer 0 while the LED will be off in buffer 1, see buffering_mode)
176 | # [:buffering/tt>] updates the LED for the current update_buffer only
177 | #
178 | # Errors raised:
179 | #
180 | # [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range
181 | # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
182 | # [Launchpad::NoOutputAllowedError] when output is not enabled
183 | def change(type, opts = nil)
184 | opts ||= {}
185 | status = %w(up down left right session user1 user2 mixer).include?(type.to_s) ? Status::CC : Status::ON
186 | output(status, note(type, opts), velocity(opts))
187 | end
188 |
189 | # Changes all LEDs in batch mode.
190 | #
191 | # Parameters (see Launchpad for values):
192 | #
193 | # [+colors] an array of colors, each either being an integer or a Hash
194 | # * integer: calculated using the formula
195 | # color = 16 * green + red
196 | # * Hash:
197 | # [:red] brightness of red LED
198 | # [:green] brightness of green LED
199 | # [:mode] button mode, defaults to :normal, one of:
200 | # [:normal/tt>] updates the LEDs for all circumstances (the new value will be written to both buffers)
201 | # [:flashing/tt>] updates the LEDs for flashing (the new values will be written to buffer 0 while the LEDs will be off in buffer 1, see buffering_mode)
202 | # [:buffering/tt>] updates the LEDs for the current update_buffer only
203 | # the array consists of 64 colors for the grid buttons,
204 | # 8 colors for the scene buttons (top to bottom)
205 | # and 8 colors for the top control buttons (left to right),
206 | # maximum 80 values - excessive values will be ignored,
207 | # missing values will be filled with 0
208 | #
209 | # Errors raised:
210 | #
211 | # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
212 | # [Launchpad::NoOutputAllowedError] when output is not enabled
213 | def change_all(*colors)
214 | # ensure that colors is at least and most 80 elements long
215 | colors = colors.flatten[0..79]
216 | colors += [0] * (80 - colors.size) if colors.size < 80
217 | # send normal MIDI message to reset rapid LED change pointer
218 | # in this case, set mapping mode to x-y layout (the default)
219 | output(Status::CC, Status::NIL, GridLayout::XY)
220 | # send colors in slices of 2
221 | messages = []
222 | colors.each_slice(2) do |c1, c2|
223 | messages << message(Status::MULTI, velocity(c1), velocity(c2))
224 | end
225 | output_messages(messages)
226 | end
227 |
228 | # Switches LEDs marked as flashing on when using custom timer for flashing.
229 | #
230 | # Errors raised:
231 | #
232 | # [Launchpad::NoOutputAllowedError] when output is not enabled
233 | def flashing_on
234 | buffering_mode(:display_buffer => 0)
235 | end
236 |
237 | # Switches LEDs marked as flashing off when using custom timer for flashing.
238 | #
239 | # Errors raised:
240 | #
241 | # [Launchpad::NoOutputAllowedError] when output is not enabled
242 | def flashing_off
243 | buffering_mode(:display_buffer => 1)
244 | end
245 |
246 | # Starts flashing LEDs marked as flashing automatically.
247 | # Stop flashing by calling flashing_on or flashing_off.
248 | #
249 | # Errors raised:
250 | #
251 | # [Launchpad::NoOutputAllowedError] when output is not enabled
252 | def flashing_auto
253 | buffering_mode(:flashing => true)
254 | end
255 |
256 | # Controls the two buffers.
257 | #
258 | # Optional options hash:
259 | #
260 | # [:display_buffer] which buffer to use for display, defaults to +0+
261 | # [:update_buffer] which buffer to use for updates when :mode is set to :buffering, defaults to +0+ (see change)
262 | # [:copy] whether to copy the LEDs states from the new display_buffer over to the new update_buffer, true/false, defaults to false
263 | # [:flashing] whether to start flashing by automatically switching between the two buffers for display, true/false, defaults to false
264 | #
265 | # Errors raised:
266 | #
267 | # [Launchpad::NoOutputAllowedError] when output is not enabled
268 | def buffering_mode(opts = nil)
269 | opts = {
270 | :display_buffer => 0,
271 | :update_buffer => 0,
272 | :copy => false,
273 | :flashing => false
274 | }.merge(opts || {})
275 | data = opts[:display_buffer] + 4 * opts[:update_buffer] + 32
276 | data += 16 if opts[:copy]
277 | data += 8 if opts[:flashing]
278 | output(Status::CC, Status::NIL, data)
279 | end
280 |
281 | # Reads user actions (button presses/releases) that haven't been handled yet.
282 | # This is non-blocking, so when nothing happend yet you'll get an empty array.
283 | #
284 | # Returns:
285 | #
286 | # an array of hashes with (see Launchpad for values):
287 | #
288 | # [:timestamp] integer indicating the time when the action occured
289 | # [:state] state of the button after action
290 | # [:type] type of the button
291 | # [:x] x coordinate
292 | # [:y] y coordinate
293 | #
294 | # Errors raised:
295 | #
296 | # [Launchpad::NoInputAllowedError] when input is not enabled
297 | def read_pending_actions
298 | Array(input).collect do |midi_message|
299 | (code, note, velocity) = midi_message[:message]
300 | data = {
301 | :timestamp => midi_message[:timestamp],
302 | :state => (velocity == 127 ? :down : :up)
303 | }
304 | data[:type] = CODE_NOTE_TO_DATA_TYPE[[code, note]] || :grid
305 | if data[:type] == :grid
306 | data[:x] = note % 16
307 | data[:y] = note / 16
308 | end
309 | data
310 | end
311 | end
312 |
313 | private
314 |
315 | # Creates input/output devices.
316 | #
317 | # Parameters:
318 | #
319 | # [+devices+] array of portmidi devices
320 | # [+device_type] class to instantiate (Portmidi::Input/Portmidi::Output)
321 | #
322 | # Options hash:
323 | #
324 | # [:id] id of the MIDI device to use
325 | # [:name] name of the MIDI device to use,
326 | # only used when :id is not specified,
327 | # defaults to "Launchpad"
328 | #
329 | # Returns:
330 | #
331 | # newly created device
332 | #
333 | # Errors raised:
334 | #
335 | # [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
336 | # [Launchpad::DeviceBusyError] when device with ID or name specified is busy
337 | def create_device!(devices, device_type, opts)
338 | logger.debug "creating #{device_type} with #{opts.inspect}, choosing from portmidi devices #{devices.inspect}"
339 | id = opts[:id]
340 | if id.nil?
341 | name = opts[:name] || 'Launchpad'
342 | device = devices.select {|device| device.name == name}.first
343 | id = device.device_id unless device.nil?
344 | end
345 | if id.nil?
346 | message = "MIDI device #{opts[:id] || opts[:name]} doesn't exist"
347 | logger.fatal message
348 | raise NoSuchDeviceError.new(message)
349 | end
350 | device_type.new(id)
351 | rescue RuntimeError => e
352 | logger.fatal "error creating #{device_type}: #{e.inspect}"
353 | raise DeviceBusyError.new(e)
354 | end
355 |
356 | # Reads input from the MIDI device.
357 | #
358 | # Returns:
359 | #
360 | # an array of hashes with:
361 | #
362 | # [:message] an array of
363 | # MIDI status code,
364 | # MIDI data 1 (note),
365 | # MIDI data 2 (velocity)
366 | # and a fourth value
367 | # [:timestamp] integer indicating the time when the MIDI message was created
368 | #
369 | # Errors raised:
370 | #
371 | # [Launchpad::NoInputAllowedError] when output is not enabled
372 | def input
373 | if @input.nil?
374 | logger.error "trying to read from device that's not been initialized for input"
375 | raise NoInputAllowedError
376 | end
377 | @input.read(16)
378 | end
379 |
380 | # Writes data to the MIDI device.
381 | #
382 | # Parameters:
383 | #
384 | # [+status+] MIDI status code
385 | # [+data1+] MIDI data 1 (note)
386 | # [+data2+] MIDI data 2 (velocity)
387 | #
388 | # Errors raised:
389 | #
390 | # [Launchpad::NoOutputAllowedError] when output is not enabled
391 | def output(status, data1, data2)
392 | output_messages([message(status, data1, data2)])
393 | end
394 |
395 | # Writes several messages to the MIDI device.
396 | #
397 | # Parameters:
398 | #
399 | # [+messages+] an array of hashes (usually created with message) with:
400 | # [:message] an array of
401 | # MIDI status code,
402 | # MIDI data 1 (note),
403 | # MIDI data 2 (velocity)
404 | # [:timestamp] integer indicating the time when the MIDI message was created
405 | def output_messages(messages)
406 | if @output.nil?
407 | logger.error "trying to write to device that's not been initialized for output"
408 | raise NoOutputAllowedError
409 | end
410 | logger.debug "writing messages to launchpad:\n #{messages.join("\n ")}" if logger.debug?
411 | @output.write(messages)
412 | nil
413 | end
414 |
415 | # Calculates the MIDI data 1 value (note) for a button.
416 | #
417 | # Parameters (see Launchpad for values):
418 | #
419 | # [+type+] type of the button
420 | #
421 | # Options hash:
422 | #
423 | # [:x] x coordinate
424 | # [:y] y coordinate
425 | #
426 | # Returns:
427 | #
428 | # integer to be used for MIDI data 1
429 | #
430 | # Errors raised:
431 | #
432 | # [Launchpad::NoValidGridCoordinatesError] when coordinates aren't within the valid range
433 | def note(type, opts)
434 | note = TYPE_TO_NOTE[type]
435 | if note.nil?
436 | x = (opts[:x] || -1).to_i
437 | y = (opts[:y] || -1).to_i
438 | if x < 0 || x > 7 || y < 0 || y > 7
439 | logger.error "wrong coordinates specified: x=#{x}, y=#{y}"
440 | raise NoValidGridCoordinatesError.new("you need to specify valid coordinates (x/y, 0-7, from top left), you specified: x=#{x}, y=#{y}")
441 | end
442 | note = y * 16 + x
443 | end
444 | note
445 | end
446 |
447 | # Calculates the MIDI data 2 value (velocity) for given brightness and mode values.
448 | #
449 | # Options hash:
450 | #
451 | # [:red] brightness of red LED
452 | # [:green] brightness of green LED
453 | # [:mode] button mode, defaults to :normal, one of:
454 | # [:normal/tt>] updates the LED for all circumstances (the new value will be written to both buffers)
455 | # [:flashing/tt>] updates the LED for flashing (the new value will be written to buffer 0 while in buffer 1, the value will be :off, see )
456 | # [:buffering/tt>] updates the LED for the current update_buffer only
457 | #
458 | # Returns:
459 | #
460 | # integer to be used for MIDI data 2
461 | #
462 | # Errors raised:
463 | #
464 | # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
465 | def velocity(opts)
466 | if opts.is_a?(Hash)
467 | red = brightness(opts[:red] || 0)
468 | green = brightness(opts[:green] || 0)
469 | color = 16 * green + red
470 | flags = case opts[:mode]
471 | when :flashing then 8
472 | when :buffering then 0
473 | else 12
474 | end
475 | color + flags
476 | else
477 | opts.to_i + 12
478 | end
479 | end
480 |
481 | # Calculates the integer brightness for given brightness values.
482 | #
483 | # Parameters (see Launchpad for values):
484 | #
485 | # [+brightness+] brightness
486 | #
487 | # Errors raised:
488 | #
489 | # [Launchpad::NoValidBrightnessError] when brightness values aren't within the valid range
490 | def brightness(brightness)
491 | case brightness
492 | when 0, :off then 0
493 | when 1, :low, :lo then 1
494 | when 2, :medium, :med then 2
495 | when 3, :high, :hi then 3
496 | else
497 | logger.error "wrong brightness specified: #{brightness}"
498 | 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}")
499 | end
500 | end
501 |
502 | # Creates a MIDI message.
503 | #
504 | # Parameters:
505 | #
506 | # [+status+] MIDI status code
507 | # [+data1+] MIDI data 1 (note)
508 | # [+data2+] MIDI data 2 (velocity)
509 | #
510 | # Returns:
511 | #
512 | # an array with:
513 | #
514 | # [:message] an array of
515 | # MIDI status code,
516 | # MIDI data 1 (note),
517 | # MIDI data 2 (velocity)
518 | # [:timestamp] integer indicating the time when the MIDI message was created, in this case 0
519 | def message(status, data1, data2)
520 | {:message => [status, data1, data2], :timestamp => 0}
521 | end
522 |
523 | end
524 |
525 | end
526 |
--------------------------------------------------------------------------------
/lib/launchpad/errors.rb:
--------------------------------------------------------------------------------
1 | module Launchpad
2 |
3 | # Generic launchpad error.
4 | class LaunchpadError < StandardError; end
5 |
6 | # Error raised when the MIDI device specified doesn't exist.
7 | class NoSuchDeviceError < LaunchpadError; end
8 |
9 | # Error raised when the MIDI device specified is busy.
10 | class DeviceBusyError < LaunchpadError; end
11 |
12 | # Error raised when an input has been requested, although
13 | # launchpad has been initialized without input.
14 | class NoInputAllowedError < LaunchpadError; end
15 |
16 | # Error raised when an output has been requested, although
17 | # launchpad has been initialized without output.
18 | class NoOutputAllowedError < LaunchpadError; end
19 |
20 | # Error raised when x/y coordinates outside of the grid
21 | # or none were specified.
22 | class NoValidGridCoordinatesError < LaunchpadError; end
23 |
24 | # Error raised when wrong brightness was specified.
25 | class NoValidBrightnessError < LaunchpadError; end
26 |
27 | # Error raised when anything fails while communicating
28 | # with the launchpad.
29 | class CommunicationError < LaunchpadError
30 | attr_accessor :source
31 | def initialize(e)
32 | super(e.portmidi_error)
33 | self.source = e
34 | end
35 | end
36 |
37 | end
38 |
--------------------------------------------------------------------------------
/lib/launchpad/interaction.rb:
--------------------------------------------------------------------------------
1 | require 'launchpad/device'
2 | require 'launchpad/logging'
3 |
4 | module Launchpad
5 |
6 | # This class provides advanced interaction features.
7 | #
8 | # Example:
9 | #
10 | # require 'launchpad'
11 | #
12 | # interaction = Launchpad::Interaction.new
13 | # interaction.response_to(:grid, :down) do |interaction, action|
14 | # interaction.device.change(:grid, action.merge(:red => :high))
15 | # end
16 | # interaction.response_to(:mixer, :down) do |interaction, action|
17 | # interaction.stop
18 | # end
19 | #
20 | # interaction.start
21 | class Interaction
22 |
23 | include Logging
24 |
25 | # Returns the Launchpad::Device the Launchpad::Interaction acts on.
26 | attr_reader :device
27 |
28 | # Returns whether the Launchpad::Interaction is active or not.
29 | attr_reader :active
30 |
31 | # Initializes the interaction.
32 | #
33 | # Optional options hash:
34 | #
35 | # [:device] Launchpad::Device to act on,
36 | # optional, :input_device_id/:output_device_id will be used if omitted
37 | # [:input_device_id] ID of the MIDI input device to use,
38 | # optional, :device_name will be used if omitted
39 | # [:output_device_id] ID of the MIDI output device to use,
40 | # optional, :device_name will be used if omitted
41 | # [:device_name] Name of the MIDI device to use,
42 | # optional, defaults to "Launchpad"
43 | # [:latency] delay (in s, fractions allowed) between MIDI pulls,
44 | # optional, defaults to 0.001 (1ms)
45 | # [:logger] [Logger] to be used by this interaction instance, can be changed afterwards
46 | #
47 | # Errors raised:
48 | #
49 | # [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
50 | # [Launchpad::DeviceBusyError] when device with ID or name specified is busy
51 | def initialize(opts = nil)
52 | opts ||= {}
53 |
54 | self.logger = opts[:logger]
55 | logger.debug "initializing Launchpad::Interaction##{object_id} with #{opts.inspect}"
56 |
57 | @device = opts[:device]
58 | @device ||= Device.new(opts.merge(
59 | :input => true,
60 | :output => true,
61 | :logger => opts[:logger]
62 | ))
63 | @latency = (opts[:latency] || 0.001).to_f.abs
64 | @active = false
65 |
66 | @action_threads = ThreadGroup.new
67 | end
68 |
69 | # Sets the logger to be used by the current instance and the device.
70 | #
71 | # [+logger+] the [Logger] instance
72 | def logger=(logger)
73 | @logger = logger
74 | @device.logger = logger if @device
75 | end
76 |
77 | # Closes the interaction's device - nothing can be done with the interaction/device afterwards.
78 | #
79 | # Errors raised:
80 | #
81 | # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
82 | # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the
83 | def close
84 | logger.debug "closing Launchpad::Interaction##{object_id}"
85 | stop
86 | @device.close
87 | end
88 |
89 | # Determines whether this interaction's device has been closed.
90 | def closed?
91 | @device.closed?
92 | end
93 |
94 | # Starts interacting with the launchpad. Resets the device when
95 | # the interaction was properly stopped via stop or close.
96 | #
97 | # Optional options hash:
98 | #
99 | # [:detached] true/false,
100 | # whether to detach the interaction, method is blocking when +false+,
101 | # optional, defaults to +false+
102 | #
103 | # Errors raised:
104 | #
105 | # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
106 | # [Launchpad::NoOutputAllowedError] when output is not enabled on the interaction's device
107 | # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the launchpad
108 | def start(opts = nil)
109 | logger.debug "starting Launchpad::Interaction##{object_id}"
110 |
111 | opts = {
112 | :detached => false
113 | }.merge(opts || {})
114 |
115 | @active = true
116 |
117 | @reader_thread ||= Thread.new do
118 | begin
119 | while @active do
120 | @device.read_pending_actions.each do |action|
121 | action_thread = Thread.new(action) do |action|
122 | respond_to_action(action)
123 | end
124 | @action_threads.add(action_thread)
125 | end
126 | sleep @latency# if @latency > 0.0
127 | end
128 | rescue Portmidi::DeviceError => e
129 | logger.fatal "could not read from device, stopping to read actions"
130 | raise CommunicationError.new(e)
131 | rescue Exception => e
132 | logger.fatal "error causing action reading to stop: #{e.inspect}"
133 | raise e
134 | ensure
135 | @device.reset
136 | end
137 | end
138 | @reader_thread.join unless opts[:detached]
139 | end
140 |
141 | # Stops interacting with the launchpad.
142 | #
143 | # Errors raised:
144 | #
145 | # [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
146 | # [Launchpad::CommunicationError] when anything unexpected happens while communicating with the
147 | def stop
148 | logger.debug "stopping Launchpad::Interaction##{object_id}"
149 | @active = false
150 | if @reader_thread
151 | # run (resume from sleep) and wait for @reader_thread to end
152 | @reader_thread.run if @reader_thread.alive?
153 | @reader_thread.join
154 | @reader_thread = nil
155 | end
156 | ensure
157 | @action_threads.list.each do |thread|
158 | begin
159 | thread.kill
160 | thread.join
161 | rescue Exception => e
162 | logger.error "error when killing action thread: #{e.inspect}"
163 | end
164 | end
165 | nil
166 | end
167 |
168 | # Registers a response to one or more actions.
169 | #
170 | # Parameters (see Launchpad for values):
171 | #
172 | # [+types+] one or an array of button types to respond to,
173 | # additional value :all for all buttons
174 | # [+state+] button state to respond to,
175 | # additional value :both
176 | #
177 | # Optional options hash:
178 | #
179 | # [:exclusive] true/false,
180 | # whether to deregister all other responses to the specified actions,
181 | # optional, defaults to +false+
182 | # [:x] x coordinate(s), can contain arrays and ranges, when specified
183 | # without y coordinate, it's interpreted as a whole column
184 | # [:y] y coordinate(s), can contain arrays and ranges, when specified
185 | # without x coordinate, it's interpreted as a whole row
186 | #
187 | # Takes a block which will be called when an action matching the parameters occurs.
188 | #
189 | # Block parameters:
190 | #
191 | # [+interaction+] the interaction object that received the action
192 | # [+action+] the action received from Launchpad::Device.read_pending_actions
193 | def response_to(types = :all, state = :both, opts = nil, &block)
194 | logger.debug "setting response to #{types.inspect} for state #{state.inspect} with #{opts.inspect}"
195 | types = Array(types)
196 | opts ||= {}
197 | no_response_to(types, state) if opts[:exclusive] == true
198 | Array(state == :both ? %w(down up) : state).each do |state|
199 | types.each do |type|
200 | combined_types(type, opts).each do |combined_type|
201 | responses[combined_type][state.to_sym] << block
202 | end
203 | end
204 | end
205 | nil
206 | end
207 |
208 | # Deregisters all responses to one or more actions.
209 | #
210 | # Parameters (see Launchpad for values):
211 | #
212 | # [+types+] one or an array of button types to respond to,
213 | # additional value :all for actions on all buttons
214 | # (but not meaning "all responses"),
215 | # optional, defaults to +nil+, meaning "all responses"
216 | # [+state+] button state to respond to,
217 | # additional value :both
218 | #
219 | # Optional options hash:
220 | #
221 | # [:x] x coordinate(s), can contain arrays and ranges, when specified
222 | # without y coordinate, it's interpreted as a whole column
223 | # [:y] y coordinate(s), can contain arrays and ranges, when specified
224 | # without x coordinate, it's interpreted as a whole row
225 | def no_response_to(types = nil, state = :both, opts = nil)
226 | logger.debug "removing response to #{types.inspect} for state #{state.inspect}"
227 | types = Array(types)
228 | Array(state == :both ? %w(down up) : state).each do |state|
229 | types.each do |type|
230 | combined_types(type, opts).each do |combined_type|
231 | responses[combined_type][state.to_sym].clear
232 | end
233 | end
234 | end
235 | nil
236 | end
237 |
238 | # Responds to an action by executing all matching responses, effectively simulating
239 | # a button press/release.
240 | #
241 | # Parameters (see Launchpad for values):
242 | #
243 | # [+type+] type of the button to trigger
244 | # [+state+] state of the button
245 | #
246 | # Optional options hash (see Launchpad for values):
247 | #
248 | # [:x] x coordinate
249 | # [:y] y coordinate
250 | def respond_to(type, state, opts = nil)
251 | respond_to_action((opts || {}).merge(:type => type, :state => state))
252 | end
253 |
254 | private
255 |
256 | # Returns the hash storing all responses. Keys are button types, values are
257 | # hashes themselves, keys are :down/:up, values are arrays of responses.
258 | def responses
259 | @responses ||= Hash.new {|hash, key| hash[key] = {:down => [], :up => []}}
260 | end
261 |
262 | # Returns an array of grid positions for a range.
263 | #
264 | # Parameters:
265 | #
266 | # [+range+] the range definitions, can be
267 | # * a Fixnum
268 | # * a Range
269 | # * an Array of Fixnum, Range or Array objects
270 | def grid_range(range)
271 | return nil if range.nil?
272 | Array(range).flatten.map do |pos|
273 | pos.respond_to?(:to_a) ? pos.to_a : pos
274 | end.flatten.uniq
275 | end
276 |
277 | # Returns a list of combined types for the type and opts specified. Combined
278 | # types are just the type, except for grid, where the opts are interpreted
279 | # and all combinations of x and y coordinates are added as a position suffix.
280 | #
281 | # Example:
282 | #
283 | # combined_types(:grid, :x => 1..2, y => 2) => [:grid12, :grid22]
284 | #
285 | # Parameters (see Launchpad for values):
286 | #
287 | # [+type+] type of the button
288 | #
289 | # Optional options hash:
290 | #
291 | # [:x] x coordinate(s), can contain arrays and ranges, when specified
292 | # without y coordinate, it's interpreted as a whole column
293 | # [:y] y coordinate(s), can contain arrays and ranges, when specified
294 | # without x coordinate, it's interpreted as a whole row
295 | def combined_types(type, opts = nil)
296 | if type.to_sym == :grid && opts
297 | x = grid_range(opts[:x])
298 | y = grid_range(opts[:y])
299 | return [:grid] if x.nil? && y.nil? # whole grid
300 | x ||= ['-'] # whole row
301 | y ||= ['-'] # whole column
302 | x.product(y).map {|x, y| :"grid#{x}#{y}"}
303 | else
304 | [type.to_sym]
305 | end
306 | end
307 |
308 | # Reponds to an action by executing all matching responses.
309 | #
310 | # Parameters:
311 | #
312 | # [+action+] hash containing an action from Launchpad::Device.read_pending_actions
313 | def respond_to_action(action)
314 | type = action[:type].to_sym
315 | state = action[:state].to_sym
316 | actions = []
317 | if type == :grid
318 | actions += responses[:"grid#{action[:x]}#{action[:y]}"][state]
319 | actions += responses[:"grid#{action[:x]}-"][state]
320 | actions += responses[:"grid-#{action[:y]}"][state]
321 | end
322 | actions += responses[type][state]
323 | actions += responses[:all][state]
324 | actions.compact.each {|block| block.call(self, action)}
325 | nil
326 | rescue Exception => e
327 | logger.error "error when responding to action #{action.inspect}: #{e.inspect}"
328 | raise e
329 | end
330 |
331 | end
332 |
333 | end
--------------------------------------------------------------------------------
/lib/launchpad/logging.rb:
--------------------------------------------------------------------------------
1 | require 'logger'
2 |
3 | module Launchpad
4 |
5 | # This module provides logging facilities. Just include it to be able to log
6 | # stuff.
7 | module Logging
8 |
9 | # Returns the logger to be used by the current instance.
10 | #
11 | # Returns:
12 | #
13 | # the logger set externally or a logger that swallows everything
14 | def logger
15 | @logger ||= Logger.new(nil)
16 | end
17 |
18 | # Sets the logger to be used by the current instance.
19 | #
20 | # [+logger+] the [Logger] instance
21 | def logger=(logger)
22 | @logger = logger
23 | end
24 |
25 | end
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/lib/launchpad/midi_codes.rb:
--------------------------------------------------------------------------------
1 | module Launchpad
2 |
3 | # Module defining constants for MIDI codes.
4 | module MidiCodes
5 |
6 | # Module defining MIDI status codes.
7 | module Status
8 | NIL = 0x00
9 | OFF = 0x80
10 | ON = 0x90
11 | MULTI = 0x92
12 | CC = 0xB0
13 | end
14 |
15 | # Module defininig MIDI data 1 (note) codes for control buttons.
16 | module ControlButton
17 | UP = 0x68
18 | DOWN = 0x69
19 | LEFT = 0x6A
20 | RIGHT = 0x6B
21 | SESSION = 0x6C
22 | USER1 = 0x6D
23 | USER2 = 0x6E
24 | MIXER = 0x6F
25 | end
26 |
27 | # Module defininig MIDI data 1 (note) codes for scene buttons.
28 | module SceneButton
29 | SCENE1 = 0x08
30 | SCENE2 = 0x18
31 | SCENE3 = 0x28
32 | SCENE4 = 0x38
33 | SCENE5 = 0x48
34 | SCENE6 = 0x58
35 | SCENE7 = 0x68
36 | SCENE8 = 0x78
37 | end
38 |
39 | # Module defining MIDI data 2 (velocity) codes.
40 | module Velocity
41 | TEST_LEDS = 0x7C
42 | end
43 |
44 | # Module defining MIDI data 2 codes for selecting the grid layout.
45 | module GridLayout
46 | XY = 0x01
47 | DRUM_RACK = 0x02
48 | end
49 |
50 | end
51 |
52 | end
53 |
--------------------------------------------------------------------------------
/lib/launchpad/version.rb:
--------------------------------------------------------------------------------
1 | module Launchpad
2 | VERSION = '0.3.0'
3 | end
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/spec'
2 | require 'minitest/autorun'
3 |
4 | begin
5 | require 'minitest/reporters'
6 | MiniTest::Reporters.use!
7 | rescue LoadError
8 | # ignore when it's not there - must be ruby 1.8
9 | end
10 |
11 | require 'mocha/setup'
12 |
13 | require 'launchpad'
14 |
15 | # mock Portmidi for tests
16 | module Portmidi
17 |
18 | class Input
19 | attr_accessor :device_id
20 | def initialize(device_id)
21 | self.device_id = device_id
22 | end
23 | def read(*args); nil; end
24 | def close; nil; end
25 | end
26 |
27 | class Output
28 | attr_accessor :device_id
29 | def initialize(device_id)
30 | self.device_id = device_id
31 | end
32 | def write(*args); nil; end
33 | def close; nil; end
34 | end
35 |
36 | def self.input_devices; mock_devices; end
37 | def self.output_devices; mock_devices; end
38 | def self.start; end
39 |
40 | end
41 |
42 | def mock_devices(opts = {})
43 | [Portmidi::Device.new(opts[:id] || 1, 0, 0, opts[:name] || 'Launchpad')]
44 | end
45 |
--------------------------------------------------------------------------------
/test/test_device.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 |
3 | describe Launchpad::Device do
4 |
5 | CONTROL_BUTTONS = {
6 | :up => 0x68,
7 | :down => 0x69,
8 | :left => 0x6A,
9 | :right => 0x6B,
10 | :session => 0x6C,
11 | :user1 => 0x6D,
12 | :user2 => 0x6E,
13 | :mixer => 0x6F
14 | }
15 | SCENE_BUTTONS = {
16 | :scene1 => 0x08,
17 | :scene2 => 0x18,
18 | :scene3 => 0x28,
19 | :scene4 => 0x38,
20 | :scene5 => 0x48,
21 | :scene6 => 0x58,
22 | :scene7 => 0x68,
23 | :scene8 => 0x78
24 | }
25 | COLORS = {
26 | nil => 0, 0 => 0, :off => 0,
27 | 1 => 1, :lo => 1, :low => 1,
28 | 2 => 2, :med => 2, :medium => 2,
29 | 3 => 3, :hi => 3, :high => 3
30 | }
31 | STATES = {
32 | :down => 127,
33 | :up => 0
34 | }
35 |
36 | def expects_output(device, *args)
37 | args = [args] unless args.first.is_a?(Array)
38 | messages = args.collect {|data| {:message => data, :timestamp => 0}}
39 | device.instance_variable_get('@output').expects(:write).with(messages)
40 | end
41 |
42 | def stub_input(device, *args)
43 | device.instance_variable_get('@input').stubs(:read).returns(args)
44 | end
45 |
46 | describe '#initialize' do
47 |
48 | it 'tries to initialize both input and output when not specified' do
49 | Portmidi.expects(:input_devices).returns(mock_devices)
50 | Portmidi.expects(:output_devices).returns(mock_devices)
51 | d = Launchpad::Device.new
52 | refute_nil d.instance_variable_get('@input')
53 | refute_nil d.instance_variable_get('@output')
54 | end
55 |
56 | it 'does not try to initialize input when set to false' do
57 | Portmidi.expects(:input_devices).never
58 | d = Launchpad::Device.new(:input => false)
59 | assert_nil d.instance_variable_get('@input')
60 | refute_nil d.instance_variable_get('@output')
61 | end
62 |
63 | it 'does not try to initialize output when set to false' do
64 | Portmidi.expects(:output_devices).never
65 | d = Launchpad::Device.new(:output => false)
66 | refute_nil d.instance_variable_get('@input')
67 | assert_nil d.instance_variable_get('@output')
68 | end
69 |
70 | it 'does not try to initialize any of both when set to false' do
71 | Portmidi.expects(:input_devices).never
72 | Portmidi.expects(:output_devices).never
73 | d = Launchpad::Device.new(:input => false, :output => false)
74 | assert_nil d.instance_variable_get('@input')
75 | assert_nil d.instance_variable_get('@output')
76 | end
77 |
78 | it 'initializes the correct input output devices when specified by name' do
79 | Portmidi.stubs(:input_devices).returns(mock_devices(:id => 4, :name => 'Launchpad Name'))
80 | Portmidi.stubs(:output_devices).returns(mock_devices(:id => 5, :name => 'Launchpad Name'))
81 | d = Launchpad::Device.new(:device_name => 'Launchpad Name')
82 | assert_equal Portmidi::Input, (input = d.instance_variable_get('@input')).class
83 | assert_equal 4, input.device_id
84 | assert_equal Portmidi::Output, (output = d.instance_variable_get('@output')).class
85 | assert_equal 5, output.device_id
86 | end
87 |
88 | it 'initializes the correct input output devices when specified by id' do
89 | Portmidi.stubs(:input_devices).returns(mock_devices(:id => 4))
90 | Portmidi.stubs(:output_devices).returns(mock_devices(:id => 5))
91 | d = Launchpad::Device.new(:input_device_id => 4, :output_device_id => 5, :device_name => 'nonexistant')
92 | assert_equal Portmidi::Input, (input = d.instance_variable_get('@input')).class
93 | assert_equal 4, input.device_id
94 | assert_equal Portmidi::Output, (output = d.instance_variable_get('@output')).class
95 | assert_equal 5, output.device_id
96 | end
97 |
98 | it 'raises NoSuchDeviceError when requested input device does not exist' do
99 | assert_raises Launchpad::NoSuchDeviceError do
100 | Portmidi.stubs(:input_devices).returns(mock_devices(:name => 'Launchpad Input'))
101 | Launchpad::Device.new
102 | end
103 | end
104 |
105 | it 'raises NoSuchDeviceError when requested output device does not exist' do
106 | assert_raises Launchpad::NoSuchDeviceError do
107 | Portmidi.stubs(:output_devices).returns(mock_devices(:name => 'Launchpad Output'))
108 | Launchpad::Device.new
109 | end
110 | end
111 |
112 | it 'raises DeviceBusyError when requested input device is busy' do
113 | assert_raises Launchpad::DeviceBusyError do
114 | Portmidi::Input.stubs(:new).raises(RuntimeError)
115 | Launchpad::Device.new
116 | end
117 | end
118 |
119 | it 'raises DeviceBusyError when requested output device is busy' do
120 | assert_raises Launchpad::DeviceBusyError do
121 | Portmidi::Output.stubs(:new).raises(RuntimeError)
122 | Launchpad::Device.new
123 | end
124 | end
125 |
126 | it 'stores the logger given' do
127 | logger = Logger.new(nil)
128 | device = Launchpad::Device.new(:logger => logger)
129 | assert_same logger, device.logger
130 | end
131 |
132 | end
133 |
134 | describe '#close' do
135 |
136 | it 'does not fail when neither input nor output are there' do
137 | Launchpad::Device.new(:input => false, :output => false).close
138 | end
139 |
140 | describe 'with input and output devices' do
141 |
142 | before do
143 | Portmidi::Input.stubs(:new).returns(@input = mock('input'))
144 | Portmidi::Output.stubs(:new).returns(@output = mock('output', :write => nil))
145 | @device = Launchpad::Device.new
146 | end
147 |
148 | it 'closes input/output and raise NoInputAllowedError/NoOutputAllowedError on subsequent read/write accesses' do
149 | @input.expects(:close)
150 | @output.expects(:close)
151 | @device.close
152 | assert_raises Launchpad::NoInputAllowedError do
153 | @device.read_pending_actions
154 | end
155 | assert_raises Launchpad::NoOutputAllowedError do
156 | @device.change(:session)
157 | end
158 | end
159 |
160 | end
161 |
162 | end
163 |
164 | describe '#closed?' do
165 |
166 | it 'returns true when neither input nor output are there' do
167 | assert Launchpad::Device.new(:input => false, :output => false).closed?
168 | end
169 |
170 | it 'returns false when initialized with input' do
171 | assert !Launchpad::Device.new(:input => true, :output => false).closed?
172 | end
173 |
174 | it 'returns false when initialized with output' do
175 | assert !Launchpad::Device.new(:input => false, :output => true).closed?
176 | end
177 |
178 | it 'returns false when initialized with both but true after calling close' do
179 | d = Launchpad::Device.new
180 | assert !d.closed?
181 | d.close
182 | assert d.closed?
183 | end
184 |
185 | end
186 |
187 | {
188 | :reset => [0xB0, 0x00, 0x00],
189 | :flashing_on => [0xB0, 0x00, 0x20],
190 | :flashing_off => [0xB0, 0x00, 0x21],
191 | :flashing_auto => [0xB0, 0x00, 0x28]
192 | }.each do |method, codes|
193 | describe "##{method}" do
194 |
195 | it 'raises NoOutputAllowedError when not initialized with output' do
196 | assert_raises Launchpad::NoOutputAllowedError do
197 | Launchpad::Device.new(:output => false).send(method)
198 | end
199 | end
200 |
201 | it "sends #{codes.inspect}" do
202 | d = Launchpad::Device.new
203 | expects_output(d, *codes)
204 | d.send(method)
205 | end
206 |
207 | end
208 | end
209 |
210 | describe '#test_leds' do
211 |
212 | it 'raises NoOutputAllowedError when not initialized with output' do
213 | assert_raises Launchpad::NoOutputAllowedError do
214 | Launchpad::Device.new(:output => false).test_leds
215 | end
216 | end
217 |
218 | describe 'initialized with output' do
219 |
220 | before do
221 | @device = Launchpad::Device.new(:input => false)
222 | end
223 |
224 | it 'returns nil' do
225 | assert_nil @device.test_leds
226 | end
227 |
228 | COLORS.merge(nil => 3).each do |name, value|
229 | if value == 0
230 | it "sends 0xB0, 0x00, 0x00 when given #{name}" do
231 | expects_output(@device, 0xB0, 0x00, 0x00)
232 | @device.test_leds(value)
233 | end
234 | else
235 | it "sends 0xB0, 0x00, 0x7C + #{value} when given #{name}" do
236 | d = Launchpad::Device.new
237 | expects_output(@device, 0xB0, 0x00, 0x7C + value)
238 | value.nil? ? @device.test_leds : @device.test_leds(value)
239 | end
240 | end
241 | end
242 |
243 | end
244 |
245 | end
246 |
247 | describe '#change' do
248 |
249 | it 'raises NoOutputAllowedError when not initialized with output' do
250 | assert_raises Launchpad::NoOutputAllowedError do
251 | Launchpad::Device.new(:output => false).change(:up)
252 | end
253 | end
254 |
255 | describe 'initialized with output' do
256 |
257 | before do
258 | @device = Launchpad::Device.new(:input => false)
259 | end
260 |
261 | it 'returns nil' do
262 | assert_nil @device.change(:up)
263 | end
264 |
265 | describe 'control buttons' do
266 | CONTROL_BUTTONS.each do |type, value|
267 | it "sends 0xB0, #{value}, 12 when given #{type}" do
268 | expects_output(@device, 0xB0, value, 12)
269 | @device.change(type)
270 | end
271 | end
272 | end
273 |
274 | describe 'scene buttons' do
275 | SCENE_BUTTONS.each do |type, value|
276 | it "sends 0x90, #{value}, 12 when given #{type}" do
277 | expects_output(@device, 0x90, value, 12)
278 | @device.change(type)
279 | end
280 | end
281 | end
282 |
283 | describe 'grid buttons' do
284 | 8.times do |x|
285 | 8.times do |y|
286 | it "sends 0x90, #{16 * y + x}, 12 when given :grid, :x => #{x}, :y => #{y}" do
287 | expects_output(@device, 0x90, 16 * y + x, 12)
288 | @device.change(:grid, :x => x, :y => y)
289 | end
290 | end
291 | end
292 |
293 | it 'raises NoValidGridCoordinatesError if x is not specified' do
294 | assert_raises Launchpad::NoValidGridCoordinatesError do
295 | @device.change(:grid, :y => 1)
296 | end
297 | end
298 |
299 | it 'raises NoValidGridCoordinatesError if x is below 0' do
300 | assert_raises Launchpad::NoValidGridCoordinatesError do
301 | @device.change(:grid, :x => -1, :y => 1)
302 | end
303 | end
304 |
305 | it 'raises NoValidGridCoordinatesError if x is above 7' do
306 | assert_raises Launchpad::NoValidGridCoordinatesError do
307 | @device.change(:grid, :x => 8, :y => 1)
308 | end
309 | end
310 |
311 | it 'raises NoValidGridCoordinatesError if y is not specified' do
312 | assert_raises Launchpad::NoValidGridCoordinatesError do
313 | @device.change(:grid, :x => 1)
314 | end
315 | end
316 |
317 | it 'raises NoValidGridCoordinatesError if y is below 0' do
318 | assert_raises Launchpad::NoValidGridCoordinatesError do
319 | @device.change(:grid, :x => 1, :y => -1)
320 | end
321 | end
322 |
323 | it 'raises NoValidGridCoordinatesError if y is above 7' do
324 | assert_raises Launchpad::NoValidGridCoordinatesError do
325 | @device.change(:grid, :x => 1, :y => 8)
326 | end
327 | end
328 |
329 | end
330 |
331 | describe 'colors' do
332 | COLORS.each do |red_key, red_value|
333 | COLORS.each do |green_key, green_value|
334 | it "sends 0x90, 0, #{16 * green_value + red_value + 12} when given :red => #{red_key}, :green => #{green_key}" do
335 | expects_output(@device, 0x90, 0, 16 * green_value + red_value + 12)
336 | @device.change(:grid, :x => 0, :y => 0, :red => red_key, :green => green_key)
337 | end
338 | end
339 | end
340 |
341 | it 'raises NoValidBrightnessError if red is below 0' do
342 | assert_raises Launchpad::NoValidBrightnessError do
343 | @device.change(:grid, :x => 0, :y => 0, :red => -1)
344 | end
345 | end
346 |
347 | it 'raises NoValidBrightnessError if red is above 3' do
348 | assert_raises Launchpad::NoValidBrightnessError do
349 | @device.change(:grid, :x => 0, :y => 0, :red => 4)
350 | end
351 | end
352 |
353 | it 'raises NoValidBrightnessError if red is an unknown symbol' do
354 | assert_raises Launchpad::NoValidBrightnessError do
355 | @device.change(:grid, :x => 0, :y => 0, :red => :unknown)
356 | end
357 | end
358 |
359 | it 'raises NoValidBrightnessError if green is below 0' do
360 | assert_raises Launchpad::NoValidBrightnessError do
361 | @device.change(:grid, :x => 0, :y => 0, :green => -1)
362 | end
363 | end
364 |
365 | it 'raises NoValidBrightnessError if green is above 3' do
366 | assert_raises Launchpad::NoValidBrightnessError do
367 | @device.change(:grid, :x => 0, :y => 0, :green => 4)
368 | end
369 | end
370 |
371 | it 'raises NoValidBrightnessError if green is an unknown symbol' do
372 | assert_raises Launchpad::NoValidBrightnessError do
373 | @device.change(:grid, :x => 0, :y => 0, :green => :unknown)
374 | end
375 | end
376 |
377 | end
378 |
379 | describe 'mode' do
380 |
381 | it 'sends color + 12 when nothing given' do
382 | expects_output(@device, 0x90, 0, 12)
383 | @device.change(:grid, :x => 0, :y => 0, :red => 0, :green => 0)
384 | end
385 |
386 | it 'sends color + 12 when given :normal' do
387 | expects_output(@device, 0x90, 0, 12)
388 | @device.change(:grid, :x => 0, :y => 0, :red => 0, :green => 0, :mode => :normal)
389 | end
390 |
391 | it 'sends color + 8 when given :flashing' do
392 | expects_output(@device, 0x90, 0, 8)
393 | @device.change(:grid, :x => 0, :y => 0, :red => 0, :green => 0, :mode => :flashing)
394 | end
395 |
396 | it 'sends color when given :buffering' do
397 | expects_output(@device, 0x90, 0, 0)
398 | @device.change(:grid, :x => 0, :y => 0, :red => 0, :green => 0, :mode => :buffering)
399 | end
400 |
401 | end
402 |
403 | end
404 |
405 | end
406 |
407 | describe '#change_all' do
408 |
409 | it 'raises NoOutputAllowedError when not initialized with output' do
410 | assert_raises Launchpad::NoOutputAllowedError do
411 | Launchpad::Device.new(:output => false).change_all
412 | end
413 | end
414 |
415 | describe 'initialized with output' do
416 |
417 | before do
418 | @device = Launchpad::Device.new(:input => false)
419 | end
420 |
421 | it 'returns nil' do
422 | assert_nil @device.change_all([0])
423 | end
424 |
425 | it 'fills colors with 0, set grid layout to XY and flush colors' do
426 | expects_output(@device, 0xB0, 0, 0x01)
427 | expects_output(@device, *([[0x92, 17, 17]] * 20 + [[0x92, 12, 12]] * 20))
428 | @device.change_all([5] * 40)
429 | end
430 |
431 | it 'cuts off exceeding colors, set grid layout to XY and flush colors' do
432 | expects_output(@device, 0xB0, 0, 0x01)
433 | expects_output(@device, *([[0x92, 17, 17]] * 40))
434 | @device.change_all([5] * 100)
435 | end
436 |
437 | end
438 |
439 | end
440 |
441 | describe '#buffering_mode' do
442 |
443 | it 'raises NoOutputAllowedError when not initialized with output' do
444 | assert_raises Launchpad::NoOutputAllowedError do
445 | Launchpad::Device.new(:output => false).buffering_mode
446 | end
447 | end
448 |
449 | {
450 | nil => [0xB0, 0x00, 0x20],
451 | {} => [0xB0, 0x00, 0x20],
452 | {:display_buffer => 1} => [0xB0, 0x00, 0x21],
453 | {:update_buffer => 1} => [0xB0, 0x00, 0x24],
454 | {:copy => true} => [0xB0, 0x00, 0x30],
455 | {:flashing => true} => [0xB0, 0x00, 0x28],
456 | {
457 | :display_buffer => 1,
458 | :update_buffer => 1,
459 | :copy => true,
460 | :flashing => true
461 | } => [0xB0, 0x00, 0x3D]
462 | }.each do |opts, codes|
463 | it "sends #{codes.inspect} when called with #{opts.inspect}" do
464 | d = Launchpad::Device.new
465 | expects_output(d, *codes)
466 | d.buffering_mode(opts)
467 | end
468 | end
469 |
470 | end
471 |
472 | describe '#read_pending_actions' do
473 |
474 | it 'raises NoInputAllowedError when not initialized with input' do
475 | assert_raises Launchpad::NoInputAllowedError do
476 | Launchpad::Device.new(:input => false).read_pending_actions
477 | end
478 | end
479 |
480 | describe 'initialized with input' do
481 |
482 | before do
483 | @device = Launchpad::Device.new(:output => false)
484 | end
485 |
486 | describe 'control buttons' do
487 | CONTROL_BUTTONS.each do |type, value|
488 | STATES.each do |state, velocity|
489 | it "builds proper action for control button #{type}, #{state}" do
490 | stub_input(@device, {:timestamp => 0, :message => [0xB0, value, velocity]})
491 | assert_equal [{:timestamp => 0, :state => state, :type => type}], @device.read_pending_actions
492 | end
493 | end
494 | end
495 | end
496 |
497 | describe 'scene buttons' do
498 | SCENE_BUTTONS.each do |type, value|
499 | STATES.each do |state, velocity|
500 | it "builds proper action for scene button #{type}, #{state}" do
501 | stub_input(@device, {:timestamp => 0, :message => [0x90, value, velocity]})
502 | assert_equal [{:timestamp => 0, :state => state, :type => type}], @device.read_pending_actions
503 | end
504 | end
505 | end
506 | end
507 |
508 | describe '#grid buttons' do
509 | 8.times do |x|
510 | 8.times do |y|
511 | STATES.each do |state, velocity|
512 | it "builds proper action for grid button #{x},#{y}, #{state}" do
513 | stub_input(@device, {:timestamp => 0, :message => [0x90, 16 * y + x, velocity]})
514 | assert_equal [{:timestamp => 0, :state => state, :type => :grid, :x => x, :y => y}], @device.read_pending_actions
515 | end
516 | end
517 | end
518 | end
519 | end
520 |
521 | it 'builds proper actions for multiple pending actions' do
522 | stub_input(@device, {:timestamp => 1, :message => [0x90, 0, 127]}, {:timestamp => 2, :message => [0xB0, 0x68, 0]})
523 | assert_equal [{:timestamp => 1, :state => :down, :type => :grid, :x => 0, :y => 0}, {:timestamp => 2, :state => :up, :type => :up}], @device.read_pending_actions
524 | end
525 |
526 | end
527 |
528 | end
529 |
530 | end
531 |
--------------------------------------------------------------------------------
/test/test_interaction.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'timeout'
3 |
4 | class BreakError < StandardError; end
5 |
6 | describe Launchpad::Interaction do
7 |
8 | # returns true/false whether the operation ended or the timeout was hit
9 | def timeout(timeout = 0.02, &block)
10 | Timeout.timeout(timeout, &block)
11 | true
12 | rescue Timeout::Error
13 | false
14 | end
15 |
16 | def press(interaction, type, opts = nil)
17 | interaction.respond_to(type, :down, opts)
18 | interaction.respond_to(type, :up, opts)
19 | end
20 |
21 |
22 | def press_all(interaction)
23 | %w(up down left right session user1 user2 mixer).each do |type|
24 | press(interaction, type.to_sym)
25 | end
26 | 8.times do |y|
27 | 8.times do |x|
28 | press(interaction, :grid, :x => x, :y => y)
29 | end
30 | press(interaction, :"scene#{y + 1}")
31 | end
32 | end
33 |
34 | describe '#initialize' do
35 |
36 | it 'creates device if not given' do
37 | device = Launchpad::Device.new
38 | Launchpad::Device.expects(:new).
39 | with(:input => true, :output => true, :logger => nil).
40 | returns(device)
41 | interaction = Launchpad::Interaction.new
42 | assert_same device, interaction.device
43 | end
44 |
45 | it 'creates device with given device_name' do
46 | device = Launchpad::Device.new
47 | Launchpad::Device.expects(:new).
48 | with(:device_name => 'device', :input => true, :output => true, :logger => nil).
49 | returns(device)
50 | interaction = Launchpad::Interaction.new(:device_name => 'device')
51 | assert_same device, interaction.device
52 | end
53 |
54 | it 'creates device with given input_device_id' do
55 | device = Launchpad::Device.new
56 | Launchpad::Device.expects(:new).
57 | with(:input_device_id => 'in', :input => true, :output => true, :logger => nil).
58 | returns(device)
59 | interaction = Launchpad::Interaction.new(:input_device_id => 'in')
60 | assert_same device, interaction.device
61 | end
62 |
63 | it 'creates device with given output_device_id' do
64 | device = Launchpad::Device.new
65 | Launchpad::Device.expects(:new).
66 | with(:output_device_id => 'out', :input => true, :output => true, :logger => nil).
67 | returns(device)
68 | interaction = Launchpad::Interaction.new(:output_device_id => 'out')
69 | assert_same device, interaction.device
70 | end
71 |
72 | it 'creates device with given input_device_id/output_device_id' do
73 | device = Launchpad::Device.new
74 | Launchpad::Device.expects(:new).
75 | with(:input_device_id => 'in', :output_device_id => 'out', :input => true, :output => true, :logger => nil).
76 | returns(device)
77 | interaction = Launchpad::Interaction.new(:input_device_id => 'in', :output_device_id => 'out')
78 | assert_same device, interaction.device
79 | end
80 |
81 | it 'initializes device if given' do
82 | device = Launchpad::Device.new
83 | interaction = Launchpad::Interaction.new(:device => device)
84 | assert_same device, interaction.device
85 | end
86 |
87 | it 'stores the logger given' do
88 | logger = Logger.new(nil)
89 | interaction = Launchpad::Interaction.new(:logger => logger)
90 | assert_same logger, interaction.logger
91 | assert_same logger, interaction.device.logger
92 | end
93 |
94 | it 'doesn\'t activate the interaction' do
95 | assert !Launchpad::Interaction.new.active
96 | end
97 |
98 | end
99 |
100 | describe '#logger=' do
101 |
102 | it 'stores the logger and passes it to the device as well' do
103 | logger = Logger.new(nil)
104 | interaction = Launchpad::Interaction.new
105 | interaction.logger = logger
106 | assert_same logger, interaction.logger
107 | assert_same logger, interaction.device.logger
108 | end
109 |
110 | end
111 |
112 | describe '#close' do
113 |
114 | it 'stops the interaction' do
115 | interaction = Launchpad::Interaction.new
116 | interaction.expects(:stop)
117 | interaction.close
118 | end
119 |
120 | it 'closes the device' do
121 | interaction = Launchpad::Interaction.new
122 | interaction.device.expects(:close)
123 | interaction.close
124 | end
125 |
126 | end
127 |
128 | describe '#closed?' do
129 |
130 | it 'returns false on a newly created interaction, but true after closing' do
131 | interaction = Launchpad::Interaction.new
132 | assert !interaction.closed?
133 | interaction.close
134 | assert interaction.closed?
135 | end
136 |
137 | end
138 |
139 | describe '#start' do
140 |
141 | before do
142 | @interaction = Launchpad::Interaction.new
143 | end
144 |
145 | after do
146 | mocha_teardown # so that expectations on Thread.join don't fail in here
147 | begin
148 | @interaction.close
149 | rescue
150 | # ignore, should be handled in tests, this is just to close all the spawned threads
151 | end
152 | end
153 |
154 | it 'sets active to true in blocking mode' do
155 | refute @interaction.active
156 | erg = timeout { @interaction.start }
157 | refute erg, 'there was no timeout'
158 | assert @interaction.active
159 | end
160 |
161 | it 'sets active to true in detached mode' do
162 | refute @interaction.active
163 | @interaction.start(:detached => true)
164 | assert @interaction.active
165 | end
166 |
167 | it 'blocks in blocking mode' do
168 | erg = timeout { @interaction.start }
169 | refute erg, 'there was no timeout'
170 | end
171 |
172 | it 'returns immediately in detached mode' do
173 | erg = timeout { @interaction.start(:detached => true) }
174 | assert erg, 'there was a timeout'
175 | end
176 |
177 | it 'raises CommunicationError when Portmidi::DeviceError occurs' do
178 | @interaction.device.stubs(:read_pending_actions).raises(Portmidi::DeviceError.new(0))
179 | assert_raises Launchpad::CommunicationError do
180 | @interaction.start
181 | end
182 | end
183 |
184 | describe 'action handling' do
185 |
186 | before do
187 | @interaction.response_to(:mixer, :down) { @mixer_down = true }
188 | @interaction.response_to(:mixer, :up) do |i,a|
189 | sleep 0.001 # sleep to make "sure" :mixer :down has been processed
190 | i.stop
191 | end
192 | @interaction.device.expects(:read_pending_actions).
193 | at_least_once.
194 | returns([
195 | {
196 | :timestamp => 0,
197 | :state => :down,
198 | :type => :mixer
199 | },
200 | {
201 | :timestamp => 0,
202 | :state => :up,
203 | :type => :mixer
204 | }
205 | ])
206 | end
207 |
208 | it 'calls respond_to_action with actions from respond_to_action in blocking mode' do
209 | erg = timeout(0.5) { @interaction.start }
210 | assert erg, 'the actions weren\'t called'
211 | assert @mixer_down, 'the mixer button wasn\'t pressed'
212 | end
213 |
214 | it 'calls respond_to_action with actions from respond_to_action in detached mode' do
215 | @interaction.start(:detached => true)
216 | erg = timeout(0.5) { while @interaction.active; sleep 0.01; end }
217 | assert erg, 'there was a timeout'
218 | assert @mixer_down, 'the mixer button wasn\'t pressed'
219 | end
220 |
221 | end
222 |
223 | describe 'latency' do
224 |
225 | before do
226 | @device = @interaction.device
227 | @times = []
228 | @device.instance_variable_set("@test_interaction_latency_times", @times)
229 | def @device.read_pending_actions
230 | @test_interaction_latency_times << Time.now.to_f
231 | []
232 | end
233 | end
234 |
235 | it 'sleeps with default latency of 0.001s when none given' do
236 | timeout { @interaction.start }
237 | assert @times.size > 1
238 | @times.each_cons(2) do |a,b|
239 | assert_in_delta 0.001, b - a, 0.01
240 | end
241 | end
242 |
243 | it 'sleeps with given latency' do
244 | @interaction = Launchpad::Interaction.new(:latency => 0.5, :device => @device)
245 | timeout(0.55) { @interaction.start }
246 | assert @times.size > 1
247 | @times.each_cons(2) do |a,b|
248 | assert_in_delta 0.5, b - a, 0.01
249 | end
250 | end
251 |
252 | it 'sleeps with absolute value of given negative latency' do
253 | @interaction = Launchpad::Interaction.new(:latency => -0.1, :device => @device)
254 | timeout(0.15) { @interaction.start }
255 | assert @times.size > 1
256 | @times.each_cons(2) do |a,b|
257 | assert_in_delta 0.1, b - a, 0.01
258 | end
259 | end
260 |
261 | it 'does not sleep when latency is 0' do
262 | @interaction = Launchpad::Interaction.new(:latency => 0, :device => @device)
263 | timeout(0.001) { @interaction.start }
264 | assert @times.size > 1
265 | @times.each_cons(2) do |a,b|
266 | assert_in_delta 0, b - a, 0.1
267 | end
268 | end
269 |
270 | end
271 |
272 | it 'resets the device after the loop' do
273 | @interaction.device.expects(:reset)
274 | @interaction.start(:detached => true)
275 | @interaction.stop
276 | end
277 |
278 | it 'raises NoOutputAllowedError on closed interaction' do
279 | @interaction.close
280 | assert_raises Launchpad::NoOutputAllowedError do
281 | @interaction.start
282 | end
283 | end
284 |
285 | end
286 |
287 | describe '#stop' do
288 |
289 | before do
290 | @interaction = Launchpad::Interaction.new
291 | end
292 |
293 | it 'sets active to false in blocking mode' do
294 | erg = timeout { @interaction.start }
295 | refute erg, 'there was no timeout'
296 | assert @interaction.active
297 | @interaction.stop
298 | assert !@interaction.active
299 | end
300 |
301 | it 'sets active to false in detached mode' do
302 | @interaction.start(:detached => true)
303 | assert @interaction.active
304 | @interaction.stop
305 | assert !@interaction.active
306 | end
307 |
308 | it 'is callable anytime' do
309 | @interaction.stop
310 | @interaction.start(:detached => true)
311 | @interaction.stop
312 | @interaction.stop
313 | end
314 |
315 | # this is kinda greybox tested, since I couldn't come up with another way to test tread handling [thomas, 2010-01-24]
316 | it 'raises pending exceptions in detached mode' do
317 | t = Thread.new {raise BreakError}
318 | Thread.expects(:new).returns(t)
319 | @interaction.start(:detached => true)
320 | assert_raises BreakError do
321 | @interaction.stop
322 | end
323 | end
324 |
325 | end
326 |
327 | describe '#response_to/#no_response_to/#respond_to' do
328 |
329 | before do
330 | @interaction = Launchpad::Interaction.new
331 | end
332 |
333 | it 'calls all responses that match, and not others' do
334 | @interaction.response_to(:mixer, :down) {|i, a| @mixer_down = true}
335 | @interaction.response_to(:all, :down) {|i, a| @all_down = true}
336 | @interaction.response_to(:all, :up) {|i, a| @all_up = true}
337 | @interaction.response_to(:grid, :down) {|i, a| @grid_down = true}
338 | @interaction.respond_to(:mixer, :down)
339 | assert @mixer_down
340 | assert @all_down
341 | assert !@all_up
342 | assert !@grid_down
343 | end
344 |
345 | it 'does not call responses when they are deregistered' do
346 | @interaction.response_to(:mixer, :down) {|i, a| @mixer_down = true}
347 | @interaction.response_to(:mixer, :up) {|i, a| @mixer_up = true}
348 | @interaction.response_to(:all, :both) {|i, a| @all_down = a[:state] == :down}
349 | @interaction.no_response_to(:mixer, :down)
350 | @interaction.respond_to(:mixer, :down)
351 | assert !@mixer_down
352 | assert !@mixer_up
353 | assert @all_down
354 | @interaction.respond_to(:mixer, :up)
355 | assert !@mixer_down
356 | assert @mixer_up
357 | assert !@all_down
358 | end
359 |
360 | it 'does not call responses registered for both when removing for one of both states' do
361 | @interaction.response_to(:mixer, :both) {|i, a| @mixer = true}
362 | @interaction.no_response_to(:mixer, :down)
363 | @interaction.respond_to(:mixer, :down)
364 | assert !@mixer
365 | @interaction.respond_to(:mixer, :up)
366 | assert @mixer
367 | end
368 |
369 | it 'removes other responses when adding a new exclusive response' do
370 | @interaction.response_to(:mixer, :both) {|i, a| @mixer = true}
371 | @interaction.response_to(:mixer, :down, :exclusive => true) {|i, a| @exclusive_mixer = true}
372 | @interaction.respond_to(:mixer, :down)
373 | assert !@mixer
374 | assert @exclusive_mixer
375 | @interaction.respond_to(:mixer, :up)
376 | assert @mixer
377 | assert @exclusive_mixer
378 | end
379 |
380 | it 'allows for multiple types' do
381 | @downs = []
382 | @interaction.response_to([:up, :down], :down) {|i, a| @downs << a[:type]}
383 | @interaction.respond_to(:up, :down)
384 | @interaction.respond_to(:down, :down)
385 | @interaction.respond_to(:up, :down)
386 | assert_equal [:up, :down, :up], @downs
387 | end
388 |
389 | describe 'allows to bind to specific grid buttons' do
390 |
391 | before do
392 | @downs = []
393 | @action = lambda {|i, a| @downs << [a[:x], a[:y]]}
394 | end
395 |
396 | it 'one specific grid button' do
397 | @interaction.response_to(:grid, :down, :x => 4, :y => 2, &@action)
398 | press_all @interaction
399 | assert_equal [[4, 2]], @downs
400 | end
401 |
402 | it 'a complete row of grid buttons' do
403 | @interaction.response_to(:grid, :down, :y => 2, &@action)
404 | press_all @interaction
405 | assert_equal [[0, 2], [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2]], @downs
406 | end
407 |
408 | it 'a complete column of grid buttons' do
409 | @interaction.response_to(:grid, :down, :x => 3, &@action)
410 | press_all @interaction
411 | assert_equal [[3, 0], [3, 1], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6], [3, 7]], @downs
412 | end
413 |
414 | it 'a complex range of grid buttons' do
415 | @interaction.response_to(:grid, :down, :x => [1,[2]], :y => [1, 3..5], &@action)
416 | press_all @interaction
417 | assert_equal [[1, 1], [2, 1], [1, 3], [2, 3], [1, 4], [2, 4], [1, 5], [2, 5]], @downs
418 | end
419 |
420 | it 'a specific grid buttons, a column, a row, all grid buttons and all buttons' do
421 | @interaction.response_to(:all, :down) {|i, a| @downs << [a[:x], a[:y], :all]}
422 | @interaction.response_to(:grid, :down) {|i, a| @downs << [a[:x], a[:y], :grid]}
423 | @interaction.response_to(:grid, :down, :x => 0) {|i, a| @downs << [a[:x], a[:y], :col]}
424 | @interaction.response_to(:grid, :down, :y => 0) {|i, a| @downs << [a[:x], a[:y], :row]}
425 | @interaction.response_to(:grid, :down, :x => 0, :y => 0, &@action)
426 | press @interaction, :grid, :x => 0, :y => 0
427 | assert_equal [[0, 0], [0, 0, :col], [0, 0, :row], [0, 0, :grid], [0, 0, :all]], @downs
428 | end
429 |
430 | end
431 |
432 | end
433 |
434 | describe 'regression tests' do
435 |
436 | it 'does not raise an exception or write an error to the logger when calling stop within a response in attached mode' do
437 | log = StringIO.new
438 | logger = Logger.new(log)
439 | logger.level = Logger::ERROR
440 | i = Launchpad::Interaction.new(:logger => logger)
441 | i.response_to(:mixer, :down) {|i,a| i.stop}
442 | i.device.expects(:read_pending_actions).
443 | at_least_once.
444 | returns([{
445 | :timestamp => 0,
446 | :state => :down,
447 | :type => :mixer
448 | }])
449 | erg = timeout { i.start }
450 | # assert erg, 'the actions weren\'t called'
451 | assert_equal '', log.string
452 | end
453 |
454 | end
455 |
456 | end
457 |
--------------------------------------------------------------------------------