voicemeeter 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +775 -0
- data/lib/voicemeeter/base.rb +225 -0
- data/lib/voicemeeter/bus.rb +129 -0
- data/lib/voicemeeter/button.rb +70 -0
- data/lib/voicemeeter/cbindings.rb +81 -0
- data/lib/voicemeeter/command.rb +35 -0
- data/lib/voicemeeter/configs.rb +117 -0
- data/lib/voicemeeter/device.rb +28 -0
- data/lib/voicemeeter/event.rb +82 -0
- data/lib/voicemeeter/fx.rb +42 -0
- data/lib/voicemeeter/install.rb +38 -0
- data/lib/voicemeeter/iremote.rb +51 -0
- data/lib/voicemeeter/kinds.rb +51 -0
- data/lib/voicemeeter/logger.rb +9 -0
- data/lib/voicemeeter/meta.rb +75 -0
- data/lib/voicemeeter/midi.rb +14 -0
- data/lib/voicemeeter/mixins.rb +83 -0
- data/lib/voicemeeter/option.rb +69 -0
- data/lib/voicemeeter/patch.rb +79 -0
- data/lib/voicemeeter/recorder.rb +90 -0
- data/lib/voicemeeter/remote.rb +102 -0
- data/lib/voicemeeter/strip.rb +254 -0
- data/lib/voicemeeter/util.rb +35 -0
- data/lib/voicemeeter/vban.rb +105 -0
- data/lib/voicemeeter/version.rb +25 -0
- data/lib/voicemeeter/worker.rb +64 -0
- data/lib/voicemeeter.rb +56 -0
- metadata +149 -0
| @@ -0,0 +1,225 @@ | |
| 1 | 
            +
            module Voicemeeter
         | 
| 2 | 
            +
              # Base class for Remote
         | 
| 3 | 
            +
              class Base
         | 
| 4 | 
            +
                include Logging
         | 
| 5 | 
            +
                include Worker
         | 
| 6 | 
            +
                include Events::Director
         | 
| 7 | 
            +
                prepend Util::Cache
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                attr_reader :kind, :midi, :event, :delay, :cache
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                RATELIMIT = 0.033
         | 
| 12 | 
            +
                DELAY = 0.001
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def initialize(kind, **kwargs)
         | 
| 15 | 
            +
                  @kind = kind
         | 
| 16 | 
            +
                  @sync = kwargs[:sync] || false
         | 
| 17 | 
            +
                  @ratelimit = kwargs[:ratelimit] || RATELIMIT
         | 
| 18 | 
            +
                  @delay = kwargs[:delay] || DELAY
         | 
| 19 | 
            +
                  @event =
         | 
| 20 | 
            +
                    Events::Tracker.new(
         | 
| 21 | 
            +
                      **(kwargs.select { |k, _| %i[pdirty mdirty ldirty midi].include? k })
         | 
| 22 | 
            +
                    )
         | 
| 23 | 
            +
                  @midi = Midi.new
         | 
| 24 | 
            +
                  @cache = {strip_mode: 0}
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def to_s
         | 
| 28 | 
            +
                  "Voicemeeter #{kind}"
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def login
         | 
| 32 | 
            +
                  CBindings.call(:bind_login, ok: [0, 1]) == 1 and run_voicemeeter(kind.name)
         | 
| 33 | 
            +
                  clear_dirty
         | 
| 34 | 
            +
                  logger.info "Successfully logged into #{self} version #{version}"
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def logout
         | 
| 38 | 
            +
                  sleep(0.1)
         | 
| 39 | 
            +
                  CBindings.call(:bind_logout)
         | 
| 40 | 
            +
                  logger.info "Successfully logged out of #{self}"
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def pdirty?
         | 
| 44 | 
            +
                  CBindings.call(:bind_is_parameters_dirty, ok: [0, 1]) == 1
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def mdirty?
         | 
| 48 | 
            +
                  CBindings.call(:bind_macro_button_is_dirty, ok: [0, 1]) == 1
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def ldirty?
         | 
| 52 | 
            +
                  cache[:strip_buf], cache[:bus_buf] = _get_levels
         | 
