Spouting VBAN into Ruby
The VBAN
protocol is what drives the best parts of VoiceMeeter. It has a pretty friendly spec document, and a brief implementation on GitHub by the original author. Outside of this, it's pretty much a best-guess with experimentation on speaking the language. There's a very helpful Python implementation too that helped with debugging a few quirks.
VBAN
has a sub-protocol for Service
data, which is what we'll be chatting with to get data in and out of VoiceMeeter. The spec document says it's partially implemented, but seems to work with VoiceMeeter Potato (at 3.0.2.2 anyway).
Starting with Ruby, a RT
packet looks something like this:
require 'bindata'
class VbanPacket < BinData::Record
endian :little
SUB_PROTOCOLS = [
:audio, :serial, :txt, :service, :undefined1, :undefined2, :undefined3, :user
]
string :vban, length: 4
uint8 :sample_rate_protocol
def sub_protocol_raw
sample_rate_protocol >> 5 # Top three bits only
end
def sub_protocol
SUB_PROTOCOLS[sub_protocol_raw]
end
end
class RtPacket < VbanPacket
VOICEMEETER_TYPES = [
'', 'basic', 'banana', 'potato'
]
uint8 :function
uint8 :service
uint8 :additional_info
string :stream_name, length: 16
uint32 :frame_id
# packet body
uint8 :voicemeeter_type_raw
uint8 :reserved
uint16 :buffer_size
array :voicemeeter_version_raw, type: :int8, initial_length: 4
uint32 :options
uint32 :sample_rate
array :input_levels_raw, type: :int16, initial_length: 34
array :output_levels_raw, type: :int16, initial_length: 64
uint32 :transport
array :strip_state, type: :uint32, initial_length: 8
array :bus_state, type: :uint32, initial_length: 8
array :strip_gain_layer_1, type: :uint16, initial_length: 8
array :strip_gain_layer_2, type: :uint16, initial_length: 8
array :strip_gain_layer_3, type: :uint16, initial_length: 8
array :strip_gain_layer_4, type: :uint16, initial_length: 8
array :strip_gain_layer_5, type: :uint16, initial_length: 8
array :strip_gain_layer_6, type: :uint16, initial_length: 8
array :strip_gain_layer_7, type: :uint16, initial_length: 8
array :strip_gain_layer_8, type: :uint16, initial_length: 8
array :bus_gain, type: :uint16, initial_length: 8
array :strip_labels, initial_length: 8 do
string length: 60, trim_padding: true
end
array :bus_labels, initial_length: 8 do
string length: 60, trim_padding: true
end
def voicemeeter_type
VOICEMEETER_TYPES[voicemeeter_type_raw]
end
def voicemeeter_version
voicemeeter_version_raw.to_a.reverse.join('.')
end
end
The input_levels
an output_levels
are packed according to this layout, with physical inputs having 2 channels, and virtual inputs and bus outputs having 8 channels each.
To get VoiceMeeter to spout all of its lovely internals at us, we have to 'register' as a receiver. This register packet just tells VoiceMeeter what we're interested in receiving and for how long. You have to periodically resend this register packet to ensure that you keep receiving all that wonderful state goodness. A register packet looks something like this:
class RegisterRtPacket < VbanPacket
RTPACKET_REGISTER = 32
uint8 :function
uint8 :service
uint8 :additional_info
string :stream_name, length: 16
uint32 :frame_id
array :packet_ids, type: :uint8, initial_length: 128
end
If you don't specify any packet IDs, then you should receive all of them. Actually sending the packet gets a bit meatier, but still easily doable.
require 'socket'
require 'thread'
@vban = UDPSocket.new
@vban.bind('192.168.x.x', 6980)
@outgoing_vban = Queue.new
# vban sender
Thread.new do
loop do
message = @outgoing_vban.pop(true) rescue next
@vban.send(message.to_binary_s, 0, '192.168.x.x', 6980)
end
end
register_rt_packet = RegisterRtPacket.new.tap do |p|
p.vban = "VBAN"
p.sub_protocol = :service
p.function = 0
p.service = RegisterRtPacket::RTPACKET_REGISTER
p.additional_info = 50
p.stream_name = "X-Touch meters"
p.frame_id = 1
end
@outgoing_vban << register_rt_packet
From there, you should start to get VoiceMeeter spouting a lot of data down that same port, but we'll get to that next time!