libatem 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bc36e2d5444044ba6b674c2cb93c3779497b59cb
4
+ data.tar.gz: 7d977b1f3cee724021e8c38c0a3b82df6cbf3bfc
5
+ SHA512:
6
+ metadata.gz: 73624d5e7736f99017e16d2d843f2dbeaeb8a77346ebde9d3880498e5c2d0d51e5a5277d33d915f89192530a9a7b95c693a678f3dd6123c3076cf00cd1793d14
7
+ data.tar.gz: 1c2e7bb4cdacba2e24cb2827465f02f84205915a80018b82362610a960b37cbec2b64f028c2c1d660153f5906e49ca060883405ef919a37115f3992fe2b11239
@@ -0,0 +1,39 @@
1
+ # <img src="https://raw.githubusercontent.com/InsanityRadio/OnAirController/master/doc/headphones_dark.png" align="left" height=48 /> libatem
2
+
3
+ libatem is a Ruby library that can control ATEM switchers.
4
+
5
+ # Basic usage
6
+
7
+ ```
8
+ switcher = ATEM.connect("10.32.0.199")
9
+
10
+ switcher.inputs
11
+
12
+ => []
13
+
14
+ switcher.inputs[1]
15
+ switcher.inputs['Camera 1']
16
+
17
+ switcher.inputs[0].preview
18
+ switcher.inputs[1].program
19
+
20
+
21
+ ```
22
+
23
+ ## Audio!
24
+
25
+ ```
26
+
27
+ # Just a warning: this generates quite a bit of traffic from the ATEM
28
+ switcher.use_audio_levels = true
29
+
30
+ switcher.inputs['Camera 1'].audio.level
31
+
32
+ ```
33
+
34
+ # TODO:
35
+
36
+ - Add more features/command support!
37
+ - Smoke & unit test, improve stability
38
+ - Audio
39
+ - Create Gem
@@ -0,0 +1,18 @@
1
+ require_relative 'atem/network'
2
+ require_relative 'atem/switcher'
3
+ require_relative 'atem/switcher/input'
4
+ require_relative 'atem/switcher/input_collection'
5
+ require_relative 'atem/switcher/input/audio'
6
+
7
+ module ATEM
8
+
9
+ def self.connect ip, port = 9910
10
+
11
+ s = Switcher.new({ :ip => ip, :port => port, :uid => 0x1337 })
12
+ s.connect
13
+
14
+ s
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,186 @@
1
+ require 'socket'
2
+
3
+ class String
4
+
5
+ def to_hex
6
+ self.bytes.map{|a|"0x" + a.to_s(16)}.join(" ")
7
+ end
8
+
9
+ end
10
+
11
+
12
+ module ATEM
13
+
14
+ class Network
15
+
16
+ module Packet
17
+ NOP = 0x00
18
+ ACK_REQ = 0x01
19
+ HELLO = 0x02
20
+ RESEND = 0x04
21
+ UNDEFINED = 0x08
22
+ ACK = 0x10
23
+ end
24
+
25
+ class Retry < StandardError
26
+ end
27
+
28
+ @@SIZE_OF_HEADER = 0x0c
29
+
30
+ @package_id = 0
31
+
32
+ def initialize ip, port, uid = 0x1337
33
+
34
+ @socket = UDPSocket.new
35
+ @socket.bind "0.0.0.0", 9100
36
+
37
+ @ip = ip
38
+ @port = port
39
+ @uid = uid
40
+ @package_id = 0
41
+
42
+ end
43
+
44
+ def disconnect
45
+ @socket.close
46
+ end
47
+
48
+ def << data
49
+
50
+ bitmask, ack_id, payload = data
51
+
52
+ bitmask = bitmask << 11
53
+ bitmask |= (payload.length + @@SIZE_OF_HEADER)
54
+
55
+ package_id = 0
56
+ if (bitmask & (Packet::HELLO | Packet::ACK)) != 0 and @ready and payload.length != 0
57
+ # p "SENDING PACKAGE"
58
+ @package_id += 1
59
+ package_id = @package_id
60
+ end
61
+
62
+ packet = [bitmask, @uid, ack_id, 0, package_id].pack("S>S>S>L>S>")
63
+ packet += payload
64
+
65
+ # print "TX(#{packet.length}, #{@package_id}, #{ack_id})"; p packet.to_hex
66
+ @socket.send packet, 0, @ip, @port
67
+
68
+ end
69
+
70
+ def send! cmd, payload
71
+
72
+ raise "Invalid command" if cmd.bytes.length != 4
73
+
74
+ size = cmd.length + payload.length + 4
75
+ datagram = [size, 0, 0].pack("S>CC") + cmd + payload
76
+
77
+ self << [Packet::ACK_REQ, @ack_id, datagram]
78
+ self.receive
79
+
80
+ end
81
+
82
+ def hello
83
+
84
+ self << [0x02, 0x0, [0x01000000, 0x00].pack("L>L>")]
85
+ self.receive_until_ready
86
+
87
+ end
88
+
89
+ def receive
90
+
91
+ packets = []
92
+ next_packet = nil
93
+
94
+ begin
95
+
96
+ begin
97
+ data, _ = @socket.recvfrom(2048)
98
+ rescue
99
+ p "ERR"
100
+ return []
101
+ end
102
+
103
+ # print "RX(#{data.length}) "; p data.to_hex
104
+
105
+ bitmask, size, uid, ack_id, _, package_id = data.unpack("CXS>S>S>LS>")
106
+ @uid = uid
107
+
108
+ bitmask = bitmask >> 3
109
+ size &= 0x07FF
110
+
111
+ # print "RX HEADER: "
112
+ # p [bitmask, size, uid, ack_id, package_id]
113
+
114
+ @ack_id = package_id
115
+
116
+ packet = [ack_id, bitmask, package_id, data[ @@SIZE_OF_HEADER .. -1 ]]
117
+
118
+ packets += handle(packet)
119
+
120
+ # raise Retry
121
+
122
+ # rescue Retry
123
+ # retry if next_packet and next_packet.length >= @@SIZE_OF_HEADER
124
+ end
125
+
126
+ packets
127
+
128
+ end
129
+
130
+ def receive_until_ready
131
+
132
+ packets = []
133
+ while !@ready
134
+ packets += receive
135
+ end
136
+ packets
137
+
138
+ end
139
+
140
+ private
141
+
142
+ def handle packet
143
+
144
+ bitmask = packet[1]
145
+
146
+ if (bitmask & Packet::HELLO) == Packet::HELLO
147
+
148
+ @ready = false
149
+ self << [Packet::ACK, 0x0, '']
150
+
151
+ elsif ((bitmask & Packet::ACK_REQ) == Packet::ACK_REQ) and (@ready or (!@ready and packet[3].length == 0))
152
+
153
+ self << [Packet::ACK, packet[2], '']
154
+ @ready = true
155
+
156
+ end
157
+
158
+ data = packet[3]
159
+ packets = []
160
+
161
+ while data != nil and data.length > 0
162
+
163
+ packet, data = payload(data)
164
+ packets << packet
165
+
166
+ end
167
+
168
+ packets
169
+
170
+ end
171
+
172
+ def payload packet
173
+
174
+ size = packet.unpack("S>")[0]
175
+ pack = packet[4..size-1]
176
+ packet = packet[size..-1]
177
+
178
+ command = pack.slice!(0, 4)
179
+
180
+ [[command, pack], packet]
181
+
182
+ end
183
+
184
+ end
185
+
186
+ end
@@ -0,0 +1,158 @@
1
+ module ATEM
2
+
3
+ class Switcher
4
+
5
+ attr_reader :version, :product, :topology, :video_mode, :master
6
+
7
+ def initialize config
8
+ @config = config
9
+ @inputs = ATEM::Switcher::InputCollection.new self
10
+ @_audio_by_index = []
11
+ end
12
+
13
+ def connect
14
+
15
+ @airtower = ATEM::Network.new @config[:ip], @config[:port], @config[:uid]
16
+
17
+ response = @airtower.hello
18
+ # @airtower.send! "FTSU", "\x0" * 12
19
+ response.each { | c | handle c }
20
+
21
+ end
22
+
23
+ # YIKES!
24
+ def handle packet
25
+
26
+ case packet[0]
27
+ when "_ver"
28
+
29
+ @version = packet[1].unpack("S>S>")
30
+
31
+ when "_pin"
32
+
33
+ @product = packet[1].unpack("a20")[0]
34
+
35
+ when "_top"
36
+
37
+ top = ["MEs", "Sources", "Colour Generators", "AUX busses", "DSKs", "Stingers", "DVEs", "SuperSources"]
38
+ @topology = Hash[top.zip(packet[1].unpack("CCCCCCCC"))]
39
+
40
+ when "VidM"
41
+
42
+ @video_mode = packet[1].unpack("C")
43
+
44
+ when "InPr"
45
+
46
+ input = ATEM::Switcher::Input.from packet[1], self, ATEM::Switcher::Input::Type::VIDEO
47
+ @inputs.add input
48
+
49
+ when "AMIP"
50
+
51
+ audio_id = packet[1].unpack("S>")[0] #("S>CxxxCCCxS>s>")
52
+
53
+ input = @inputs[audio_id]
54
+
55
+ if !@inputs[audio_id]
56
+ input = ATEM::Switcher::Input.new self
57
+ input.init audio_id
58
+ @inputs.add(input)
59
+ end
60
+
61
+ input.type |= ATEM::Switcher::Input::Type::AUDIO
62
+ input.audio = ATEM::Switcher::Input::Audio.from packet[1], self, input
63
+
64
+ when "AMLv"
65
+
66
+ master = {}
67
+ sources, master[:left], master[:right], master[:left_peak], master[:right_peak],
68
+ monitor = packet[1].unpack("S>xxl>l>l>l>l>")
69
+
70
+ @master = master
71
+ start_offset = 38 + sources * 2
72
+
73
+ (0..sources-1).each do | source |
74
+
75
+ source_id = packet[1][(36 + source * 2)..-1].unpack("S>")[0]
76
+
77
+ levels = {}
78
+
79
+ levels[:left], levels[:right], levels[:left_peak], levels[:right_peak] =
80
+ packet[1][(start_offset + source * 16)..-1].unpack("l>l>l>l>")
81
+
82
+ @inputs[source_id].audio.levels = levels
83
+
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+
90
+ def disconnect
91
+
92
+ @airtower.disconnect
93
+
94
+ end
95
+
96
+ def inputs
97
+ @inputs
98
+ end
99
+
100
+ def multithreading
101
+ @thread != nil
102
+ end
103
+
104
+ def multithreading= enabled
105
+
106
+ @thread.kill if @thread
107
+ @thread = nil
108
+ return if !enabled
109
+
110
+ Thread.abort_on_exception = true
111
+ @thread = Thread.new do
112
+
113
+ loop do
114
+
115
+ packets = @airtower.receive
116
+ packets.each do | packet |
117
+ handle packet
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ end
125
+
126
+ def use_audio_levels
127
+ @use_audio_levels
128
+ end
129
+
130
+ def use_audio_levels= enabled
131
+
132
+ self.multithreading = true if !@thread
133
+ @airtower.send! "SALN", [enabled ? 1 : 0].pack("C") + "\0\0\0"
134
+
135
+ end
136
+
137
+ def reset_audio_peaks
138
+
139
+ @inputs.each do | id, input |
140
+
141
+ puts "Resetting #{input.name}" if input.audio != nil
142
+ @airtower.send! "RAMP", [2, 0, input.audio.id, 1, 0, 0, 0].pack("CCS>CCCC") if input.audio != nil
143
+
144
+ end
145
+
146
+ end
147
+
148
+ def preview id
149
+ @airtower.send! "CPvI", [0, 0, id].pack("CCS>")
150
+ end
151
+
152
+ def program id
153
+ @airtower.send! "CPgI", [0, 0, id].pack("CCS>")
154
+ end
155
+
156
+ end
157
+
158
+ end
@@ -0,0 +1,63 @@
1
+ module ATEM
2
+
3
+ class Switcher
4
+
5
+ class Input
6
+
7
+ attr_reader :switcher, :id, :name, :short_name, :quick_init
8
+ attr_accessor :type, :audio
9
+
10
+ module Type
11
+ VIDEO = 0x01
12
+ AUDIO = 0x02
13
+ AUDIO_VIDEO = 0x03
14
+ end
15
+
16
+ def self.from packet, switcher, type
17
+
18
+ input = self.new switcher
19
+ input.init_from packet
20
+
21
+ input.type = type
22
+
23
+ input
24
+
25
+ end
26
+
27
+ def initialize switcher
28
+
29
+ @switcher = switcher
30
+
31
+ end
32
+
33
+ def init_from packet
34
+
35
+ @id, @name, @short_name, @supported, @ext_port_type, @port_type, @availability =
36
+ packet.unpack("S>Z20Z4xCxCCxC")
37
+
38
+ end
39
+
40
+ def init id, name = nil, short_name = nil
41
+
42
+ @id = id
43
+ @name = name or "Input #{@id}"
44
+ @short_name = short_name or "#{@id.to_s.rjust(4, "0")}"
45
+ @quick_init = true
46
+
47
+ end
48
+
49
+ ######
50
+
51
+ def preview
52
+ @switcher.preview @id
53
+ end
54
+
55
+ def program
56
+ @switcher.program @id
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,59 @@
1
+ module ATEM
2
+
3
+ class Switcher
4
+
5
+ class Input
6
+
7
+ class Audio
8
+
9
+ attr_reader :switcher, :input, :id, :type, :media_player, :plug, :mix, :gain, :balance
10
+ attr_accessor :levels
11
+
12
+ def self.from packet, switcher, input
13
+
14
+ audio = self.new switcher, input
15
+ audio.init_from packet
16
+ audio
17
+
18
+ end
19
+
20
+ def initialize switcher, input
21
+
22
+ @switcher = switcher
23
+ @input = input
24
+ @level = 0
25
+
26
+ end
27
+
28
+ def init_from packet
29
+
30
+ @id, @type, @media_player, @plug, @mix, @gain, @balance =
31
+ packet.unpack("S>CxxxCCCxS>s>")
32
+
33
+ # Now we have the right data, we can name the input itself
34
+ if @input.quick_init
35
+
36
+ values = {
37
+ 1001 => ['XLR', 'XLR0'],
38
+ 1101 => ['AES/EBU', 'AES3'],
39
+ 1201 => ['RCA', 'RCA_'],
40
+ }
41
+ @input.init @input.id, values[@input.id][0], values[@input.id][1]
42
+
43
+ end
44
+
45
+ end
46
+
47
+ #####
48
+
49
+ def level
50
+ (@levels[:left] + @levels[:right]) / 2
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,42 @@
1
+ module ATEM
2
+
3
+ class Switcher
4
+
5
+ class InputCollection
6
+
7
+ include Enumerable
8
+
9
+ attr_reader :switcher
10
+
11
+ def initialize switcher
12
+ @switcher = switcher
13
+ @inputs = {}
14
+ end
15
+
16
+ def add input
17
+
18
+ @inputs[input.id] = input
19
+
20
+ end
21
+
22
+ def [] index
23
+
24
+ return @inputs[index] if @inputs[index]
25
+
26
+ @inputs.each do | a, input |
27
+
28
+ return input if input.name == index or input.short_name.downcase == index.downcase
29
+
30
+ end if index.is_a? String
31
+
32
+ end
33
+
34
+ def each(&block)
35
+ @inputs.each(&block)
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: libatem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jamie Woods
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A library that allows control of Blackmagic Design ATEM switchers through
14
+ Ruby
15
+ email: gnitupmoc@insanityradiodotcom
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/atem/network.rb
21
+ - lib/atem/switcher/input/audio.rb
22
+ - lib/atem/switcher/input.rb
23
+ - lib/atem/switcher/input_collection.rb
24
+ - lib/atem/switcher.rb
25
+ - lib/atem.rb
26
+ - README.md
27
+ homepage: https://tech.insanityradio.com/
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.0.14.1
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: ATEM for Ruby
51
+ test_files: []