surface_master 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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