surface_master 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  module SurfaceMaster
2
- # Base class for event-based drivers. Sub-classes should extend the constructor, and implement `respond_to_action`
2
+ # Base class for event-based drivers. Sub-classes should extend the constructor, and implement
3
+ # `respond_to_action`, etc.
3
4
  class Interaction
4
5
  include Logging
5
6
 
@@ -12,10 +13,9 @@ module SurfaceMaster
12
13
  logger.debug "Initializing #{self.class}##{object_id} with #{opts.inspect}"
13
14
 
14
15
  @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]))
16
+ @device = opts[:device] || @device_class.new(opts.merge(input: true,
17
+ output: true,
18
+ logger: opts[:logger]))
19
19
  @latency = (opts[:latency] || 0.001).to_f.abs
20
20
  @active = false
21
21
 
@@ -36,35 +36,10 @@ module SurfaceMaster
36
36
  def start(opts = nil)
37
37
  logger.debug "Starting #{self.class}##{object_id}"
38
38
 
39
- opts = { detached: false }.merge(opts || {})
39
+ opts = { detached: false }.merge(opts || {})
40
+ @active = true
41
+ @reader_thread ||= create_reader_thread
40
42
 
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
43
  @reader_thread.join unless opts[:detached]
69
44
  end
70
45
 
@@ -78,28 +53,18 @@ module SurfaceMaster
78
53
  @reader_thread = nil
79
54
  end
80
55
  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
56
+ kill_action_threads!
89
57
  nil
90
58
  end
91
59
 
92
60
  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}"
61
+ logger.debug "Setting response to #{types.inspect} for state #{state.inspect} with"\
62
+ " #{opts.inspect}"
94
63
  types = Array(types)
95
64
  opts ||= {}
96
65
  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
66
+ expand_states(state).each do |st|
67
+ add_response_for_state!(types, opts, st, block)
103
68
  end
104
69
  nil
105
70
  end
@@ -107,12 +72,8 @@ module SurfaceMaster
107
72
  def no_response_to(types = nil, state = :both, opts = nil)
108
73
  logger.debug "Removing response to #{types.inspect} for state #{state.inspect}"
109
74
  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
75
+ expand_states(state).each do |st|
76
+ clear_responses_for_state!(types, opts, st)
116
77
  end
117
78
  nil
118
79
  end
@@ -123,11 +84,76 @@ module SurfaceMaster
123
84
 
124
85
  protected
125
86
 
87
+ def create_reader_thread
88
+ Thread.new do
89
+ guard_input_and_reset_at_end! do
90
+ while @active
91
+ @device.read.each { |action| handle_action(action) }
92
+ sleep @latency if @latency && @latency > 0.0
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def guard_input_and_reset_at_end!(&block)
99
+ block.call
100
+ rescue Portmidi::DeviceError => e
101
+ logger.fatal "Could not read from device, stopping reader!"
102
+ raise SurfaceMaster::CommunicationError, e
103
+ rescue Exception => e
104
+ logger.fatal "Unkown error, stopping reader: #{e.inspect}"
105
+ raise e
106
+ ensure
107
+ @device.reset!
108
+ end
109
+
110
+ def kill_action_threads!
111
+ @action_threads.list.each do |thread|
112
+ begin
113
+ thread.kill
114
+ thread.join
115
+ rescue StandardException => e # TODO: RuntimeError, Exception, or this?
116
+ logger.error "Error when killing action thread: #{e.inspect}"
117
+ end
118
+ end
119
+ end
120
+
121
+ def add_response_for_state!(types, opts, state, block)
122
+ response_groups_for(types, opts, state) do |responses|
123
+ responses << block
124
+ end
125
+ end
126
+
127
+ def clear_responses_for_state!(types, opts, state)
128
+ response_groups_for(types, opts, state, &:clear)
129
+ end
130
+
131
+ def response_groups_for(types, opts, state, &block)
132
+ types.each do |type|
133
+ combined_types(type, opts).each do |combined_type|
134
+ block.call(responses[combined_type][state])
135
+ end
136
+ end
137
+ end
138
+
139
+ def expand_states(state); Array(state == :both ? %i(down up) : state); end
140
+
141
+ def handle_action(action)
142
+ if @use_threads
143
+ action_thread = Thread.new(action) do |act|
144
+ respond_to_action(act)
145
+ end
146
+ @action_threads.add(action_thread)
147
+ else
148
+ respond_to_action(action)
149
+ end
150
+ end
151
+
126
152
  def responses
