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,14 @@
1
+ #!/usr/bin/env ruby
2
+ fname = ARGV.shift
3
+ raise "Must specify filename." unless fname
4
+ outname = fname.gsub(/\.raw/, ".txt")
5
+
6
+ bytes = File.read(fname).bytes.map { |x| "0x%02X" % x }
7
+ output = [bytes.shift(5).join(", ")] # SysEx vendor header...
8
+
9
+ output << bytes.shift(2).join(", ") # Apparent prefix...
10
+ while (row = bytes.shift(3).join(", ")) != ""
11
+ output << row
12
+ end
13
+
14
+ File.write(outname, output.join("\n") + "\n")
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ FNAME=${1:-}
6
+
7
+ if [[ $FNAME == "" ]]; then
8
+ echo "Usage: extract_midi_monitor_sample.sh <save_file.mmon>"
9
+ exit 1
10
+ fi
11
+
12
+ plutil -convert xml1 $FNAME
13
+
14
+ xpath $FNAME "//dict/data" 2>/dev/null |
15
+ grep -v -E '(^<data>)|(</data>$)' |
16
+ base64 -D |
17
+ plutil -convert xml1 - -o - > ${FNAME%.mmon}.tmp
18
+
19
+ # Not sure we're guaranteed to get the same number of elements every time,
20
+ # so the `5` below may be brittle!
21
+ xpath ${FNAME%.mmon}.tmp "//dict/array/dict[5]/data" 2>/dev/null |
22
+ grep -v -E '(^<data>)|(</data>$)' |
23
+ base64 -D > ${FNAME%.mmon}.1.raw
24
+
25
+ xpath ${FNAME%.mmon}.tmp "//dict/array/dict[9]/data" 2>/dev/null |
26
+ grep -v -E '(^<data>)|(</data>$)' |
27
+ base64 -D > ${FNAME%.mmon}.2.raw
28
+
29
+ xpath ${FNAME%.mmon}.tmp "//dict/array/dict[11]/data" 2>/dev/null |
30
+ grep -v -E '(^<data>)|(</data>$)' |
31
+ base64 -D > ${FNAME%.mmon}.3.raw
32
+
33
+ rm ${FNAME%.mmon}.tmp
@@ -0,0 +1,50 @@
1
+ # Numark Orbit Quick Reference
2
+
3
+ ## Low Battery Indicator
4
+
5
+ Any blinking `Pad Bank Selectors` indicates the battery is low.
6
+
7
+
8
+ ## Check Battery Level
9
+
10
+ Hold down `Virtual Knob Selector` `K1` for several seconds. `Pad Bank Selectors` will light up to indicate charge level.
11
+
12
+
13
+ ## Reset Lights
14
+
15
+ 1. Turn Orbit off.
16
+ 1. Press and hold `Pad Bank Selectors` `1` and `4`.
17
+ 1. Turn Orbit on.
18
+ 1. Press the upper-left-most pad.
19
+
20
+
21
+ ## Pairing
22
+
23
+ 1. Turn Orbit off, and unplug USB adapter.
24
+ 1. Press and hold `Pad Bank Selectors` `1` and `4`.
25
+ 1. Turn Orbit on.
26
+ 1. Press the lower-right-most pad.
27
+ 1. Connect the USB adapter __within 10 seconds__.
28
+ 1. Press the pad __above__ the lower-right-most pad.
29
+ 1. The `Virtual Knob Selectors` will flash once to indicate successful pairing.
30
+
31
+
32
+ ## Sleep Mode
33
+
34
+ Sleep mode will activate after 3 minutes of inactivity. Move the Orbit to wake it up.
35
+
36
+
37
+ ## Wireless Concerns.
38
+
39
+ * 100 ft maximum distance with line of sight.
40
+ * 2.4Ghz frequency spectrum (turn off wifi/etc for best results).
41
+
42
+
43
+ ## Hard Reset
44
+
45
+ 1. Turn Orbit off, and unplug USB adapter.
46
+ 1. Press and hold `Pad Bank Selectors` `1` and `4`.
47
+ 1. Turn Orbit on.
48
+ 1. Press the upper-left-most pad.
49
+ 1. Press the pad to the right of the upper-left-most pad.
50
+ 1. Turn Orbit off, and back on.
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env ruby
2
+ require "rubygems"
3
+ require "bundler/setup"
4
+ Bundler.require(:default, :development)
5
+
6
+ require "surface_master"
7
+
8
+ # Flash: F0h 00h 20h 29h 02h 18h 23h <LED> <Colour> F7h
9
+ # Pulse: F0h 00h 20h 29h 02h 18h 28h <LED> <Colour> F7h
10
+ # Set all: F0h 00h 20h 29h 02h 18h 0Eh <Colour> F7h
11
+ # Set row: F0h 00h 20h 29h 02h 18h 0Dh <Row> <Colour> F7h
12
+ # Set col: F0h 00h 20h 29h 02h 18h 0Ch <Column> <Colour> F7h
13
+
14
+ # Configuration
15
+ SCALE = 2
16
+ TIME_SCALE = 4.0
17
+
18
+ # State
19
+ QUADRANTS = [
20
+ [{ red: 0x2F, green: 0x00, blue: 0x00 }, { red: 0x00, green: 0x2F, blue: 0x00 }],
21
+ [{ red: 0x00, green: 0x00, blue: 0x2F }, { red: 0x2F, green: 0x2F, blue: 0x00 }],
22
+ ]
23
+ FLIPPED = [[false, false], [false, false]]
24
+ PRESSED = (0..7).map { |x| (0..7).map { |y| false } }
25
+ NOW = [Time.now.to_f]
26
+
27
+ # Helpers
28
+ CC = %i(up down left right session user1 user2 scene1 scene2 scene3 scene4 scene5 scene6 scene7 scene8)
29
+ GRID = (0..7).map { |x| (0..7).map { |y| { grid: [x, y] } } }.flatten
30
+ WHITE = { red: 0x3F, green: 0x3F, blue: 0x3F }
31
+
32
+ def clamp(val); (val > 0x3F) ? 0x3F : val; end
33
+
34
+ def base_color(x, y)
35
+ return nil if PRESSED[x][y]
36
+ quad_x = x / 4
37
+ quad_y = 1 - (y / 4)
38
+ quad = QUADRANTS[quad_y][quad_x]
39
+ s_t = (Math.sin(NOW[0] * TIME_SCALE) * 0.5) + 0.5
40
+ tmp = { red: 0x00 + quad[:red] + (s_t * 0x3F).round,
41
+ green: (x * SCALE) + quad[:green],
42
+ blue: (y * SCALE) + quad[:blue] }
43
+ if FLIPPED[quad_y][quad_x]
44
+ carry = tmp[:red]
45
+ tmp[:red] = tmp[:green]
46
+ tmp[:green] = tmp[:blue]
47
+ tmp[:blue] = carry
48
+ end
49
+ { red: clamp(tmp[:red]),
50
+ green: clamp(tmp[:green]),
51
+ blue: clamp(tmp[:blue]) }
52
+ end
53
+
54
+ def init_board(interaction)
55
+ values = GRID.map do |value|
56
+ tmp = base_color(*value[:grid])
57
+ next unless tmp
58
+ value.merge(tmp)
59
+ end
60
+ interaction.changes(values.compact)
61
+ end
62
+
63
+ def set_grid_rgb(interaction, red:, green:, blue: )
64
+ values = GRID.map { |value| value.merge(red: red, green: green, blue: blue) }
65
+ interaction.changes(values)
66
+ end
67
+
68
+ def goodbye(interaction)
69
+ interaction.changes([{ red: 0x00, green: 0x00, blue: 0x00, cc: :mixer },
70
+ { red: 0x00, green: 0x00, blue: 0x00, cc: :scene1 },
71
+ { red: 0x00, green: 0x00, blue: 0x00, cc: :scene2 },
72
+ { red: 0x00, green: 0x00, blue: 0x00, cc: :scene3 },
73
+ { red: 0x00, green: 0x00, blue: 0x00, cc: :scene4 }])
74
+ (0..63).step(2).each do |i|
75
+ ii = (63 - i) - 1
76
+ set_grid_rgb(interaction, red: ii, green: 0x00, blue: ii)
77
+ sleep 0.01
78
+ end
79
+ interaction.close
80
+ end
81
+
82
+ SurfaceMaster.init!
83
+ interaction = SurfaceMaster::Launchpad::Interaction.new
84
+ interaction.response_to(:grid) do |inter, action|
85
+ x = action[:x]
86
+ y = action[:y]
87
+ PRESSED[x][y] = (action[:state] == :down)
88
+ value = base_color(x, y) || WHITE
89
+ value[:grid] = [x, y]
90
+ inter.change(value)
91
+ end
92
+
93
+ def flip_quad!(inter, cc, quad_x, quad_y)
94
+ FLIPPED[quad_y][quad_x] = !FLIPPED[quad_y][quad_x]
95
+ if FLIPPED[quad_y][quad_x]
96
+ color = { red: 0x1F, green: 0x1F, blue: 0x1F }
97
+ else
98
+ color = { red: 0x03, green: 0x03, blue: 0x03 }
99
+ end
100
+ inter.change(color.merge(cc: cc))
101
+ end
102
+
103
+ interaction.response_to(:scene1, :down) do |inter, action|
104
+ flip_quad!(inter, action[:type], 0, 0)
105
+ end
106
+ interaction.response_to(:scene2, :down) do |inter, action|
107
+ flip_quad!(inter, action[:type], 1, 0)
108
+ end
109
+ interaction.response_to(:scene3, :down) do |inter, action|
110
+ flip_quad!(inter, action[:type], 0, 1)
111
+ end
112
+ interaction.response_to(:scene4, :down) do |inter, action|
113
+ flip_quad!(inter, action[:type], 1, 1)
114
+ end
115
+
116
+ interaction.response_to(:mixer, :down) do |_interaction, action|
117
+ interaction.stop
118
+ end
119
+ interaction.change({ red: 0x03, green: 0x00, blue: 0x00, cc: :mixer })
120
+ interaction.changes(%i(scene1 scene2 scene3 scene4).map { |cc| { red: 0x03, green: 0x03, blue: 0x03, cc: cc } })
121
+
122
+ init_board(interaction)
123
+ input_thread = Thread.new do
124
+ interaction.start
125
+ end
126
+ animation_thread = Thread.new do
127
+ loop do
128
+ begin
129
+ NOW[0] = Time.now.to_f
130
+ init_board(interaction)
131
+ rescue Exception => e
132
+ puts e.inspect
133
+ puts e.backtrace.join("\n")
134
+ end
135
+ sleep 0.01
136
+ end
137
+ end
138
+
139
+ input_thread.join
140
+ animation_thread.terminate
141
+ goodbye(interaction)
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ # require "bignum"
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+ Bundler.require(:default, :development)
6
+
7
+ require "surface_master"
8
+
9
+ def goodbye(interaction)
10
+ data = []
11
+ (0..7).each do |x|
12
+ (0..7).each do |y|
13
+ data << { x: x, y: y, red: 0x00, green: 0x00, blue: 0x00 }
14
+ end
15
+ end
16
+ interaction.changes(data)
17
+ end
18
+
19
+ def bar(interaction, x, val, r, g, b)
20
+ data = []
21
+ (0..val).each do |y|
22
+ data << { x: x, y: y, red: r, green: g, blue: b }
23
+ end
24
+ ((val+1)..7).each do |y|
25
+ data << { x: x, y: y, red: 0x00, green: 0x00, blue: 0x00 }
26
+ end
27
+ interaction.changes(data)
28
+ end
29
+
30
+ interaction = SurfaceMaster::Launchpad::Interaction.new
31
+ monitor = Thread.new do
32
+ loop do
33
+ fields = `iostat -c 2 disk0`.split(/\n/).last.strip.split(/\s+/)
34
+ cpu_pct = 100 - fields[-4].to_i
35
+ cpu_usage = ((cpu_pct / 100.0) * 8.0).round.to_i
36
+
37
+ disk_pct = (fields[2].to_f / 750.0) * 100.0
38
+ disk_usage = ((disk_pct / 100.0) * 8.0).round.to_i
39
+
40
+ puts "I/O=#{disk_pct}%, CPU=#{cpu_pct}%"
41
+
42
+ # TODO: Network in/out...
43
+
44
+ # TODO: Make block I/O not be a bar but a fill, with scale indicated by color...
45
+
46
+ bar(interaction, 0, cpu_usage, 0x3F, 0x00, 0x00)
47
+ bar(interaction, 1, disk_usage, 0x00, 0x3F, 0x00)
48
+ end
49
+ end
50
+
51
+ interaction.response_to(:mixer, :down) do |_interaction, action|
52
+ puts "Shutting down"
53
+ begin
54
+ monitor.kill
55
+ goodbye(interaction)
56
+ interaction.stop
57
+ rescue Exception => e
58
+ puts e.inspect
59
+ end
60
+ end
61
+ interaction.start
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # require "bignum"
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+ Bundler.require(:default, :development)
6
+
7
+ require "surface_master"
8
+
9
+ class Fixnum
10
+ def to_hex; "%02X" % self; end
11
+ end
12
+
13
+ # def debug(msg)
14
+ # STDERR.puts "DEBUG: #{msg}"
15
+ # end
16
+
17
+ # def fmt_message(message)
18
+ # message[:raw][:message].map(&:to_hex).join(' ')
19
+ # end
20
+
21
+ SurfaceMaster.init!
22
+ device = SurfaceMaster::Orbit::Device.new
23
+ loop do
24
+ device.read.each do |input|
25
+ puts input.inspect
26
+ end
27
+ sleep 0.1
28
+ end
29
+
30
+ # interaction = ControlCenter::Launchpad::Interaction.new
31
+ # interaction.response_to(:grid) do |inter, action|
32
+ # x = action[:x]
33
+ # y = action[:y]
34
+ # PRESSED[x][y] = (action[:state] == :down)
35
+ # value = base_color(x, y) || WHITE
36
+ # value[:grid] = [x, y]
37
+ # inter.device.change(value)
38
+ # end
39
+
40
+ # interaction.device.change({ red: 0x03, green: 0x00, blue: 0x00, cc: :mixer })
41
+ # interaction.device.changes(%i(scene1 scene2 scene3 scene4).map { |cc| { red: 0x03, green: 0x03, blue: 0x03, cc: cc } })
42
+
43
+ # init_board(interaction)
44
+ # input_thread = Thread.new do
45
+ # interaction.start
46
+ # end
47
+ # animation_thread = Thread.new do
48
+ # loop do
49
+ # begin
50
+ # NOW[0] = Time.now.to_f
51
+ # init_board(interaction)
52
+ # rescue Exception => e
53
+ # puts e.inspect
54
+ # puts e.backtrace.join("\n")
55
+ # end
56
+ # sleep 0.01
57
+ # end
58
+ # end
59
+
60
+ # input_thread.join
61
+ # animation_thread.terminate
62
+ # goodbye(interaction)
@@ -0,0 +1,26 @@
1
+ require "portmidi"
2
+ require "logger"
3
+
4
+ # APIs to enable access to various MIDI-based control surfaces.
5
+ module SurfaceMaster
6
+ def self.init!
7
+ @initialized ||= begin
8
+ Portmidi.start
9
+ true
10
+ end
11
+ end
12
+ end
13
+
14
+ require "surface_master/version"
15
+ require "surface_master/errors"
16
+ require "surface_master/logging"
17
+ require "surface_master/device"
18
+ require "surface_master/interaction"
19
+
20
+ require "surface_master/launchpad/errors"
21
+ require "surface_master/launchpad/midi_codes"
22
+ require "surface_master/launchpad/device"
23
+ require "surface_master/launchpad/interaction"
24
+
25
+ require "surface_master/orbit/midi_codes"
26
+ require "surface_master/orbit/device"
@@ -0,0 +1,90 @@
1
+ module SurfaceMaster
2
+ # Base class for MIDI controller drivers.
3
+ #
4
+ # Sub-classes should extend the constructor, extend `sysex_prefix`, implement `reset!`, and add
5
+ # whatever methods are appropriate for them.
6
+ class Device
7
+ include Logging
8
+
9
+ def initialize(opts = nil)
10
+ opts = { input: true,
11
+ output: true }
12
+ .merge(opts || {})
13
+
14
+ self.logger = opts[:logger]
15
+ logger.debug "Initializing #{self.class}##{object_id} with #{opts.inspect}"
16
+
17
+ @input = create_device!(Portmidi.input_devices,
18
+ Portmidi::Input,
19
+ id: opts[:input_device_id],
20
+ name: opts[:device_name]) if opts[:input]
21
+ @output = create_device!(Portmidi.output_devices,
22
+ Portmidi::Output,
23
+ id: opts[:output_device_id],
24
+ name: opts[:device_name]) if opts[:output]
25
+ end
26
+
27
+ # Closes the device - nothing can be done with the device afterwards.
28
+ def close
29
+ logger.debug "Closing #{self.class}##{object_id}"
30
+ @input.close unless @input.nil?
31
+ @input = nil
32
+ @output.close unless @output.nil?
33
+ @output = nil
34
+ end
35
+
36
+ def closed?; !(input_enabled? || output_enabled?); end
37
+ def input_enabled?; !@input.nil?; end
38
+ def output_enabled?; !@output.nil?; end
39
+
40
+ def reset!; end
41
+
42
+ def read
43
+ unless input_enabled?
44
+ logger.error "Trying to read from device that's not been initialized for input!"
45
+ raise SurfaceMaster::NoInputAllowedError
46
+ end
47
+
48
+ Array(@input.read(16)).collect do |midi_message|
49
+ (code, note, velocity) = midi_message[:message]
50
+ { timestamp: midi_message[:timestamp],
51
+ state: (velocity == 127) ? :down : :up,
52
+ velocity: velocity,
53
+ code: code,
54
+ note: note }
55
+ end
56
+ end
57
+
58
+ protected
59
+
60
+ def sysex_prefix; [0xF0]; end
61
+ def sysex_suffix; 0xF7; end
62
+ def sysex_msg(*payload); (sysex_prefix + [payload, sysex_suffix]).flatten.compact; end
63
+ def sysex!(*payload)
64
+ msg = sysex_msg(payload)
65
+ logger.debug { "#{msg.length}: 0x#{msg.map(&:to_hex).join(", 0x")}" }
66
+ @output.write_sysex(msg)
67
+ end
68
+
69
+ def create_device!(devices, device_type, opts)
70
+ logger.debug "Creating #{device_type} with #{opts.inspect}, choosing from portmidi devices: #{devices.inspect}"
71
+ id = opts[:id]
72
+ if id.nil?
73
+ name = opts[:name] || @name
74
+ device = devices.select { |dev| dev.name == name }.first
75
+ id = device.device_id unless device.nil?
76
+ end
77
+ if id.nil?
78
+ message = "MIDI Device `#{opts[:id] || opts[:name]}` doesn't exist!"
79
+ logger.fatal message
80
+ raise SurfaceMaster::NoSuchDeviceError.new(message)
81
+ end
82
+ device_type.new(id)
83
+ rescue RuntimeError => e # TODO: Uh, this should be StandardException, perhaps?
84
+ logger.fatal "Error creating #{device_type}: #{e.inspect}"
85
+ raise SurfaceMaster::DeviceBusyError.new(e)
86
+ end
87
+
88
+ def message(status, data1, data2); { message: [status, data1, data2], timestamp: 0 }; end
89
+ end
90
+ end