voicemeeter_api_ruby 4.1.5 → 4.2.0

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