127
153
  # TODO: Generalize for arbitrary actions...
128
154
  @responses ||= Hash.new { |hash, key| hash[key] = { down: [], up: [] } }
129
155
  end
130
156
 
131
- def respond_to_action(action); end
157
+ def respond_to_action(_action); end
132
158
  end
133
159
  end
@@ -1,5 +1,6 @@
1
1
  module SurfaceMaster
2
2
  module Launchpad
3
+ # Low-level interface to Novation Launchpad Mark 2 control surface.
3
4
  class Device < SurfaceMaster::Device
4
5
  include MIDICodes
5
6
 
@@ -54,76 +55,101 @@ module SurfaceMaster
54
55
  # TODO: Support more of the LaunchPad Mark 2's functionality.
55
56
 
56
57
  def change(opts = nil)
58
+ fail NoOutputAllowedError unless output_enabled?
59
+
57
60
  opts ||= {}
58
61
  command, payload = color_payload(opts)
59
62
  sysex!(command, payload[:led], payload[:color])
63
+ nil
60
64
  end
61
65
 
62
66
  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.
67
+ fail NoOutputAllowedError unless output_enabled?
68
+
69
+ organize_commands(values).each do |command, payloads|
70
+ # The documented batch size for RGB LED updates is 80. The docs lie, at least on my
71
+ # current firmware version -- anything above 62 crashes the device hard.
71
72
  while (slice = payloads.shift(62)).length > 0
72
- sysex!(command, *slice.map { |payload| [payload[:led], payload[:color]] })
73
+ messages = slice.map { |payload| [payload[:led], payload[:color]] }
74
+ sysex!(command, *messages)
73
75
  end
74
76
  end
77
+ nil
75
78
  end
76
79
 
77
80
  def read
81
+ fail NoInputAllowedError unless input_enabled?
78
82
  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
83
+ note = input.delete(:note)
84
+ input[:type] = CODE_NOTE_TO_TYPE[[input.delete(:code), note]] || :grid
85
+ input[:x], input[:y] = decode_grid_coord(note) if input[:type] == :grid
86
+ input.delete(:velocity)
86
87
  input
87
88
  end
88
89
  end
89
90
 
90
91
  protected
91
92
 
93
+ def organize_commands(values)
94
+ msg_by_command = {}
95
+ values.each do |value|
96
+ command, payload = color_payload(value)
97
+ (msg_by_command[command] ||= []) << payload
98
+ end
99
+ msg_by_command
100
+ end
101
+
102
+ def decode_grid_coord(note)
103
+ note -= 11
104
+ x = note % 10
105
+ y = note / 10
106
+ [x, y]
107
+ end
108
+
92
109
  def layout!(mode); sysex!(0x22, mode); end
93
110
  def sysex_prefix; @sysex_prefix ||= super + [0x00, 0x20, 0x29, 0x02, 0x18]; end
94
111
 
95
112
  def decode_led(opts)
96
113
  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]]
114
+ when opts[:cc] then [:cc, TYPE_TO_NOTE[opts[:cc]]]
115
+ when opts[:grid] then decode_grid_led(opts)
116
+ when opts[:column] then [:column, opts[:column]]
117
+ when opts[:row] then [:row, opts[:row]]
109
118
  end
119
+ rescue
120
+ raise SurfaceMaster::Launchpad::NoValidGridCoordinatesError
110
121
  end
111
122
 
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
123
+ def decode_grid_led(opts)
124
+ if opts[:grid] == :all
125
+ [:all, nil]
126
+ else
127
+ check_xy_values!(opts[:grid])
128
+ [:grid, (opts[:grid][1] * 10) + opts[:grid][0] + 11]
125
129
  end
126
- [command, { led: led, color: color }]
130
+ end
131
+
132
+ def check_xy_values!(xy_pair)
133
+ x = xy_pair[0]
134
+ y = xy_pair[1]
135
+ return unless xy_pair.length != 2 ||
136
+ !coord_in_range?(x) ||
137
+ !coord_in_range?(y)
138
+
139
+ fail SurfaceMaster::Launchpad::NoValidGridCoordinatesError
140
+ end
141
+
142
+ def coord_in_range?(val); val && val >= 0 && val <= 7; end
143
+
144
+ def color_payload(opts)
145
+ # Hard-coded to single-LED RGB update right now.
146
+ # For paletted changes, commands available include:
147
+ # 0x0C -> Column
148
+ # 0x0D -> Row
149
+ # 0x0E -> All LEDs
150
+ [0x0B,
151
+ { led: decode_led(opts),
152
+ color: [opts[:red] || 0x00, opts[:green] || 0x00, opts[:blue] || 0x00] }]
127
153
  end