| 53 | 
            +
                  !(
         | 
| 54 | 
            +
                    cache[:strip_level] == cache[:strip_buf] &&
         | 
| 55 | 
            +
                      cache[:bus_level] == cache[:bus_buf]
         | 
| 56 | 
            +
                  )
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def clear_dirty
         | 
| 60 | 
            +
                  catch(:clear) do
         | 
| 61 | 
            +
                    loop { throw(:clear) unless pdirty? || mdirty? }
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def run_voicemeeter(kind_id)
         | 
| 66 | 
            +
                  kinds = {
         | 
| 67 | 
            +
                    basic: Kinds::KindEnum::BASIC,
         | 
| 68 | 
            +
                    banana: Kinds::KindEnum::BANANA,
         | 
| 69 | 
            +
                    potato: (Install::OS_BITS == 64) ? Kinds::KindEnum::POTATOX64 : Kinds::KindEnum::POTATO
         | 
| 70 | 
            +
                  }
         | 
| 71 | 
            +
                  if caller(1..1).first[/`(.*)'/, 1] == "login"
         | 
| 72 | 
            +
                    logger.debug "Voicemeeter engine running but the GUI appears to be down... launching."
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                  CBindings.call(:bind_run_voicemeeter, kinds[kind_id])
         | 
| 75 | 
            +
                  sleep(1)
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def type
         | 
| 79 | 
            +
                  ckind = FFI::MemoryPointer.new(:long, 1)
         | 
| 80 | 
            +
                  CBindings.call(:bind_get_voicemeeter_type, ckind)
         | 
| 81 | 
            +
                  kinds = [nil, :basic, :banana, :potato]
         | 
| 82 | 
            +
                  kinds[ckind.read_long]
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def version
         | 
| 86 | 
            +
                  cver = FFI::MemoryPointer.new(:long, 1)
         | 
| 87 | 
            +
                  CBindings.call(:bind_get_voicemeeter_version, cver)
         | 
| 88 | 
            +
                  [
         | 
| 89 | 
            +
                    (cver.read_long & 0xFF000000) >> 24,
         | 
| 90 | 
            +
                    (cver.read_long & 0x00FF0000) >> 16,
         | 
| 91 | 
            +
                    (cver.read_long & 0x0000FF00) >> 8,
         | 
| 92 | 
            +
                    cver.read_long & 0x000000FF
         | 
| 93 | 
            +
                  ].join(".")
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def get(name, is_string = false)
         | 
| 97 | 
            +
                  if is_string
         | 
| 98 | 
            +
                    cget = FFI::MemoryPointer.new(:string, 512, true)
         | 
| 99 | 
            +
                    CBindings.call(:bind_get_parameter_string_a, name, cget)
         | 
| 100 | 
            +
                    cget.read_string
         | 
| 101 | 
            +
                  else
         | 
| 102 | 
            +
                    cget = FFI::MemoryPointer.new(:float, 1)
         | 
| 103 | 
            +
                    CBindings.call(:bind_get_parameter_float, name, cget)
         | 
| 104 | 
            +
                    cget.read_float.round(1)
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                def set(name, value)
         | 
| 109 | 
            +
                  if value.is_a? String
         | 
| 110 | 
            +
                    CBindings.call(:bind_set_parameter_string_a, name, value)
         | 
| 111 | 
            +
                  else
         | 
| 112 | 
            +
                    CBindings.call(:bind_set_parameter_float, name, value.to_f)
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                  cache.store(name, value)
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def get_buttonstatus(id, mode)
         | 
| 118 | 
            +
                  cget = FFI::MemoryPointer.new(:float, 1)
         | 
| 119 | 
            +
                  CBindings.call(:bind_macro_button_get_status, id, cget, mode)
         | 
| 120 | 
            +
                  cget.read_float.to_i
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def set_buttonstatus(id, mode, state)
         | 
| 124 | 
            +
                  CBindings.call(:bind_macro_button_set_status, id, state, mode)
         | 
| 125 | 
            +
                  cache.store("mb_#{id}_#{mode}", state)
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                def get_level(mode, index)
         | 
| 129 | 
            +
                  cget = FFI::MemoryPointer.new(:float, 1)
         | 
| 130 | 
            +
                  CBindings.call(:bind_get_level, mode, index, cget)
         | 
| 131 | 
            +
                  cget.read_float
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                private def _get_levels
         | 
| 135 | 
            +
                  strip_mode = cache[:strip_mode]
         | 
| 136 | 
            +
                  [
         | 
| 137 | 
            +
                    (0...kind.num_strip_levels).map { get_level(strip_mode, _1) },
         | 
| 138 | 
            +
                    (0...kind.num_bus_levels).map { get_level(3, _1) }
         | 
| 139 | 
            +
                  ]
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def get_num_devices(dir)
         | 
| 143 | 
            +
                  unless %i[in out].include? dir
         | 
| 144 | 
            +
                    raise Errors::VMError.new "dir got: #{dir}, expected :in or :out"
         | 
| 145 | 
            +
                  end
         | 
| 146 | 
            +
                  if dir == :in
         | 
| 147 | 
            +
                    CBindings.call(:bind_input_get_device_number, exp: ->(x) { x >= 0 })
         | 
| 148 | 
            +
                  else
         | 
| 149 | 
            +
                    CBindings.call(:bind_output_get_device_number, exp: ->(x) { x >= 0 })
         | 
| 150 | 
            +
                  end
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                def get_device_description(index, dir)
         | 
| 154 | 
            +
                  unless %i[in out].include? dir
         | 
| 155 | 
            +
                    raise Errors::VMError.new "dir got: #{dir}, expected :in or :out"
         | 
| 156 | 
            +
                  end
         | 
| 157 | 
            +
                  ctype = FFI::MemoryPointer.new(:long, 1)
         | 
| 158 | 
            +
                  cname = FFI::MemoryPointer.new(:string, 256, true)
         | 
| 159 | 
            +
                  chwid = FFI::MemoryPointer.new(:string, 256, true)
         | 
| 160 | 
            +
                  if dir == :in
         | 
| 161 | 
            +
                    CBindings.call(
         | 
| 162 | 
            +
                      :bind_input_get_device_desc_a,
         | 
| 163 | 
            +
                      index,
         | 
| 164 | 
            +
                      ctype,
         | 
| 165 | 
            +
                      cname,
         | 
| 166 | 
            +
                      chwid
         | 
| 167 | 
            +
                    )
         | 
| 168 | 
            +
                  else
         | 
| 169 | 
            +
                    CBindings.call(
         | 
| 170 | 
            +
                      :bind_output_get_device_desc_a,
         | 
| 171 | 
            +
                      index,
         | 
| 172 | 
            +
                      ctype,
         | 
| 173 | 
            +
                      cname,
         | 
| 174 | 
            +
                      chwid
         | 
| 175 | 
            +
                    )
         | 
| 176 | 
            +
                  end
         | 
| 177 | 
            +
                  [cname.read_string, ctype.read_long, chwid.read_string]
         | 
| 178 | 
            +
                end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                def get_midi_message
         | 
| 181 | 
            +
                  cmsg = FFI::MemoryPointer.new(:string, 1024, true)
         | 
| 182 | 
            +
                  res =
         | 
| 183 | 
            +
                    CBindings.call(
         | 
| 184 | 
            +
                      :bind_get_midi_message,
         | 
| 185 | 
            +
                      cmsg,
         | 
| 186 | 
            +
                      1024,
         | 
| 187 | 
            +
                      ok: [-5, -6],
         | 
| 188 | 
            +
                      exp: ->(x) { x >= 0 }
         | 
| 189 | 
            +
                    )
         | 
| 190 | 
            +
                  if (got_midi = res > 0)
         | 
| 191 | 
            +
                    data = cmsg.read_bytes(res).bytes
         | 
| 192 | 
            +
                    data.each_slice(3) do |ch, key, velocity|
         | 
| 193 | 
            +
                      midi.channel = ch
         | 
| 194 | 
            +
                      midi.current = key
         | 
| 195 | 
            +
                      midi.cache[key] = velocity
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
                  got_midi
         | 
| 199 | 
            +
                end
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                def sendtext(script)
         | 
| 202 | 
            +
                  raise ArgumentError, "script must not exceed 48kB" if script.length > 48000
         | 
| 203 | 
            +
                  CBindings.call(:bind_set_parameters, script)
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                def apply(data)
         | 
| 207 | 
            +
                  data.each do |key, hash|
         | 
| 208 | 
            +
                    case key.to_s.split("-")
         | 
| 209 | 
            +
                    in [/strip|bus|button/ => kls, /^[0-9]+$/ => index]
         | 
| 210 | 
            +
                      target = send(kls)
         | 
| 211 | 
            +
                    in ["vban", /in|instream|out|oustream/ => dir, /^[0-9]+$/ => index]
         | 
| 212 | 
            +
                      target = vban.send("#{dir.chomp("stream")}stream")
         | 
| 213 | 
            +
                    else
         | 
| 214 | 
            +
                      raise KeyError, "invalid config key '#{key}'"
         | 
| 215 | 
            +
                    end
         | 
| 216 | 
            +
                    target[index.to_i].apply(hash)
         | 
| 217 | 
            +
                  end
         | 
| 218 | 
            +
                end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                def apply_config(name)
         | 
| 221 | 
            +
                  apply(configs[name])
         | 
| 222 | 
            +
                  logger.info "profile #{name} applied!"
         | 
| 223 | 
            +
                end
         | 
| 224 | 
            +
              end
         | 
| 225 | 
            +
            end
         | 
| @@ -0,0 +1,129 @@ | |
| 1 | 
            +
            module Voicemeeter
         | 
| 2 | 
            +
              module Bus
         | 
| 3 | 
            +
                # Base class for Bus
         | 
| 4 | 
            +
                class Base
         | 
| 5 | 
            +
                  include IRemote
         | 
| 6 | 
            +
                  include Mixins::Fades
         | 
| 7 | 
            +
                  include Mixins::Return
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  attr_reader :eq, :mode, :levels
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def self.make(remote, i)
         | 
| 12 | 
            +
                    (i < remote.kind.phys_out) ? PhysicalBus.new(remote, i) : VirtualBus.new(remote, i)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def initialize(remote, i)
         | 
| 16 | 
            +
                    super
         | 
| 17 | 
            +
                    make_accessor_bool :mute, :mono, :sel, :monitor
         | 
| 18 | 
            +
                    make_accessor_float :gain
         | 
| 19 | 
            +
                    make_accessor_string :label
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    @eq = BusEq.new(remote, i)
         | 
| 22 | 
            +
                    @mode = BusModes.new(remote, i)
         | 
| 23 | 
            +
                    @levels = BusLevels.new(remote, i)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def identifier
         | 
| 27 | 
            +
                    "bus[#{@index}]"
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                # Represents a Physical Bus
         | 
| 32 | 
            +
                class PhysicalBus < Base; end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                # Represents a Virtual Bus
         | 
| 35 | 
            +
                class VirtualBus < Base; end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                class BusEq
         | 
| 38 | 
            +
                  include IRemote
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def initialize(remote, i)
         | 
| 41 | 
            +
                    super
         | 
| 42 | 
            +
                    make_accessor_bool :on, :ab
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def identifier
         | 
| 46 | 
            +
                    "bus[#{@index}].eq"
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                class BusModes
         | 
| 51 | 
            +
                  include IRemote
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def initialize(remote, i)
         | 
| 54 | 
            +
                    super
         | 
| 55 | 
            +
                    make_accessor_bool :normal,
         | 
| 56 | 
            +
                      :amix,
         | 
| 57 | 
            +
                      :bmix,
         | 
| 58 | 
            +
                      :repeat,
         | 
| 59 | 
            +
                      :composite,
         | 
| 60 | 
            +
                      :tvmix,
         | 
| 61 | 
            +
                      :upmix21,
         | 
| 62 | 
            +
                      :upmix41,
         | 
| 63 | 
            +
                      :upmix61,
         | 
| 64 | 
            +
                      :centeronly,
         | 
| 65 | 
            +
                      :lfeonly,
         | 
| 66 | 
            +
                      :rearonly
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  def identifier
         | 
| 70 | 
            +
                    "bus[#{@index}].mode"
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def get
         | 
| 74 | 
            +
                    sleep(@remote.delay)
         | 
| 75 | 
            +
                    %i[amix bmix repeat composite tvmix upmix21 upmix41 upmix61 centeronly lfeonly rearonly].each do |mode|
         | 
| 76 | 
            +
                      if send(mode)
         | 
| 77 | 
            +
                        return mode
         | 
| 78 | 
            +
                      end
         | 
| 79 | 
            +
                    end
         | 
| 80 | 
            +
                    :normal
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
              end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
              class BusLevels
         | 
| 86 | 
            +
                include IRemote
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def initialize(remote, i)
         | 
| 89 | 
            +
                  super
         | 
| 90 | 
            +
                  @init = i * 8
         | 
| 91 | 
            +
                  @offset = 8
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                def identifier
         | 
| 95 | 
            +
                  "bus[#{@index}]"
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                def getter(mode)
         | 
| 99 | 
            +
                  convert = ->(x) { (x > 0) ? (20 * Math.log(x, 10)).round(1) : -200.0 }
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  vals = if @remote.running? && @remote.event.ldirty
         | 
| 102 | 
            +
                    @remote.cache[:bus_level][@init, @offset]
         | 
| 103 | 
            +
                  else
         | 
| 104 | 
            +
                    (@init...@init + @offset).map { |i| @remote.get_level(mode, i) }
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                  vals.map(&convert)
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def all
         | 
| 110 | 
            +
                  getter(Mixins::LevelEnum::BUS)
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                def isdirty? = @remote.cache[:bus_comp][@init, @offset].any?
         | 
| 114 | 
            +
              end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
              class BusDevice
         | 
| 117 | 
            +
                include IRemote
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def initialize(remote, i)
         | 
| 120 | 
            +
                  super
         | 
| 121 | 
            +
                  make_reader_only :name, :sr
         | 
| 122 | 
            +
                  make_writer_only :wdm, :ks, :mme, :asio
         | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                def identifier
         | 
| 126 | 
            +
                  "bus[#{@index}].device"
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
              end
         | 
| 129 | 
            +
            end
         | 
| @@ -0,0 +1,70 @@ | |
| 1 | 
            +
            module Voicemeeter
         | 
| 2 | 
            +
              module Button
         | 
| 3 | 
            +
                module ButtonEnum
         | 
| 4 | 
            +
                  STATE = 1
         | 
| 5 | 
            +
                  STATEONLY = 2
         | 
| 6 | 
            +
                  TRIGGER = 3
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def identifier(val)
         | 
| 9 | 
            +
                    [nil, :state, :stateonly, :trigger][val]
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  module_function :identifier
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                module ButtonColorMixin
         | 
| 16 | 
            +
                  def identifier
         | 
| 17 | 
            +
                    "command.button[#{@index}]"
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def color
         | 
| 21 | 
            +
                    method(:getter).super_method.call("color").to_i
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def color=(val)
         | 
| 25 | 
            +
                    method(:setter).super_method.call("color", val)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                # Base class for Button
         | 
| 30 | 
            +
                class Base
         | 
| 31 | 
            +
                  include Logging
         | 
| 32 | 
            +
                  include IRemote
         | 
| 33 | 
            +
                  include ButtonColorMixin
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def getter(mode)
         | 
| 36 | 
            +
                    logger.debug "getter: button[#{@index}].#{ButtonEnum.identifier(mode)}"
         | 
| 37 | 
            +
                    @remote.get_buttonstatus(@index, mode)
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def setter(mode, val)
         | 
| 41 | 
            +
                    logger.debug "setter: button[#{@index}].#{ButtonEnum.identifier(mode)}=#{val}"
         | 
| 42 | 
            +
                    @remote.set_buttonstatus(@index, mode, val)
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def state
         | 
| 46 | 
            +
                    getter(ButtonEnum::STATE) == 1
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def state=(value)
         | 
| 50 | 
            +
                    setter(ButtonEnum::STATE, value && 1 || 0)
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def stateonly
         | 
| 54 | 
            +
                    getter(ButtonEnum::STATEONLY) == 1
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def stateonly=(value)
         | 
| 58 | 
            +
                    setter(ButtonEnum::STATEONLY, value && 1 || 0)
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def trigger
         | 
| 62 | 
            +
                    getter(ButtonEnum::TRIGGER) == 1
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def trigger=(value)
         | 
| 66 | 
            +
                    setter(ButtonEnum::TRIGGER, value && 1 || 0)
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
            end
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            module Voicemeeter
         | 
| 2 | 
            +
              # Ruby bindings for the C-API functions
         | 
| 3 | 
            +
              module CBindings
         | 
| 4 | 
            +
                extend Logging
         | 
| 5 | 
            +
                extend FFI::Library
         | 
| 6 | 
            +
                using Util::CoreExtensions
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                private
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                VM_PATH = Install.get_vmpath
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                ffi_lib VM_PATH.join(
         | 
| 13 | 
            +
                  "VoicemeeterRemote#{(Install::OS_BITS == 64) ? "64" : "32"}.dll"
         | 
| 14 | 
            +
                )
         | 
| 15 | 
            +
                ffi_convention :stdcall
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                private_class_method def self.attach_function(c_name, args, returns)
         | 
| 18 | 
            +
                  ruby_name = :"bind_#{c_name.to_s.delete_prefix("VBVMR_").snakecase}"
         | 
| 19 | 
            +
                  super(ruby_name, c_name, args, returns)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                attach_function :VBVMR_Login, [], :long
         | 
| 23 | 
            +
                attach_function :VBVMR_Logout, [], :long
         | 
| 24 | 
            +
                attach_function :VBVMR_RunVoicemeeter, [:long], :long
         | 
| 25 | 
            +
                attach_function :VBVMR_GetVoicemeeterType, [:pointer], :long
         | 
| 26 | 
            +
                attach_function :VBVMR_GetVoicemeeterVersion, [:pointer], :long
         | 
| 27 | 
            +
                attach_function :VBVMR_MacroButton_IsDirty, [], :long
         | 
| 28 | 
            +
                attach_function :VBVMR_MacroButton_GetStatus, %i[long pointer long], :long
         | 
| 29 | 
            +
                attach_function :VBVMR_MacroButton_SetStatus, %i[long float long], :long
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                attach_function :VBVMR_IsParametersDirty, [], :long
         | 
| 32 | 
            +
                attach_function :VBVMR_GetParameterFloat, %i[string pointer], :long
         | 
| 33 | 
            +
                attach_function :VBVMR_SetParameterFloat, %i[string float], :long
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                attach_function :VBVMR_GetParameterStringA, %i[string pointer], :long
         | 
| 36 | 
            +
                attach_function :VBVMR_SetParameterStringA, %i[string string], :long
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                attach_function :VBVMR_SetParameters, [:string], :long
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                attach_function :VBVMR_GetLevel, %i[long long pointer], :long
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                attach_function :VBVMR_Input_GetDeviceNumber, [], :long
         | 
| 43 | 
            +
                attach_function :VBVMR_Input_GetDeviceDescA,
         | 
| 44 | 
            +
                  %i[long pointer pointer pointer],
         | 
| 45 | 
            +
                  :long
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                attach_function :VBVMR_Output_GetDeviceNumber, [], :long
         | 
| 48 | 
            +
                attach_function :VBVMR_Output_GetDeviceDescA,
         | 
| 49 | 
            +
                  %i[long pointer pointer pointer],
         | 
| 50 | 
            +
                  :long
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                attach_function :VBVMR_GetMidiMessage, %i[pointer long], :long
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def call(fn, *args, ok: [0], exp: nil)
         | 
| 55 | 
            +
                  to_cname = -> {
         | 
| 56 | 
            +
                    "VBVMR_#{fn.to_s.delete_prefix("bind_").camelcase.gsub(/(Button|Input|Output)/, '\1_')}"
         | 
| 57 | 
            +
                  }
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  res = send(fn, *args)
         | 
| 60 | 
            +
                  if exp
         | 
| 61 | 
            +
                    unless exp.call(res) || ok.include?(res)
         | 
| 62 | 
            +
                      raise Errors::VMCAPIError.new to_cname.call, res
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                  else
         | 
| 65 | 
            +
                    unless ok.include?(res)
         | 
| 66 | 
            +
                      raise Errors::VMCAPIError.new to_cname.call, res
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                  res
         | 
| 70 | 
            +
                rescue Errors::VMCAPIError => e
         | 
| 71 | 
            +
                  err_msg = [
         | 
| 72 | 
            +
                    "#{e.class.name}: #{e.message}",
         | 
| 73 | 
            +
                    *e.backtrace
         | 
| 74 | 
            +
                  ]
         | 
| 75 | 
            +
                  logger.error err_msg.join("\n")
         | 
| 76 | 
            +
                  raise
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                module_function :call
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            module Voicemeeter
         | 
| 2 | 
            +
              class Command
         | 
| 3 | 
            +
                include IRemote
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(remote)
         | 
| 6 | 
            +
                  super
         | 
| 7 | 
            +
                  make_action_method :show, :restart, :shutdown
         | 
| 8 | 
            +
                  make_writer_bool :showvbanchat, :lock
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def identifier
         | 
| 12 | 
            +
                  :command
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def hide
         | 
| 16 | 
            +
                  setter("show", 0)
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def load(value)
         | 
| 20 | 
            +
                  raise VMError.new("load got: #{value}, but expected a string") unless value.is_a? String
         | 
| 21 | 
            +
                  setter("load", value)
         | 
| 22 | 
            +
                  sleep(0.2)
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def save(value)
         | 
| 26 | 
            +
                  raise VMError.new("save got: #{value}, but expected a string") unless value.is_a? String
         | 
| 27 | 
            +
                  setter("save", value)
         | 
| 28 | 
            +
                  sleep(0.2)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def reset
         | 
| 32 | 
            +
                  @remote.apply_config(:reset)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,117 @@ | |
| 1 | 
            +
            module Voicemeeter
         | 
| 2 | 
            +
              module Configs
         | 
| 3 | 
            +
                class TOMLConfBuilder
         | 
| 4 | 
            +
                  def self.run(kind)
         | 
| 5 | 
            +
                    aouts = (0...kind.phys_out).to_h { |i| [:"A#{i + 1}", false] }
         | 
| 6 | 
            +
                    bouts = (0...kind.virt_out).to_h { |i| [:"B#{i + 1}", false] }
         | 
| 7 | 
            +
                    strip_bools = %i[mute mono solo].to_h { |param| [param, false] }
         | 
| 8 | 
            +
                    gain = [:gain].to_h { |param| [param, 0.0] }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    phys_float =
         | 
| 11 | 
            +
                      %i[comp gate denoiser].to_h { |param| [param, {knob: 0.0}] }
         | 
| 12 | 
            +
                    eq = [:eq].to_h { |param| [param, {on: false}] }
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    overrides = {B1: true}
         | 
| 15 | 
            +
                    phys_strip =
         | 
| 16 | 
            +
                      (0...kind.phys_in).to_h do |i|
         | 
| 17 | 
            +
                        [
         | 
| 18 | 
            +
                          "strip-#{i}".to_sym,
         | 
| 19 | 
            +
                          {**aouts, **bouts, **strip_bools, **gain, **phys_float, **eq, **overrides}
         | 
| 20 | 
            +
                        ]
         | 
| 21 | 
            +
                      end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    overrides = {A1: true}
         | 
| 24 | 
            +
                    virt_strip =
         | 
| 25 | 
            +
                      (kind.phys_in...kind.phys_in + kind.virt_in).to_h do |i|
         | 
| 26 | 
            +
                        [
         | 
| 27 | 
            +
                          :"strip-#{i}",
         | 
| 28 | 
            +
                          {**aouts, **bouts, **strip_bools, **gain, **overrides}
         | 
| 29 | 
            +
                        ]
         | 
| 30 | 
            +
                      end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    bus_bools = %i[mute mono].to_h { |param| [param, false] }
         | 
| 33 | 
            +
                    bus =
         | 
| 34 | 
            +
                      (0...kind.num_bus).to_h do |i|
         | 
| 35 | 
            +
                        [:"bus-#{i}", {**bus_bools, **gain, **eq}]
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    {**phys_strip, **virt_strip, **bus}
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                class FileReader
         | 
| 43 | 
            +
                  include Logging
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def initialize(kind)
         | 
| 46 | 
            +
                    @configpaths = [
         | 
| 47 | 
            +
                      Pathname.getwd.join("configs", kind.name.to_s),
         | 
| 48 | 
            +
                      Pathname.new(Dir.home).join(".config", "voicemeeter-rb", kind.name.to_s),
         | 
| 49 | 
            +
                      Pathname.new(Dir.home).join("Documents", "Voicemeeter", "configs", kind.name.to_s)
         | 
| 50 | 
            +
                    ]
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def each
         | 
| 54 | 
            +
                    @configpaths.each do |configpath|
         | 
| 55 | 
            +
                      if configpath.exist?
         | 
| 56 | 
            +
                        logger.debug "checking #{configpath} for configs"
         | 
| 57 | 
            +
                        filepaths = configpath.glob("*.{yaml,yml}")
         | 
| 58 | 
            +
                        filepaths.each do |filepath|
         | 
| 59 | 
            +
                          @filename = (filepath.basename.sub_ext "").to_s.to_sym
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                          yield @filename, YAML.load_file(
         | 
| 62 | 
            +
                            filepath,
         | 
| 63 | 
            +
                            symbolize_names: true
         | 
| 64 | 
            +
                          )
         | 
| 65 | 
            +
                        end
         | 
| 66 | 
            +
                      end
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
                  rescue Psych::SyntaxError => e
         | 
| 69 | 
            +
                    logger.error "#{e.class.name}: #{e.message}"
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                class Loader
         | 
| 74 | 
            +
                  include Logging
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  attr_reader :configs
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def initialize(kind)
         | 
| 79 | 
            +
                    @kind = kind
         | 
| 80 | 
            +
                    @configs = Hash.new do |hash, key|
         | 
| 81 | 
            +
                      raise Errors::VMError.new "unknown config '#{key}'. known configs: #{hash.keys}"
         | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
                    @filereader = FileReader.new(kind)
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  def to_s
         | 
| 87 | 
            +
                    "Loader #{@kind}"
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  def run
         | 
| 91 | 
            +
                    logger.debug "Running #{self}"
         | 
| 92 | 
            +
                    configs[:reset] = TOMLConfBuilder.run(@kind)
         | 
| 93 | 
            +
                    @filereader.each(&method(:register))
         | 
| 94 | 
            +
                    self
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  private def register(identifier, data)
         | 
| 98 | 
            +
                    if configs.key? identifier
         | 
| 99 | 
            +
                      logger.debug "config with name '#{identifier}' already in memory, skipping..."
         | 
| 100 | 
            +
                      return
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    configs[identifier] = data
         | 
| 104 | 
            +
                    logger.info "#{@kind.name}/#{identifier} loaded into memory"
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                def get(kind_id)
         | 
| 109 | 
            +
                  unless defined? @loaders
         | 
| 110 | 
            +
                    @loaders = Kinds::ALL.to_h { |kind| [kind.name, Loader.new(kind).run] }
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                  @loaders[kind_id].configs
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                module_function :get
         | 
| 116 | 
            +
              end
         | 
| 117 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            module Voicemeeter
         | 
| 2 | 
            +
              class Device
         | 
| 3 | 
            +
                def initialize(remote)
         | 
| 4 | 
            +
                  @remote = remote
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def to_s
         | 
| 8 | 
            +
                  "#{self.class.name.split("::").last}#{@index}#{@remote.kind}"
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def getter(**kwargs)
         | 
| 12 | 
            +
                  kwargs => {direction:}
         | 
| 13 | 
            +
                  return @remote.get_num_devices(direction) unless kwargs.key? :index
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  vals = @remote.get_device_description(kwargs[:index], direction)
         | 
| 16 | 
            +
                  types = {1 => "mme", 3 => "wdm", 4 => "ks", 5 => "asio"}
         | 
| 17 | 
            +
                  {name: vals[0], type: types[vals[1]], id: vals[2]}
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def ins = getter(direction: :in)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def outs = getter(direction: :out)
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def input(i) = getter(index: i, direction: :in)
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def output(i) = getter(index: i, direction: :out)
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         |