voicemeeter_api_ruby 4.1.5 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29df5251a1e4d779dd265a785112c0c8907b89f5f9a30a45a9e50985ed2400e7
4
- data.tar.gz: c4a9acbbbe3330aca966d4470373b9ec9f9fe3bcefa0b1f6e60ed665b853c712
3
+ metadata.gz: d897d4faf2ad7aac9c98689e7dc1b64dac891f610ba1153e46a7c7e97b273815
4
+ data.tar.gz: 8f275deec8cec53be8fa49a1c8e8605c6cb06153237e99539938fc2511288c8b
5
5
  SHA512:
6
- metadata.gz: c9e8f17bd773dbbbb9980f5bb91283b0f97be75acff6f2bf0c2315721fdfc9916509ec86a0a28e6db3d548fadbb161c3b40da9cbd8639b4fc98719fe309d1e27
7
- data.tar.gz: 12d256fb220ab60c3492d65855ed6cdf714af851f1a7dfcb2d6b3297e2c844a9776162ff600351c9f4baeb71a3a2e99492e19388a588f459301ed1aff4259d7d
6
+ metadata.gz: 80dc23b32bb5a398e96d249aac772246ac98b3f5b7b759018724fd0b07fc103a181e1e9caa3438dd62022252205ab21bdb0f5c652f7c686a6f9885e5cbd186f6
7
+ data.tar.gz: 83dab1c59de3a07a3441f50de27e883db07be47a6ff5d3134ad0c6a2f49e00ca813d21c94d05b09d820beecbd29f8ef07e84735ede3b9b8bc122ebd267b61e79
@@ -0,0 +1,223 @@
1
+ require "observer"
2
+
3
+ require "voicemeeter/runvm"
4
+ require "voicemeeter/configs"
5
+ require "voicemeeter/errors"
6
+
7
+ module Voicemeeter
8
+ class Base
9
+ "
10
+ Base class responsible for wrapping the C Remote API
11
+
12
+ Mixin required modules
13
+ "
14
+ include Observable
15
+ include Configs
16
+ include RunVM
17
+
18
+ attr_accessor :strip, :bus, :button, :vban, :command, :recorder, :device
19
+ attr_accessor :strip_mode
20
+
21
+ attr_reader :kind, :p_in, :v_in, :p_out, :v_out, :retval, :cache
22
+ attr_reader :running, :_strip_comp, :_bus_comp, :delay
23
+
24
+ DELAY = 0.001
25
+ SYNC = false
26
+ RATELIMIT = 0.033
27
+ SIZE = 1
28
+
29
+ def initialize(kind, **kwargs)
30
+ @kind = kind
31
+ @p_in, @v_in = kind.layout[:strip].values
32
+ @p_out, @v_out = kind.layout[:bus].values
33
+ @cache = Hash.new
34
+ @sync = kwargs[:sync] || SYNC
35
+ @ratelimit = kwargs[:ratelimit] || RATELIMIT
36
+ @running = false
37
+ @strip_mode = 0
38
+ @delay = DELAY
39
+ end
40
+
41
+ def init_thread
42
+ @running = true
43
+ @cache["strip_level"], @cache["bus_level"] = _get_levels
44
+ Thread.new do
45
+ loop do
46
+ Thread.stop if !@running
47
+ if pdirty?
48
+ changed
49
+ notify_observers("pdirty")
50
+ end
51
+ if mdirty?
52
+ changed
53
+ notify_observers("mdirty")
54
+ end
55
+ if ldirty?
56
+ changed
57
+ @_strip_comp =
58
+ @cache["strip_level"].map.with_index do |x, i|
59
+ !(x == @strip_buf[i])
60
+ end
61
+ @_bus_comp =
62
+ @cache["bus_level"].map.with_index { |x, i| !(x == @bus_buf[i]) }
63
+ @cache["strip_level"] = @strip_buf
64
+ @cache["bus_level"] = @bus_buf
65
+ notify_observers("ldirty")
66
+ end
67
+ sleep(@ratelimit)
68
+ end
69
+ end
70
+ end
71
+
72
+ def end_thread
73
+ @running = false
74
+ end
75
+
76
+ def login
77
+ @@cdll.call(:login)
78
+ clear_polling
79
+ rescue CAPIErrors => error
80
+ case
81
+ when error.value == 1
82
+ self.start(@kind.name)
83
+ clear_polling
84
+ when error.value < 0
85
+ raise
86
+ end
87
+ end
88
+
89
+ def logout
90
+ clear_polling
91
+ sleep(0.1)
92
+ @@cdll.call(:logout)
93
+ end
94
+
95
+ def type
96
+ c_type = FFI::MemoryPointer.new(:long, SIZE)
97
+ @@cdll.call(:vmtype, c_type)
98
+ types = { 1 => "basic", 2 => "banana", 3 => "potato" }
99
+ types[c_type.read_long]
100
+ end
101
+
102
+ def version
103
+ c_ver = FFI::MemoryPointer.new(:long, SIZE)
104
+ @@cdll.call(:vmversion, c_ver)
105
+ v1 = (c_ver.read_long & 0xFF000000) >> 24
106
+ v2 = (c_ver.read_long & 0x00FF0000) >> 16
107
+ v3 = (c_ver.read_long & 0x0000FF00) >> 8
108
+ v4 = c_ver.read_long & 0x000000FF
109
+ "#{v1}.#{v2}.#{v3}.#{v4}"
110
+ end
111
+
112
+ def get_parameter(name, is_string = false)
113
+ self.polling("get_parameter", name: name) do
114
+ if is_string
115
+ c_get = FFI::MemoryPointer.new(:string, 512, true)
116
+ @@cdll.call(:get_parameter_string, name, c_get)
117
+ c_get.read_string
118
+ else
119
+ c_get = FFI::MemoryPointer.new(:float, SIZE)
120
+ @@cdll.call(:get_parameter_float, name, c_get)
121
+ c_get.read_float.round(1)
122
+ end
123
+ end
124
+ end
125
+
126
+ def set_parameter(name, value)
127
+ if value.is_a? String
128
+ @@cdll.call(:set_parameter_string, name, value)
129
+ else
130
+ @@cdll.call(:set_parameter_float, name, value.to_f)
131
+ end
132
+ @cache.store(name, value)
133
+ end
134
+
135
+ def get_buttonstatus(id, mode)
136
+ self.polling("get_buttonstatus", id: id, mode: mode) do
137
+ c_get = FFI::MemoryPointer.new(:float, SIZE)
138
+ @@cdll.call(:get_buttonstatus, id, c_get, mode)
139
+ c_get.read_float.to_i
140
+ end
141
+ end
142
+
143
+ def set_buttonstatus(id, state, mode)
144
+ @@cdll.call(:set_buttonstatus, id, state, mode)
145
+ @cache.store("mb_#{id}_#{mode}", state)
146
+ end
147
+
148
+ def set_parameter_multi(param_hash)
149
+ param_hash.each do |(key, val)|
150
+ prop, m2, m3, *rem = key.to_s.split("_")
151
+ if m2.to_i.to_s == m2
152
+ m2 = m2.to_i
153
+ elsif m3.to_i.to_s == m3
154
+ m3 = m3.to_i
155
+ end
156
+
157
+ case prop
158
+ when "strip"
159
+ self.strip[m2].set_multi(val)
160
+ when "bus"
161
+ self.bus[m2].set_multi(val)
162
+ when "button", "mb"
163
+ self.button[m2].set_multi(val)
164
+ when "vban"
165
+ if %w[instream in].include? m2
166
+ self.vban.instream[m3].set_multi(val)
167
+ elsif %w[outstream out].include? m2
168
+ self.vban.outstream[m3].set_multi(val)
169
+ end
170
+ end
171
+ sleep(DELAY)
172
+ end
173
+ end
174
+
175
+ def get_level(type, index)
176
+ c_get = FFI::MemoryPointer.new(:float, SIZE)
177
+ @@cdll.call(:get_level, type, index, c_get)
178
+ c_get.read_float
179
+ end
180
+
181
+ def _get_levels
182
+ [
183
+ (0...(2 * @p_in + 8 * @v_in)).map { |i| get_level(@strip_mode, i) },
184
+ (0...(8 * (@p_out + @v_out))).map { |i| get_level(3, i) }
185
+ ]
186
+ end
187
+
188
+ def get_num_devices(direction)
189
+ unless %w[in out].include? direction
190
+ raise VMRemoteErrors.new("expected in or out")
191
+ end
192
+ if direction == "in"
193
+ val = @@cdll.call(:get_num_indevices)
194
+ else
195
+ val = @@cdll.call(:get_num_outdevices)
196
+ end
197
+ val[0]
198
+ end
199
+
200
+ def get_device_description(index, direction)
201
+ unless %w[in out].include? direction
202
+ raise VMRemoteErrors.new("expected in or out")
203
+ end
204
+ c_type = FFI::MemoryPointer.new(:long, SIZE)
205
+ c_name = FFI::MemoryPointer.new(:string, 256, true)
206
+ c_hwid = FFI::MemoryPointer.new(:string, 256, true)
207
+ if direction == "in"
208
+ @@cdll.call(:get_desc_indevices, index, c_type, c_name, c_hwid)
209
+ else
210
+ @@cdll.call(:get_desc_outdevices, index, c_type, c_name, c_hwid)
211
+ end
212
+ [c_name.read_string, c_type.read_long, c_hwid.read_string]
213
+ end
214
+
215
+ alias_method "set_multi", :set_parameter_multi
216
+ alias_method "apply", :set_parameter_multi
217
+ alias_method "get", :get_parameter
218
+ alias_method "set", :set_parameter
219
+ alias_method "pdirty", :pdirty?
220
+ alias_method "mdirty", :mdirty?
221
+ alias_method "ldirty", :ldirty?
222
+ end
223
+ end
@@ -0,0 +1,107 @@
1
+ require "voicemeeter/iremote"
2
+
3
+ module Voicemeeter
4
+ class Bus < IRemote
5
+ "
6
+ Concrete Bus class
7
+ "
8
+ include Channel_Meta_Functions
9
+
10
+ attr_accessor :mode, :levels
11
+
12
+ def self.make(remote, layout_bus)
13
+ "
14
+ Factory function for Bus classes.
15
+ "
16
+ p_out, v_out = layout_bus.values
17
+ (0...(p_out + v_out)).map do |i|
18
+ i < p_out ? PhysicalBus.new(remote, i) : VirtualBus.new(remote, i)
19
+ end
20
+ end
21
+
22
+ def initialize(remote, i)
23
+ super
24
+ self.make_accessor_bool :mute, :mono, :eq, :sel
25
+ self.make_accessor_float :gain
26
+ self.make_accessor_string :label
27
+
28
+ @mode = BusModes.new(remote, i)
29
+ @levels = BusLevels.new(remote, i)
30
+ end
31
+
32
+ def identifier
33
+ "bus[#{@index}]"
34
+ end
35
+
36
+ def fadeto(target, time)
37
+ self.setter("FadeTo", "(#{target}, #{time})")
38
+ sleep(@remote.delay)
39
+ end
40
+
41
+ def fadeby(change, time)
42
+ self.setter("FadeBy", "(#{change}, #{time})")
43
+ sleep(@remote.delay)
44
+ end
45
+ end
46
+
47
+ class PhysicalBus < Bus
48
+ def initialize(remote, i)
49
+ super
50
+ self.make_reader_only :device, :sr
51
+ end
52
+ end
53
+
54
+ class VirtualBus < Bus
55
+ end
56
+
57
+ class BusModes < IRemote
58
+ include Channel_Meta_Functions
59
+
60
+ def initialize(remote, i)
61
+ super
62
+ self.make_bus_modes :normal,
63
+ :amix,
64
+ :bmix,
65
+ :repeat,
66
+ :composite,
67
+ :tvmix,
68
+ :upmix21,
69
+ :upmix41,
70
+ :upmix61,
71
+ :centeronly,
72
+ :lfeonly,
73
+ :rearonly
74
+ end
75
+
76
+ def identifier
77
+ "bus[#{@index}].mode"
78
+ end
79
+ end
80
+
81
+ class BusLevels < IRemote
82
+ def initialize(remote, i)
83
+ super
84
+ @init = i * 8
85
+ @offset = 8
86
+ end
87
+
88
+ def identifier
89
+ "bus[#{@index}]"
90
+ end
91
+
92
+ def getter(mode)
93
+ if @remote.running
94
+ vals = @remote.cache["bus_level"][@init, @offset]
95
+ else
96
+ vals = (@init...@offset).map { |i| @remote.get_level(mode, i) }
97
+ end
98
+ vals.map { |x| x > 0 ? (20 * Math.log(x, 10)).round(1) : -200.0 }
99
+ end
100
+
101
+ def all
102
+ getter(3)
103
+ end
104
+
105
+ def isdirty? = @remote._bus_comp[@init, @offset].any?
106
+ end
107
+ end
@@ -0,0 +1,25 @@
1
+ require "voicemeeter/iremote"
2
+ require "voicemeeter/meta"
3
+
4
+ module Voicemeeter
5
+ class MacroButton < IRemote
6
+ include MacroButton_Meta_Functions
7
+
8
+ def self.make(remote, num_buttons)
9
+ (0...num_buttons).map { |i| MacroButton.new(remote, i) }
10
+ end
11
+
12
+ def initialize(remote, i)
13
+ super
14
+ self.make_accessor_macrobutton :state, :stateonly, :trigger
15
+ end
16
+
17
+ def getter(mode)
18
+ @remote.get_buttonstatus(@index, mode)
19
+ end
20
+
21
+ def setter(set, mode)
22
+ @remote.set_buttonstatus(@index, set, mode)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,126 @@
1
+ require "ffi"
2
+ require "voicemeeter/inst"
3
+
4
+ module Voicemeeter
5
+ module CBindings
6
+ "
7
+ Creates Ruby bindings to the C DLL
8
+
9
+ Performs other low level tasks
10
+ "
11
+ extend Voicemeeter::InstallationFunctions
12
+ extend FFI::Library
13
+
14
+ private
15
+
16
+ begin
17
+ OS_BITS = FFI::Platform::CPU.downcase == "x64" ? 64 : 32
18
+ VM_PATH = get_vmpath(OS_BITS)
19
+ DLL_NAME = "VoicemeeterRemote#{OS_BITS == 64 ? "64" : ""}.dll"
20
+
21
+ self.vm_dll = VM_PATH.join(DLL_NAME)
22
+ rescue InstallErrors => error
23
+ puts "ERROR: #{error.message}"
24
+ raise
25
+ end
26
+
27
+ ffi_lib @vm_dll
28
+ ffi_convention :stdcall
29
+
30
+ attach_function :vm_login, :VBVMR_Login, [], :long
31
+ attach_function :vm_logout, :VBVMR_Logout, [], :long
32
+ attach_function :vm_runvm, :VBVMR_RunVoicemeeter, [:long], :long
33
+ attach_function :vm_vmtype, :VBVMR_GetVoicemeeterType, [:pointer], :long
34
+ attach_function :vm_vmversion, :VBVMR_GetVoicemeeterVersion, [:pointer], :long
35
+
36
+ attach_function :vm_mdirty, :VBVMR_MacroButton_IsDirty, [], :long
37
+ attach_function :vm_get_buttonstatus,
38
+ :VBVMR_MacroButton_GetStatus,
39
+ %i[long pointer long],
40
+ :long
41
+ attach_function :vm_set_buttonstatus,
42
+ :VBVMR_MacroButton_SetStatus,
43
+ %i[long float long],
44
+ :long
45
+
46
+ attach_function :vm_pdirty, :VBVMR_IsParametersDirty, [], :long
47
+ attach_function :vm_get_parameter_float,
48
+ :VBVMR_GetParameterFloat,
49
+ %i[string pointer],
50
+ :long
51
+ attach_function :vm_set_parameter_float,
52
+ :VBVMR_SetParameterFloat,
53
+ %i[string float],
54
+ :long
55
+
56
+ attach_function :vm_get_parameter_string,
57
+ :VBVMR_GetParameterStringA,
58
+ %i[string pointer],
59
+ :long
60
+ attach_function :vm_set_parameter_string,
61
+ :VBVMR_SetParameterStringA,
62
+ %i[string string],
63
+ :long
64
+
65
+ attach_function :vm_set_parameter_multi,
66
+ :VBVMR_SetParameters,
67
+ [:string],
68
+ :long
69
+
70
+ attach_function :vm_get_level, :VBVMR_GetLevel, %i[long long pointer], :long
71
+
72
+ attach_function :vm_get_num_indevices, :VBVMR_Input_GetDeviceNumber, [], :long
73
+ attach_function :vm_get_desc_indevices,
74
+ :VBVMR_Input_GetDeviceDescA,
75
+ %i[long pointer pointer pointer],
76
+ :long
77
+
78
+ attach_function :vm_get_num_outdevices,
79
+ :VBVMR_Output_GetDeviceNumber,
80
+ [],
81
+ :long
82
+ attach_function :vm_get_desc_outdevices,
83
+ :VBVMR_Output_GetDeviceDescA,
84
+ %i[long pointer pointer pointer],
85
+ :long
86
+
87
+ @@cdll =
88
+ lambda { |func, *args| self.retval = [send("vm_#{func}", *args), func] }
89
+
90
+ def clear_polling = while pdirty? || mdirty?; end
91
+
92
+ def polling(func, **kwargs)
93
+ params = {
94
+ "get_parameter" => kwargs[:name],
95
+ "get_buttonstatus" => "mb_#{kwargs[:id]}_#{kwargs[:mode]}"
96
+ }
97
+ return @cache.delete(params[func]) if @cache.key? params[func]
98
+
99
+ clear_polling if @sync
100
+
101
+ yield
102
+ end
103
+
104
+ def retval=(values)
105
+ " Writer validation for CAPI calls "
106
+ retval, func = *values
107
+ unless %i[get_num_indevices get_num_outdevices].include? func
108
+ raise CAPIErrors.new(retval, func) if retval&.nonzero?
109
+ end
110
+ @retval = retval
111
+ end
112
+
113
+ public
114
+
115
+ def pdirty? = vm_pdirty&.nonzero?
116
+
117
+ def mdirty? = vm_mdirty&.nonzero?
118
+
119
+ def ldirty?
120
+ @strip_buf, @bus_buf = _get_levels
121
+ return(
122
+ !(@cache["strip_level"] == @strip_buf && @cache["bus_level"] == @bus_buf)
123
+ )
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,38 @@
1
+ require "voicemeeter/iremote"
2
+ require "voicemeeter/meta"
3
+
4
+ module Voicemeeter
5
+ class Command < IRemote
6
+ include Commands_Meta_Functions
7
+
8
+ def initialize(remote)
9
+ super
10
+ self.make_action_prop :show, :restart, :shutdown
11
+ self.make_writer_bool :showvbanchat, :lock
12
+ end
13
+
14
+ def identifier
15
+ :command
16
+ end
17
+
18
+ def hide
19
+ self.setter("show", 0)
20
+ end
21
+
22
+ def load(value)
23
+ raise VMRemoteErrors.new("Expected a string") unless value.is_a? String
24
+ self.setter("load", value)
25
+ sleep(0.2)
26
+ end
27
+
28
+ def save(value)
29
+ raise VMRemoteErrors.new("Expected a string") unless value.is_a? String
30
+ self.setter("save", value)
31
+ sleep(0.2)
32
+ end
33
+
34
+ def reset
35
+ @remote.set_config("reset")
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,83 @@
1
+ require "voicemeeter/kinds"
2
+ require "toml"
3
+
4
+ module Voicemeeter
5
+ module Configs
6
+ private
7
+
8
+ @@configs = Hash.new
9
+
10
+ class TOMLStrBuilder
11
+ def initialize(kind)
12
+ @p_in, @v_in = kind[:layout][:strip].values
13
+ @p_out, @v_out = kind[:layout][:bus].values
14
+ @vs_params =
15
+ ["mute = false", "mono = false", "solo = false", "gain = 0.0"] +
16
+ (1..@p_out).map { |i| "A#{i} = false" } +
17
+ (1..@v_out).map { |i| "B#{i} = false" }
18
+
19
+ @ps_params = @vs_params + ["comp = 0.0", "gate = 0.0"]
20
+ @bus_params = ["mono = false", "eq = false", "mute = false"]
21
+ end
22
+
23
+ def build
24
+ "
25
+ Builds a TOML script for the parser
26
+ "
27
+ @ps = (0...@p_in).map { |i| ["[strip_#{i}]"] + @ps_params }
28
+ @ps.map! { |a| a.map { |s| s.gsub("B1 = false", "B1 = true") } }
29
+ @vs =
30
+ (@p_in...(@p_in + @v_in)).map { |i| ["[strip_#{i}]"] + @vs_params }
31
+ @vs.map! { |a| a.map { |s| s.gsub("A1 = false", "A1 = true") } }
32
+
33
+ @b = (0...(@p_out + @v_out)).map { |i| ["[bus_#{i}]"] + @bus_params }
34
+
35
+ [@ps + @vs + @b].join("\n")
36
+ end
37
+ end
38
+
39
+ def parser(data)
40
+ TOML::Parser.new(data).parsed
41
+ end
42
+
43
+ def get_configs(kind_id)
44
+ file_path = File.join(Dir.pwd, "configs", "#{kind_id}")
45
+
46
+ if Dir.exist?(file_path)
47
+ Dir
48
+ .glob(File.join(file_path, "*.toml"))
49
+ .to_h do |toml_file|
50
+ filename = File.basename(toml_file, ".toml")
51
+ puts "loading config #{kind_id}/#{filename} into memory"
52
+ [filename, parser(File.read(toml_file))]
53
+ end
54
+ end
55
+ end
56
+
57
+ def loader
58
+ if @@configs.empty?
59
+ builder = TOMLStrBuilder.new(@kind)
60
+ puts "loading config reset into memory"
61
+ @@configs["reset"] = parser(builder.build)
62
+ configs = get_configs(@kind.name.to_s)
63
+
64
+ @@configs.merge!(configs) unless configs.nil?
65
+ end
66
+ end
67
+
68
+ public
69
+
70
+ def set_config(value)
71
+ loader
72
+ unless @@configs.key? value
73
+ raise VMRemoteErrors.new("No profile with name #{value} was loaded")
74
+ end
75
+
76
+ self.send("set_multi", @@configs[value])
77
+ puts "config #{@kind.name}/#{value} applied!"
78
+ sleep(@delay)
79
+ end
80
+
81
+ alias_method "apply_config", :set_config
82
+ end
83
+ end
@@ -0,0 +1,26 @@
1
+ require "voicemeeter/iremote"
2
+ require "voicemeeter/meta"
3
+
4
+ module Voicemeeter
5
+ class Device
6
+ def initialize(remote)
7
+ @remote = remote
8
+ end
9
+
10
+ def getter(**kwargs)
11
+ return @remote.get_num_devices(kwargs[:direction]) if kwargs[:index].nil?
12
+
13
+ vals = @remote.get_device_description(kwargs[:index], kwargs[:direction])
14
+ types = { 1 => "mme", 3 => "wdm", 4 => "ks", 5 => "asio" }
15
+ { name: vals[0], type: types[vals[1]], id: vals[2] }
16
+ end
17
+
18
+ def ins = getter(direction: "in")
19
+
20
+ def outs = getter(direction: "out")
21
+
22
+ def input(i) = getter(index: i, direction: "in")
23
+
24
+ def output(i) = getter(index: i, direction: "out")
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ module Voicemeeter
2
+ module Errors
3
+ class VMRemoteErrors < StandardError
4
+ end
5
+
6
+ class InstallErrors < VMRemoteErrors
7
+ end
8
+
9
+ class CAPIErrors < VMRemoteErrors
10
+ attr_accessor :value, :func
11
+
12
+ def initialize(value, func)
13
+ self.value = value
14
+ self.func = func
15
+ end
16
+
17
+ def message
18
+ "
19
+ When attempting to run function #{@func} the
20
+ C API returned value #{@value}. See documentation for further info
21
+ "
22
+ end
23
+ end
24
+
25
+ class OutOfBoundsErrors < VMRemoteErrors
26
+ attr_accessor :range
27
+
28
+ def initialize(range)
29
+ self.range = range
30
+ end
31
+
32
+ def message
33
+ if @range.kind_of?(Range)
34
+ "Value error, expected value in range (#{range.first}..#{range.last})"
35
+ elsif @range.kind_of?(Array)
36
+ "Value error, expected one of: #{@range}"
37
+ else
38
+ "Value error, expected #{@range}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end