128
154
 
129
155
  def output!(status, data1, data2)
@@ -133,27 +159,13 @@ module SurfaceMaster
133
159
  def outputs!(*messages)
134
160
  messages = Array(messages)
135
161
  if @output.nil?
136
- logger.error "trying to write to device that's not been initialized for output"
137
- raise SurfaceMaster::NoOutputAllowedError
162
+ logger.error "trying to write to device not open for output"
163
+ fail SurfaceMaster::NoOutputAllowedError
138
164
  end
139
165
  logger.debug "writing messages to launchpad:\n #{messages.join("\n ")}" if logger.debug?
140
166
  @output.write(messages)
141
167
  nil
142
168
  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
169
  end
158
170
  end
159
171
  end
@@ -1,11 +1,10 @@
1
- module Launchpad
2
- # Generic launchpad error.
3
- class LaunchpadError < SurfaceMaster::GenericError; end
1
+ module SurfaceMaster
2
+ module Launchpad
3
+ # Generic launchpad error.
4
+ class LaunchpadError < SurfaceMaster::GenericError; end
4
5
 
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
6
+ # Error raised when <tt>x/y</tt> coordinates outside of the grid
7
+ # or none were specified.
8
+ class NoValidGridCoordinatesError < LaunchpadError; end
9
+ end
11
10
  end
@@ -1,43 +1,13 @@
1
1
  module SurfaceMaster
2
2
  module Launchpad
3
+ # Higher-level interface to Novation Launchpad Mark 2, providing an input
4
+ # handling loop and event-hooks for input events.
3
5
  class Interaction < SurfaceMaster::Interaction
4
6
  def initialize(opts = nil)
5
7
  @device_class = Device
6
8
  super(opts)
7
9
  end
8
10
 
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
11
  protected
42
12
 
43
13
  def responses
@@ -56,8 +26,8 @@ module SurfaceMaster
56
26
  x = grid_range(opts[:x])
57
27
  y = grid_range(opts[:y])
58
28
  return [:grid] if x.nil? && y.nil? # whole grid
59
- x ||= ['-'] # whole row
60
- y ||= ['-'] # whole column
29
+ x ||= ["-"] # whole row
30
+ y ||= ["-"] # whole column
61
31
  x.product(y).map { |xx, yy| :"grid#{xx}#{yy}" }
62
32
  else
63
33
  [type.to_sym]
@@ -65,21 +35,33 @@ module SurfaceMaster
65
35
  end
66
36
 
67
37
  def respond_to_action(action)
38
+ mappings_for_action(action).each do |block|
39
+ block.call(self, action)
40
+ end
41
+ nil
42
+ rescue Exception => e # TODO: StandardException, RuntimeError, or Exception?
43
+ logger.error "Error when responding to action #{action.inspect}: #{e.inspect}"
44
+ raise e
45
+ end
46
+
47
+ def mappings_for_action(action)
68
48
  type = action[:type].to_sym
69
49
  state = action[:state].to_sym
70
50
  actions = []
71
51
  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]
52
+ actions += mappings_for_grid_action(state, action[:x], action[:y])
75
53
  end
76
54
  actions += responses[type][state]
77
55
  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
56
+ actions.compact
57
+ end
58
+
59
+ def mappings_for_grid_action(state, x, y)
60
+ actions = []
61
+ actions += responses[:"grid#{x}#{y}"][state]
62
+ actions += responses[:"grid#{x}-"][state]
63
+ actions += responses[:"grid-#{y}"][state]
64
+ actions
83
65
  end
84
66
  end
85
67
  end
@@ -1,5 +1,6 @@
1
1
  module SurfaceMaster
2
2
  module Orbit
3
+ # Low-level interface to Numark Orbit wireless MIDI control surface.
3
4
  class Device < SurfaceMaster::Device
4
5
  include MIDICodes
5
6
 
@@ -11,91 +12,8 @@ module SurfaceMaster
11
12
 
12
13
  def reset!
13
14
  # TODO: This... doesn't appear to work. At all.
