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