voicemeeter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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