14
- mappings = [0x03, 0x01, 0x70,
15
-
16
- 0x00, 0x00, 0x00,
17
- 0x00, 0x04, 0x00,
18
- 0x00, 0x08, 0x00,
19
- 0x00, 0x0C, 0x00,
20
- 0x00, 0x01, 0x00,
21
- 0x00, 0x05, 0x00,
22
- 0x00, 0x09, 0x00,
23
- 0x00, 0x0D, 0x00,
24
- 0x00, 0x02, 0x00,
25
- 0x00, 0x06, 0x00,
26
- 0x00, 0x0A, 0x00,
27
- 0x00, 0x0E, 0x00,
28
- 0x00, 0x03, 0x00,
29
- 0x00, 0x07, 0x00,
30
- 0x00, 0x0B, 0x00,
31
- 0x00, 0x0F, 0x00,
32
- 0x01, 0x00, 0x00,
33
- 0x01, 0x04, 0x00,
34
- 0x01, 0x08, 0x00,
35
- 0x01, 0x0C, 0x00,
36
- 0x01, 0x01, 0x00,
37
- 0x01, 0x05, 0x00,
38
- 0x01, 0x09, 0x00,
39
- 0x01, 0x0D, 0x00,
40
- 0x01, 0x02, 0x00,
41
- 0x01, 0x06, 0x00,
42
- 0x01, 0x0A, 0x00,
43
- 0x01, 0x0E, 0x00,
44
- 0x01, 0x03, 0x00,
45
- 0x01, 0x07, 0x00,
46
- 0x01, 0x0B, 0x00,
47
- 0x01, 0x0F, 0x00,
48
- 0x02, 0x00, 0x00,
49
- 0x02, 0x04, 0x00,
50
- 0x02, 0x08, 0x00,
51
- 0x02, 0x0C, 0x00,
52
- 0x02, 0x01, 0x00,
53
- 0x02, 0x05, 0x00,
54
- 0x02, 0x09, 0x00,
55
- 0x02, 0x0D, 0x00,
56
- 0x02, 0x02, 0x00,
57
- 0x02, 0x06, 0x00,
58
- 0x02, 0x0A, 0x00,
59
- 0x02, 0x0E, 0x00,
60
- 0x02, 0x03, 0x00,
61
- 0x02, 0x07, 0x00,
62
- 0x02, 0x0B, 0x00,
63
- 0x02, 0x0F, 0x00,
64
- 0x03, 0x00, 0x00,
65
- 0x03, 0x04, 0x00,
66
- 0x03, 0x08, 0x00,
67
- 0x03, 0x0C, 0x00,
68
- 0x03, 0x01, 0x00,
69
- 0x03, 0x05, 0x00,
70
- 0x03, 0x09, 0x00,
71
- 0x03, 0x0D, 0x00,
72
- 0x03, 0x02, 0x00,
73
- 0x03, 0x06, 0x00,
74
- 0x03, 0x0A, 0x00,
75
- 0x03, 0x0E, 0x00,
76
- 0x03, 0x03, 0x00,
77
- 0x03, 0x07, 0x00,
78
- 0x03, 0x0B, 0x00,
79
- 0x03, 0x0F, 0x00,
80
- 0x00, 0x00, 0x01,
81
- 0x00, 0x02, 0x00,
82
- 0x03, 0x00, 0x00,
83
- 0x01, 0x01, 0x01,
84
- 0x02, 0x01, 0x03,
85
- 0x01, 0x00, 0x02,
86
- 0x01, 0x02, 0x02,
87
- 0x02, 0x03, 0x02,
88
- 0x00, 0x03, 0x01,
89
- 0x03, 0x02, 0x03,
90
- 0x03, 0x03, 0x0C,
91
- 0x00, 0x0D, 0x00,
92
- 0x0C, 0x00, 0x0D,
93
- 0x00, 0x0C, 0x00,
94
- 0x0D, 0x00, 0x0C,
95
- 0x00, 0x0D, 0x00]
96
-
97
- if (result = sysex!(*mappings)) != 0
98
- raise "Expected success (0) setting mappings, got: #{result}"
15
+ if (result = sysex!(*MAPPINGS)) != 0
16
+ fail "Expected success (0) setting mappings, got: #{result}"
99
17
  end
100
18
  sysex!(0x01, 0x00, 0x00)
101
19
  end
@@ -108,6 +26,88 @@ module SurfaceMaster
108
26
 
109
27
  protected
110
28
 
