surface_master 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +13 -0
  5. data/LICENSE +21 -0
  6. data/README.md +48 -0
  7. data/Rakefile +10 -0
  8. data/debug_tools/Numark_Orbit_Pad_Coloring.mmon +98 -0
  9. data/debug_tools/OrbitLightingExample.json +662 -0
  10. data/debug_tools/Orbit_Color_Test.json +662 -0
  11. data/debug_tools/Orbit_Colors_And_Reset.1.raw +0 -0
  12. data/debug_tools/Orbit_Colors_And_Reset.1.txt +82 -0
  13. data/debug_tools/Orbit_Colors_And_Reset.2.raw +0 -0
  14. data/debug_tools/Orbit_Colors_And_Reset.2.txt +2 -0
  15. data/debug_tools/Orbit_Colors_And_Reset.3.raw +0 -0
  16. data/debug_tools/Orbit_Colors_And_Reset.3.txt +82 -0
  17. data/debug_tools/Orbit_Colors_And_Reset.mmon +93 -0
  18. data/debug_tools/Orbit_Preset.1.raw +0 -0
  19. data/debug_tools/Orbit_Preset.1.txt +82 -0
  20. data/debug_tools/Orbit_Preset.2.raw +0 -0
  21. data/debug_tools/Orbit_Preset.2.txt +2 -0
  22. data/debug_tools/Orbit_Preset.mmon +72 -0
  23. data/debug_tools/compare.sh +12 -0
  24. data/debug_tools/decode.rb +14 -0
  25. data/debug_tools/extract_midi_monitor_sample.sh +33 -0
  26. data/docs/Numark_Orbit_QuickRef.md +50 -0
  27. data/examples/launchpad_testbed.rb +141 -0
  28. data/examples/monitor.rb +61 -0
  29. data/examples/orbit_testbed.rb +62 -0
  30. data/lib/control_center.rb +26 -0
  31. data/lib/surface_master/device.rb +90 -0
  32. data/lib/surface_master/errors.rb +27 -0
  33. data/lib/surface_master/interaction.rb +133 -0
  34. data/lib/surface_master/launchpad/device.rb +159 -0
  35. data/lib/surface_master/launchpad/errors.rb +11 -0
  36. data/lib/surface_master/launchpad/interaction.rb +86 -0
  37. data/lib/surface_master/launchpad/midi_codes.rb +51 -0
  38. data/lib/surface_master/logging.rb +15 -0
  39. data/lib/surface_master/orbit/device.rb +160 -0
  40. data/lib/surface_master/orbit/interaction.rb +29 -0
  41. data/lib/surface_master/orbit/midi_codes.rb +31 -0
  42. data/lib/surface_master/version.rb +3 -0
  43. data/mappings/Orbit_Preset.json +662 -0
  44. data/surface_master.gemspec +26 -0
  45. data/test/helper.rb +44 -0
  46. data/test/test_device.rb +530 -0
  47. data/test/test_interaction.rb +456 -0
  48. 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