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.
@@ -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