29
+ MAPPINGS = [0x03, 0x01, 0x70,
30
+ 0x00, 0x00, 0x00,
31
+ 0x00, 0x04, 0x00,
32
+ 0x00, 0x08, 0x00,
33
+ 0x00, 0x0C, 0x00,
34
+ 0x00, 0x01, 0x00,
35
+ 0x00, 0x05, 0x00,
36
+ 0x00, 0x09, 0x00,
37
+ 0x00, 0x0D, 0x00,
38
+ 0x00, 0x02, 0x00,
39
+ 0x00, 0x06, 0x00,
40
+ 0x00, 0x0A, 0x00,
41
+ 0x00, 0x0E, 0x00,
42
+ 0x00, 0x03, 0x00,
43
+ 0x00, 0x07, 0x00,
44
+ 0x00, 0x0B, 0x00,
45
+ 0x00, 0x0F, 0x00,
46
+ 0x01, 0x00, 0x00,
47
+ 0x01, 0x04, 0x00,
48
+ 0x01, 0x08, 0x00,
49
+ 0x01, 0x0C, 0x00,
50
+ 0x01, 0x01, 0x00,
51
+ 0x01, 0x05, 0x00,
52
+ 0x01, 0x09, 0x00,
53
+ 0x01, 0x0D, 0x00,
54
+ 0x01, 0x02, 0x00,
55
+ 0x01, 0x06, 0x00,
56
+ 0x01, 0x0A, 0x00,
57
+ 0x01, 0x0E, 0x00,
58
+ 0x01, 0x03, 0x00,
59
+ 0x01, 0x07, 0x00,
60
+ 0x01, 0x0B, 0x00,
61
+ 0x01, 0x0F, 0x00,
62
+ 0x02, 0x00, 0x00,
63
+ 0x02, 0x04, 0x00,
64
+ 0x02, 0x08, 0x00,
65
+ 0x02, 0x0C, 0x00,
66
+ 0x02, 0x01, 0x00,
67
+ 0x02, 0x05, 0x00,
68
+ 0x02, 0x09, 0x00,
69
+ 0x02, 0x0D, 0x00,
70
+ 0x02, 0x02, 0x00,
71
+ 0x02, 0x06, 0x00,
72
+ 0x02, 0x0A, 0x00,
73
+ 0x02, 0x0E, 0x00,
74
+ 0x02, 0x03, 0x00,
75
+ 0x02, 0x07, 0x00,
76
+ 0x02, 0x0B, 0x00,
77
+ 0x02, 0x0F, 0x00,
78
+ 0x03, 0x00, 0x00,
79
+ 0x03, 0x04, 0x00,
80
+ 0x03, 0x08, 0x00,
81
+ 0x03, 0x0C, 0x00,
82
+ 0x03, 0x01, 0x00,
83
+ 0x03, 0x05, 0x00,
84
+ 0x03, 0x09, 0x00,
85
+ 0x03, 0x0D, 0x00,
86
+ 0x03, 0x02, 0x00,
87
+ 0x03, 0x06, 0x00,
88
+ 0x03, 0x0A, 0x00,
89
+ 0x03, 0x0E, 0x00,
90
+ 0x03, 0x03, 0x00,
91
+ 0x03, 0x07, 0x00,
92
+ 0x03, 0x0B, 0x00,
93
+ 0x03, 0x0F, 0x00,
94
+ 0x00, 0x00, 0x01,
95
+ 0x00, 0x02, 0x00,
96
+ 0x03, 0x00, 0x00,
97
+ 0x01, 0x01, 0x01,
98
+ 0x02, 0x01, 0x03,
99
+ 0x01, 0x00, 0x02,
100
+ 0x01, 0x02, 0x02,
101
+ 0x02, 0x03, 0x02,
102
+ 0x00, 0x03, 0x01,
103
+ 0x03, 0x02, 0x03,
104
+ 0x03, 0x03, 0x0C,
105
+ 0x00, 0x0D, 0x00,
106
+ 0x0C, 0x00, 0x0D,
107
+ 0x00, 0x0C, 0x00,
108
+ 0x0D, 0x00, 0x0C,
109
+ 0x00, 0x0D, 0x00]
110
+
111
111
  def sysex_prefix; @sysex_prefix ||= super + [0x00, 0x01, 0x3F, 0x2B]; end
112
112
 
113
113
  def decode_shoulder(decoded, note, _velocity)
@@ -1,5 +1,6 @@
1
1
  module SurfaceMaster
2
2
  module Orbit
3
+ # Higher-level interface for Numark Orbit wireless MIDI control surface.
3
4
  class Interaction < SurfaceMaster::Interaction
4
5
  def initialize(opts = nil)
5
6
  @device_class = Device