surface_master 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +8 -0
- data/Gemfile +13 -0
- data/LICENSE +21 -0
- data/README.md +48 -0
- data/Rakefile +10 -0
- data/debug_tools/Numark_Orbit_Pad_Coloring.mmon +98 -0
- data/debug_tools/OrbitLightingExample.json +662 -0
- data/debug_tools/Orbit_Color_Test.json +662 -0
- data/debug_tools/Orbit_Colors_And_Reset.1.raw +0 -0
- data/debug_tools/Orbit_Colors_And_Reset.1.txt +82 -0
- data/debug_tools/Orbit_Colors_And_Reset.2.raw +0 -0
- data/debug_tools/Orbit_Colors_And_Reset.2.txt +2 -0
- data/debug_tools/Orbit_Colors_And_Reset.3.raw +0 -0
- data/debug_tools/Orbit_Colors_And_Reset.3.txt +82 -0
- data/debug_tools/Orbit_Colors_And_Reset.mmon +93 -0
- data/debug_tools/Orbit_Preset.1.raw +0 -0
- data/debug_tools/Orbit_Preset.1.txt +82 -0
- data/debug_tools/Orbit_Preset.2.raw +0 -0
- data/debug_tools/Orbit_Preset.2.txt +2 -0
- data/debug_tools/Orbit_Preset.mmon +72 -0
- data/debug_tools/compare.sh +12 -0
- data/debug_tools/decode.rb +14 -0
- data/debug_tools/extract_midi_monitor_sample.sh +33 -0
- data/docs/Numark_Orbit_QuickRef.md +50 -0
- data/examples/launchpad_testbed.rb +141 -0
- data/examples/monitor.rb +61 -0
- data/examples/orbit_testbed.rb +62 -0
- data/lib/control_center.rb +26 -0
- data/lib/surface_master/device.rb +90 -0
- data/lib/surface_master/errors.rb +27 -0
- data/lib/surface_master/interaction.rb +133 -0
- data/lib/surface_master/launchpad/device.rb +159 -0
- data/lib/surface_master/launchpad/errors.rb +11 -0
- data/lib/surface_master/launchpad/interaction.rb +86 -0
- data/lib/surface_master/launchpad/midi_codes.rb +51 -0
- data/lib/surface_master/logging.rb +15 -0
- data/lib/surface_master/orbit/device.rb +160 -0
- data/lib/surface_master/orbit/interaction.rb +29 -0
- data/lib/surface_master/orbit/midi_codes.rb +31 -0
- data/lib/surface_master/version.rb +3 -0
- data/mappings/Orbit_Preset.json +662 -0
- data/surface_master.gemspec +26 -0
- data/test/helper.rb +44 -0
- data/test/test_device.rb +530 -0
- data/test/test_interaction.rb +456 -0
- metadata +121 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module SurfaceMaster
|
2
|
+
# Unclassified error.
|
3
|
+
class GenericError < StandardError; end
|
4
|
+
|
5
|
+
# Error raised when the MIDI device specified doesn't exist.
|
6
|
+
class NoSuchDeviceError < GenericError; end
|
7
|
+
|
8
|
+
# Error raised when the MIDI device specified is busy.
|
9
|
+
class DeviceBusyError < GenericError; end
|
10
|
+
|
11
|
+
# Error raised when an input has been requested, although device has been initialized without
|
12
|
+
# input.
|
13
|
+
class NoInputAllowedError < GenericError; end
|
14
|
+
|
15
|
+
# Error raised when an output has been requested, although device has been initialized without
|
16
|
+
# output.
|
17
|
+
class NoOutputAllowedError < GenericError; end
|
18
|
+
|
19
|
+
# Error raised when anything fails while communicating with a device.
|
20
|
+
class CommunicationError < GenericError
|
21
|
+
attr_accessor :source
|
22
|
+
def initialize(e)
|
23
|
+
super(e.portmidi_error)
|
24
|
+
self.source = e
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module SurfaceMaster
|
2
|
+
# Base class for event-based drivers. Sub-classes should extend the constructor, and implement `respond_to_action`
|
3
|
+
class Interaction
|
4
|
+
include Logging
|
5
|
+
|
6
|
+
attr_reader :device, :active
|
7
|
+
|
8
|
+
def initialize(opts = nil)
|
9
|
+
opts ||= {}
|
10
|
+
|
11
|
+
self.logger = opts[:logger]
|
12
|
+
logger.debug "Initializing #{self.class}##{object_id} with #{opts.inspect}"
|
13
|
+
|
14
|
+
@use_threads = opts[:use_threads] || true
|
15
|
+
@device = opts[:device]
|
16
|
+
@device ||= @device_class.new(opts.merge(input: true,
|
17
|
+
output: true,
|
18
|
+
logger: opts[:logger]))
|
19
|
+
@latency = (opts[:latency] || 0.001).to_f.abs
|
20
|
+
@active = false
|
21
|
+
|
22
|
+
@action_threads = ThreadGroup.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def change(opts); @device.change(opts); end
|
26
|
+
def changes(opts); @device.changes(opts); end
|
27
|
+
|
28
|
+
def close
|
29
|
+
logger.debug "Closing #{self.class}##{object_id}"
|
30
|
+
stop
|
31
|
+
@device.close
|
32
|
+
end
|
33
|
+
|
34
|
+
def closed?; @device.closed?; end
|
35
|
+
|
36
|
+
def start(opts = nil)
|
37
|
+
logger.debug "Starting #{self.class}##{object_id}"
|
38
|
+
|
39
|
+
opts = { detached: false }.merge(opts || {})
|
40
|
+
|
41
|
+
@active = true
|
42
|
+
|
43
|
+
@reader_thread ||= Thread.new do
|
44
|
+
begin
|
45
|
+
while @active do
|
46
|
+
@device.read.each do |action|
|
47
|
+
if @use_threads
|
48
|
+
action_thread = Thread.new(action) do |act|
|
49
|
+
respond_to_action(act)
|
50
|
+
end
|
51
|
+
@action_threads.add(action_thread)
|
52
|
+
else
|
53
|
+
respond_to_action(action)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
sleep @latency# if @latency > 0.0
|
57
|
+
end
|
58
|
+
rescue Portmidi::DeviceError => e
|
59
|
+
logger.fatal "Could not read from device, stopping reader!"
|
60
|
+
raise SurfaceMaster::CommunicationError.new(e)
|
61
|
+
rescue Exception => e
|
62
|
+
logger.fatal "Unkown error, stopping reader: #{e.inspect}"
|
63
|
+
raise e
|
64
|
+
ensure
|
65
|
+
@device.reset!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
@reader_thread.join unless opts[:detached]
|
69
|
+
end
|
70
|
+
|
71
|
+
def stop
|
72
|
+
logger.debug "Stopping #{self.class}##{object_id}"
|
73
|
+
@active = false
|
74
|
+
if @reader_thread
|
75
|
+
# run (resume from sleep) and wait for @reader_thread to end
|
76
|
+
@reader_thread.run if @reader_thread.alive?
|
77
|
+
@reader_thread.join
|
78
|
+
@reader_thread = nil
|
79
|
+
end
|
80
|
+
ensure
|
81
|
+
@action_threads.list.each do |thread|
|
82
|
+
begin
|
83
|
+
thread.kill
|
84
|
+
thread.join
|
85
|
+
rescue StandardException => e # TODO: RuntimeError, Exception, or this?
|
86
|
+
logger.error "Error when killing action thread: #{e.inspect}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def response_to(types = :all, state = :both, opts = nil, &block)
|
93
|
+
logger.debug "Setting response to #{types.inspect} for state #{state.inspect} with #{opts.inspect}"
|
94
|
+
types = Array(types)
|
95
|
+
opts ||= {}
|
96
|
+
no_response_to(types, state) if opts[:exclusive] == true
|
97
|
+
Array(state == :both ? %i(down up) : state).each do |st|
|
98
|
+
types.each do |type|
|
99
|
+
combined_types(type, opts).each do |combined_type|
|
100
|
+
responses[combined_type][st] << block
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
|
107
|
+
def no_response_to(types = nil, state = :both, opts = nil)
|
108
|
+
logger.debug "Removing response to #{types.inspect} for state #{state.inspect}"
|
109
|
+
types = Array(types)
|
110
|
+
Array(state == :both ? %i(down up) : state).each do |st|
|
111
|
+
types.each do |type|
|
112
|
+
combined_types(type, opts).each do |combined_type|
|
113
|
+
responses[combined_type][st].clear
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def respond_to(type, state, opts = nil)
|
121
|
+
respond_to_action((opts || {}).merge(type: type, state: state))
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
def responses
|
127
|
+
# TODO: Generalize for arbitrary actions...
|
128
|
+
@responses ||= Hash.new { |hash, key| hash[key] = { down: [], up: [] } }
|
129
|
+
end
|
130
|
+
|
131
|
+
def respond_to_action(action); end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module SurfaceMaster
|
2
|
+
module Launchpad
|
3
|
+
class Device < SurfaceMaster::Device
|
4
|
+
include MIDICodes
|
5
|
+
|
6
|
+
# TODO: Rename scenes to match Mk2
|
7
|
+
CODE_NOTE_TO_TYPE = Hash.new { |*_| :grid }
|
8
|
+
.merge([Status::ON, Scene::SCENE1] => :scene1,
|
9
|
+
[Status::ON, Scene::SCENE2] => :scene2,
|
10
|
+
[Status::ON, Scene::SCENE3] => :scene3,
|
11
|
+
[Status::ON, Scene::SCENE4] => :scene4,
|
12
|
+
[Status::ON, Scene::SCENE5] => :scene5,
|
13
|
+
[Status::ON, Scene::SCENE6] => :scene6,
|
14
|
+
[Status::ON, Scene::SCENE7] => :scene7,
|
15
|
+
[Status::ON, Scene::SCENE8] => :scene8,
|
16
|
+
[Status::CC, Control::UP] => :up,
|
17
|
+
[Status::CC, Control::DOWN] => :down,
|
18
|
+
[Status::CC, Control::LEFT] => :left,
|
19
|
+
[Status::CC, Control::RIGHT] => :right,
|
20
|
+
[Status::CC, Control::SESSION] => :session,
|
21
|
+
[Status::CC, Control::USER1] => :user1,
|
22
|
+
[Status::CC, Control::USER2] => :user2,
|
23
|
+
[Status::CC, Control::MIXER] => :mixer)
|
24
|
+
.freeze
|
25
|
+
TYPE_TO_NOTE = { up: Control::UP,
|
26
|
+
down: Control::DOWN,
|
27
|
+
left: Control::LEFT,
|
28
|
+
right: Control::RIGHT,
|
29
|
+
session: Control::SESSION,
|
30
|
+
user1: Control::USER1,
|
31
|
+
user2: Control::USER2,
|
32
|
+
mixer: Control::MIXER,
|
33
|
+
scene1: Scene::SCENE1, # Volume
|
34
|
+
scene2: Scene::SCENE2, # Pan
|
35
|
+
scene3: Scene::SCENE3, # Send A
|
36
|
+
scene4: Scene::SCENE4, # Send B
|
37
|
+
scene5: Scene::SCENE5, # Stop
|
38
|
+
scene6: Scene::SCENE6, # Mute
|
39
|
+
scene7: Scene::SCENE7, # Solo
|
40
|
+
scene8: Scene::SCENE8 }.freeze # Record Arm
|
41
|
+
|
42
|
+
def initialize(opts = nil)
|
43
|
+
@name = "Launchpad MK2"
|
44
|
+
super(opts)
|
45
|
+
reset! if output_enabled?
|
46
|
+
end
|
47
|
+
|
48
|
+
def reset
|
49
|
+
# TODO: Suss out what this should be for the Mark 2.
|
50
|
+
layout!(0x00)
|
51
|
+
output!(Status::CC, Status::NIL, Status::NIL)
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO: Support more of the LaunchPad Mark 2's functionality.
|
55
|
+
|
56
|
+
def change(opts = nil)
|
57
|
+
opts ||= {}
|
58
|
+
command, payload = color_payload(opts)
|
59
|
+
sysex!(command, payload[:led], payload[:color])
|
60
|
+
end
|
61
|
+
|
62
|
+
def changes(values)
|
63
|
+
msg_by_command = {}
|
64
|
+
values.each do |value|
|
65
|
+
command, payload = color_payload(value)
|
66
|
+
(msg_by_command[command] ||= []) << payload
|
67
|
+
end
|
68
|
+
msg_by_command.each do |command, payloads|
|
69
|
+
# The documented batch size for RGB LED updates is 80. The docs lie, at least on my current
|
70
|
+
# firmware version -- anything above 62 crashes the device hard.
|
71
|
+
while (slice = payloads.shift(62)).length > 0
|
72
|
+
sysex!(command, *slice.map { |payload| [payload[:led], payload[:color]] })
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def read
|
78
|
+
super.collect do |input|
|
79
|
+
note = input[:note]
|
80
|
+
input[:type] = CODE_NOTE_TO_TYPE[[input[:code], note]] || :grid
|
81
|
+
if input[:type] == :grid
|
82
|
+
note = note - 11
|
83
|
+
input[:x] = note % 10
|
84
|
+
input[:y] = note / 10
|
85
|
+
end
|
86
|
+
input
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
def layout!(mode); sysex!(0x22, mode); end
|
93
|
+
def sysex_prefix; @sysex_prefix ||= super + [0x00, 0x20, 0x29, 0x02, 0x18]; end
|
94
|
+
|
95
|
+
def decode_led(opts)
|
96
|
+
case
|
97
|
+
when opts[:cc]
|
98
|
+
[:cc, TYPE_TO_NOTE[opts[:cc]]]
|
99
|
+
when opts[:grid]
|
100
|
+
if opts[:grid] == :all
|
101
|
+
[:all, nil]
|
102
|
+
else
|
103
|
+
[:grid, (opts[:grid][1] * 10) + opts[:grid][0] + 11]
|
104
|
+
end
|
105
|
+
when opts[:column]
|
106
|
+
[:column, opts[:column]]
|
107
|
+
when opts[:row]
|
108
|
+
[:row, opts[:row]]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
TYPE_TO_COMMAND = { cc: 0x0B,
|
113
|
+
grid: 0x0B,
|
114
|
+
column: 0x0C,
|
115
|
+
row: 0x0D,
|
116
|
+
all: 0x0E }.freeze
|
117
|
+
def color_payload(opts)
|
118
|
+
type, led = decode_led(opts)
|
119
|
+
command = TYPE_TO_COMMAND[type]
|
120
|
+
case type
|
121
|
+
when :cc, :grid
|
122
|
+
color = [opts[:red] || 0x00, opts[:green] || 0x00, opts[:blue] || 0x00]
|
123
|
+
when :column, :row, :all
|
124
|
+
color = opts[:color] || 0x00
|
125
|
+
end
|
126
|
+
[command, { led: led, color: color }]
|
127
|
+
end
|
128
|
+
|
129
|
+
def output!(status, data1, data2)
|
130
|
+
outputs!(message(status, data1, data2))
|
131
|
+
end
|
132
|
+
|
133
|
+
def outputs!(*messages)
|
134
|
+
messages = Array(messages)
|
135
|
+
if @output.nil?
|
136
|
+
logger.error "trying to write to device that's not been initialized for output"
|
137
|
+
raise SurfaceMaster::NoOutputAllowedError
|
138
|
+
end
|
139
|
+
logger.debug "writing messages to launchpad:\n #{messages.join("\n ")}" if logger.debug?
|
140
|
+
@output.write(messages)
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def note(type, opts)
|
145
|
+
note = TYPE_TO_NOTE[type]
|
146
|
+
if note.nil?
|
147
|
+
x = (opts[:x] || -1).to_i
|
148
|
+
y = (opts[:y] || -1).to_i
|
149
|
+
if x < 0 || x > 7 || y < 0 || y > 7
|
150
|
+
logger.error "wrong coordinates specified: x=#{x}, y=#{y}"
|
151
|
+
raise NoValidGridCoordinatesError.new("you need to specify valid coordinates (x/y, 0-7, from top left), you specified: x=#{x}, y=#{y}")
|
152
|
+
end
|
153
|
+
note = y * 10 + x
|
154
|
+
end
|
155
|
+
note
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Launchpad
|
2
|
+
# Generic launchpad error.
|
3
|
+
class LaunchpadError < SurfaceMaster::GenericError; end
|
4
|
+
|
5
|
+
# Error raised when <tt>x/y</tt> coordinates outside of the grid
|
6
|
+
# or none were specified.
|
7
|
+
class NoValidGridCoordinatesError < LaunchpadError; end
|
8
|
+
|
9
|
+
# Error raised when wrong brightness was specified.
|
10
|
+
class NoValidBrightnessError < LaunchpadError; end
|
11
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module SurfaceMaster
|
2
|
+
module Launchpad
|
3
|
+
class Interaction < SurfaceMaster::Interaction
|
4
|
+
def initialize(opts = nil)
|
5
|
+
@device_class = Device
|
6
|
+
super(opts)
|
7
|
+
end
|
8
|
+
|
9
|
+
# def response_to(types = :all, state = :both, opts = nil, &block)
|
10
|
+
# logger.debug "setting response to #{types.inspect} for state #{state.inspect} with #{opts.inspect}"
|
11
|
+
# types = Array(types)
|
12
|
+
# opts ||= {}
|
13
|
+
# no_response_to(types, state) if opts[:exclusive] == true
|
14
|
+
# Array(state == :both ? %i(down up) : state).each do |st|
|
15
|
+
# types.each do |type|
|
16
|
+
# combined_types(type, opts).each do |combined_type|
|
17
|
+
# responses[combined_type][st] << block
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# nil
|
22
|
+
# end
|
23
|
+
|
24
|
+
# def no_response_to(types = nil, state = :both, opts = nil)
|
25
|
+
# logger.debug "removing response to #{types.inspect} for state #{state.inspect}"
|
26
|
+
# types = Array(types)
|
27
|
+
# Array(state == :both ? %i(down up) : state).each do |st|
|
28
|
+
# types.each do |type|
|
29
|
+
# combined_types(type, opts).each do |combined_type|
|
30
|
+
# responses[combined_type][st].clear
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# nil
|
35
|
+
# end
|
36
|
+
|
37
|
+
# def respond_to(type, state, opts = nil)
|
38
|
+
# respond_to_action((opts || {}).merge(type: type, state: state))
|
39
|
+
# end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def responses
|
44
|
+
@responses ||= Hash.new { |hash, key| hash[key] = { down: [], up: [] } }
|
45
|
+
end
|
46
|
+
|
47
|
+
def grid_range(range)
|
48
|
+
return nil if range.nil?
|
49
|
+
Array(range).flatten.map do |pos|
|
50
|
+
pos.respond_to?(:to_a) ? pos.to_a : pos
|
51
|
+
end.flatten.uniq
|
52
|
+
end
|
53
|
+
|
54
|
+
def combined_types(type, opts = nil)
|
55
|
+
if type.to_sym == :grid && opts
|
56
|
+
x = grid_range(opts[:x])
|
57
|
+
y = grid_range(opts[:y])
|
58
|
+
return [:grid] if x.nil? && y.nil? # whole grid
|
59
|
+
x ||= ['-'] # whole row
|
60
|
+
y ||= ['-'] # whole column
|
61
|
+
x.product(y).map { |xx, yy| :"grid#{xx}#{yy}" }
|
62
|
+
else
|
63
|
+
[type.to_sym]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def respond_to_action(action)
|
68
|
+
type = action[:type].to_sym
|
69
|
+
state = action[:state].to_sym
|
70
|
+
actions = []
|
71
|
+
if type == :grid
|
72
|
+
actions += responses[:"grid#{action[:x]}#{action[:y]}"][state]
|
73
|
+
actions += responses[:"grid#{action[:x]}-"][state]
|
74
|
+
actions += responses[:"grid-#{action[:y]}"][state]
|
75
|
+
end
|
76
|
+
actions += responses[type][state]
|
77
|
+
actions += responses[:all][state]
|
78
|
+
actions.compact.each {|block| block.call(self, action)}
|
79
|
+
nil
|
80
|
+
rescue Exception => e # TODO: StandardException, RuntimeError, or Exception?
|
81
|
+
logger.error "Error when responding to action #{action.inspect}: #{e.inspect}"
|
82
|
+
raise e
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module SurfaceMaster
|
2
|
+
module Launchpad
|
3
|
+
module MIDICodes
|
4
|
+
# Module defining MIDI status codes.
|
5
|
+
# TODO: Some of these are fairly generic? Can we hoist them?
|
6
|
+
module Status
|
7
|
+
NIL = 0x00
|
8
|
+
OFF = 0x80
|
9
|
+
ON = 0x90
|
10
|
+
MULTI = 0x92
|
11
|
+
CC = 0xB0
|
12
|
+
end
|
13
|
+
|
14
|
+
# Module defininig MIDI data 1 (note) codes for control buttons.
|
15
|
+
module Control
|
16
|
+
UP = 0x68
|
17
|
+
DOWN = 0x69
|
18
|
+
LEFT = 0x6A
|
19
|
+
RIGHT = 0x6B
|
20
|
+
SESSION = 0x6C
|
21
|
+
USER1 = 0x6D
|
22
|
+
USER2 = 0x6E
|
23
|
+
MIXER = 0x6F
|
24
|
+
end
|
25
|
+
|
26
|
+
# Module defininig MIDI data 1 (note) codes for scene buttons.
|
27
|
+
# TODO: Rename to match Mk2...
|
28
|
+
module Scene
|
29
|
+
SCENE1 = 0x59
|
30
|
+
SCENE2 = 0x4f
|
31
|
+
SCENE3 = 0x45
|
32
|
+
SCENE4 = 0x3b
|
33
|
+
SCENE5 = 0x31
|
34
|
+
SCENE6 = 0x27
|
35
|
+
SCENE7 = 0x1d
|
36
|
+
SCENE8 = 0x13
|
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
|
+
end
|
50
|
+
end
|
51
|
+
end
|