voicemeeter-rb/lib/voicemeeter/base.rb
2023-08-29 14:07:52 +01:00

226 lines
6.0 KiB
Ruby

module Voicemeeter
# Base class for Remote
class Base
include Logging
include Worker
include Events::Director
prepend Util::Cache
attr_reader :kind, :midi, :event, :delay, :cache
RATELIMIT = 0.033
DELAY = 0.001
def initialize(kind, **kwargs)
@kind = kind
@sync = kwargs[:sync] || false
@ratelimit = kwargs[:ratelimit] || RATELIMIT
@delay = kwargs[:delay] || DELAY
@event =
Events::Tracker.new(
**(kwargs.select { |k, _| %i[pdirty mdirty ldirty midi].include? k })
)
@midi = Midi.new
@cache = {strip_mode: 0}
end
def to_s
"Voicemeeter #{kind}"
end
def login
CBindings.call(:bind_login, ok: [0, 1]) == 1 and run_voicemeeter(kind.name)
clear_dirty
logger.info "Successfully logged into #{self} version #{version}"
end
def logout
sleep(0.1)
CBindings.call(:bind_logout)
logger.info "Successfully logged out of #{self}"
end
def pdirty?
CBindings.call(:bind_is_parameters_dirty, ok: [0, 1]) == 1
end
def mdirty?
CBindings.call(:bind_macro_button_is_dirty, ok: [0, 1]) == 1
end
def ldirty?
cache[:strip_buf], cache[:bus_buf] = _get_levels
!(
cache[:strip_level] == cache[:strip_buf] &&
cache[:bus_level] == cache[:bus_buf]
)
end
def clear_dirty
catch(:clear) do
loop { throw(:clear) unless pdirty? || mdirty? }
end
end
def run_voicemeeter(kind_id)
kinds = {
basic: Kinds::KindEnum::BASIC,
banana: Kinds::KindEnum::BANANA,
potato: (Install::OS_BITS == 64) ? Kinds::KindEnum::POTATOX64 : Kinds::KindEnum::POTATO
}
if caller(1..1).first[/`(.*)'/, 1] == "login"
logger.debug "Voicemeeter engine running but the GUI appears to be down... launching."
end
CBindings.call(:bind_run_voicemeeter, kinds[kind_id])
sleep(1)
end
def type
ckind = FFI::MemoryPointer.new(:long, 1)
CBindings.call(:bind_get_voicemeeter_type, ckind)
kinds = [nil, :basic, :banana, :potato]
kinds[ckind.read_long]
end
def version
cver = FFI::MemoryPointer.new(:long, 1)
CBindings.call(:bind_get_voicemeeter_version, cver)
[
(cver.read_long & 0xFF000000) >> 24,
(cver.read_long & 0x00FF0000) >> 16,
(cver.read_long & 0x0000FF00) >> 8,
cver.read_long & 0x000000FF
].join(".")
end
def get(name, is_string = false)
if is_string
cget = FFI::MemoryPointer.new(:string, 512, true)
CBindings.call(:bind_get_parameter_string_a, name, cget)
cget.read_string
else
cget = FFI::MemoryPointer.new(:float, 1)
CBindings.call(:bind_get_parameter_float, name, cget)
cget.read_float.round(1)
end
end
def set(name, value)
if value.is_a? String
CBindings.call(:bind_set_parameter_string_a, name, value)
else
CBindings.call(:bind_set_parameter_float, name, value.to_f)
end
cache.store(name, value)
end
def get_buttonstatus(id, mode)
cget = FFI::MemoryPointer.new(:float, 1)
CBindings.call(:bind_macro_button_get_status, id, cget, mode)
cget.read_float.to_i
end
def set_buttonstatus(id, mode, state)
CBindings.call(:bind_macro_button_set_status, id, state, mode)
cache.store("mb_#{id}_#{mode}", state)
end
def get_level(mode, index)
cget = FFI::MemoryPointer.new(:float, 1)
CBindings.call(:bind_get_level, mode, index, cget)
cget.read_float
end
private def _get_levels
strip_mode = cache[:strip_mode]
[
(0...kind.num_strip_levels).map { get_level(strip_mode, _1) },
(0...kind.num_bus_levels).map { get_level(3, _1) }
]
end
def get_num_devices(dir)
unless %i[in out].include? dir
raise Errors::VMError.new "dir got: #{dir}, expected :in or :out"
end
if dir == :in
CBindings.call(:bind_input_get_device_number, exp: ->(x) { x >= 0 })
else
CBindings.call(:bind_output_get_device_number, exp: ->(x) { x >= 0 })
end
end
def get_device_description(index, dir)
unless %i[in out].include? dir
raise Errors::VMError.new "dir got: #{dir}, expected :in or :out"
end
ctype = FFI::MemoryPointer.new(:long, 1)
cname = FFI::MemoryPointer.new(:string, 256, true)
chwid = FFI::MemoryPointer.new(:string, 256, true)
if dir == :in
CBindings.call(
:bind_input_get_device_desc_a,
index,
ctype,
cname,
chwid
)
else
CBindings.call(
:bind_output_get_device_desc_a,
index,
ctype,
cname,
chwid
)
end
[cname.read_string, ctype.read_long, chwid.read_string]
end
def get_midi_message
cmsg = FFI::MemoryPointer.new(:string, 1024, true)
res =
CBindings.call(
:bind_get_midi_message,
cmsg,
1024,
ok: [-5, -6],
exp: ->(x) { x >= 0 }
)
if (got_midi = res > 0)
data = cmsg.read_bytes(res).bytes
data.each_slice(3) do |ch, key, velocity|
midi.channel = ch
midi.current = key
midi.cache[key] = velocity
end
end
got_midi
end
def sendtext(script)
raise ArgumentError, "script must not exceed 48kB" if script.length > 48000
CBindings.call(:bind_set_parameters, script)
end
def apply(data)
data.each do |key, hash|
case key.to_s.split("-")
in [/strip|bus|button/ => kls, /^[0-9]+$/ => index]
target = send(kls)
in ["vban", /in|instream|out|oustream/ => dir, /^[0-9]+$/ => index]
target = vban.send("#{dir.chomp("stream")}stream")
else
raise KeyError, "invalid config key '#{key}'"
end
target[index.to_i].apply(hash)
end
end
def apply_config(name)
apply(configs[name])
logger.info "profile #{name} applied!"
end
end
end