deftones 0.1.0 → 1.0.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -6
  3. data/README.md +5 -0
  4. data/Rakefile +50 -1
  5. data/lib/deftones/analysis/meter.rb +22 -2
  6. data/lib/deftones/component/channel.rb +1 -1
  7. data/lib/deftones/component/compressor.rb +127 -22
  8. data/lib/deftones/component/filter.rb +29 -19
  9. data/lib/deftones/component/merge.rb +14 -0
  10. data/lib/deftones/component/multiband_compressor.rb +1 -1
  11. data/lib/deftones/component/one_pole_filter.rb +10 -3
  12. data/lib/deftones/component/panner.rb +25 -2
  13. data/lib/deftones/component/panner3d.rb +0 -10
  14. data/lib/deftones/component/split.rb +14 -0
  15. data/lib/deftones/context.rb +90 -9
  16. data/lib/deftones/core/audio_block.rb +64 -5
  17. data/lib/deftones/core/audio_node.rb +98 -8
  18. data/lib/deftones/core/gain.rb +0 -8
  19. data/lib/deftones/core/instrument.rb +52 -10
  20. data/lib/deftones/core/param.rb +51 -1
  21. data/lib/deftones/core/signal.rb +79 -28
  22. data/lib/deftones/core/source.rb +71 -11
  23. data/lib/deftones/destination.rb +41 -17
  24. data/lib/deftones/draw.rb +6 -10
  25. data/lib/deftones/dsp/biquad.rb +9 -4
  26. data/lib/deftones/dsp/delay_line.rb +2 -2
  27. data/lib/deftones/dsp/helpers.rb +7 -0
  28. data/lib/deftones/effect/bit_crusher.rb +10 -2
  29. data/lib/deftones/effect/chebyshev.rb +7 -3
  30. data/lib/deftones/effect/distortion.rb +5 -3
  31. data/lib/deftones/effect/feedback_delay.rb +2 -1
  32. data/lib/deftones/effect/oversampling.rb +43 -0
  33. data/lib/deftones/effect/phaser.rb +2 -1
  34. data/lib/deftones/effect/pitch_shift.rb +1 -2
  35. data/lib/deftones/effect/reverb.rb +73 -5
  36. data/lib/deftones/event/callback_behavior.rb +7 -3
  37. data/lib/deftones/event/loop.rb +7 -2
  38. data/lib/deftones/event/part.rb +18 -3
  39. data/lib/deftones/event/pattern.rb +51 -6
  40. data/lib/deftones/event/sequence.rb +19 -5
  41. data/lib/deftones/event/tone_event.rb +7 -2
  42. data/lib/deftones/event/transport.rb +243 -21
  43. data/lib/deftones/instrument/poly_synth.rb +81 -15
  44. data/lib/deftones/instrument/sampler.rb +53 -10
  45. data/lib/deftones/io/buffer.rb +376 -55
  46. data/lib/deftones/io/buffers.rb +28 -4
  47. data/lib/deftones/io/recorder.rb +2 -1
  48. data/lib/deftones/music/frequency.rb +13 -8
  49. data/lib/deftones/music/midi.rb +132 -9
  50. data/lib/deftones/music/note.rb +13 -3
  51. data/lib/deftones/music/time.rb +42 -4
  52. data/lib/deftones/offline_context.rb +194 -17
  53. data/lib/deftones/portaudio_support.rb +68 -9
  54. data/lib/deftones/source/fat_oscillator.rb +28 -9
  55. data/lib/deftones/source/grain_player.rb +49 -2
  56. data/lib/deftones/source/noise.rb +42 -10
  57. data/lib/deftones/source/omni_oscillator.rb +1 -2
  58. data/lib/deftones/source/oscillator.rb +83 -19
  59. data/lib/deftones/source/player.rb +24 -6
  60. data/lib/deftones/source/players.rb +39 -6
  61. data/lib/deftones/source/tone_buffer_source.rb +12 -6
  62. data/lib/deftones/source/tone_oscillator_node.rb +4 -3
  63. data/lib/deftones/source/user_media.rb +83 -10
  64. data/lib/deftones/version.rb +1 -1
  65. data/lib/deftones.rb +108 -31
  66. metadata +3 -44
@@ -6,31 +6,45 @@ module Deftones
6
6
  DEFAULT_BUFFER_SIZE = 256
7
7
  DEFAULT_CHANNELS = 2
8
8
 
9
- attr_reader :sample_rate, :buffer_size, :channels, :stream_error, :latency_hint, :look_ahead
9
+ attr_reader :buffer_size, :channels, :draw, :latency_hint, :look_ahead, :output_device_id,
10
+ :output_device_label, :sample_rate, :stream_error, :stream_status_flags, :transport
11
+ attr_accessor :on_stream_error
10
12
 
11
13
  def initialize(sample_rate: DEFAULT_SAMPLE_RATE, buffer_size: DEFAULT_BUFFER_SIZE, channels: DEFAULT_CHANNELS,
12
- realtime_backend: nil, autostart: true, latency_hint: "interactive", look_ahead: nil)
14
+ realtime_backend: nil, autostart: true, latency_hint: "interactive", look_ahead: nil,
15
+ transport: nil, draw: nil, on_stream_error: nil, stream_error_mode: :abort,
16
+ output_device_id: nil, output_device_label: nil)
13
17
  @sample_rate = sample_rate
14
18
  @buffer_size = buffer_size
15
19
  @channels = channels
20
+ @transport = transport || Event::Transport.new(clock: self)
21
+ @draw = draw || Draw.new
16
22
  @realtime_backend = realtime_backend
17
23
  @autostart = autostart
18
24
  @latency_hint = latency_hint
19
25
  @look_ahead = look_ahead || (buffer_size.to_f / sample_rate)
26
+ @output_device_id = output_device_id
27
+ @output_device_label = output_device_label
28
+ @on_stream_error = on_stream_error
29
+ @stream_error_mode = normalize_stream_error_mode(stream_error_mode)
20
30
  @output = Core::Gain.new(context: self, gain: 1.0)
21
31
  @running = false
22
32
  @closed = false
23
33
  @started_at = monotonic_time
24
34
  @stream = nil
25
35
  @rendered_frames = 0
36
+ @scheduler_position = 0.0
26
37
  @stream_error = nil
38
+ @stream_status_flags = []
27
39
  end
28
40
 
29
41
  def start(use_realtime: true)
30
42
  @closed = false
31
43
  @started_at = monotonic_time
32
44
  @rendered_frames = 0
45
+ @scheduler_position = 0.0
33
46
  @stream_error = nil
47
+ @stream_status_flags.clear
34
48
  @running = true
35
49
  start_realtime_stream if use_realtime
36
50
  self
@@ -101,11 +115,38 @@ module Deftones
101
115
  buffer_size.to_f / sample_rate
102
116
  end
103
117
 
118
+ def reset!
119
+ stop
120
+ @transport = Event::Transport.new(clock: self)
121
+ @draw = Draw.new
122
+ @stream_error = nil
123
+ @stream_status_flags.clear
124
+ @rendered_frames = 0
125
+ @scheduler_position = 0.0
126
+ self
127
+ end
128
+
104
129
  alias rawContext raw_context
105
130
  alias sampleTime sample_time
106
131
  alias blockTime block_time
107
132
  alias latencyHint latency_hint
108
133
  alias lookAhead look_ahead
134
+ alias outputDeviceId output_device_id
135
+ alias outputDeviceLabel output_device_label
136
+ alias onStreamError on_stream_error
137
+ alias onStreamError= on_stream_error=
138
+
139
+ def stream_error_mode
140
+ @stream_error_mode
141
+ end
142
+
143
+ def stream_error_mode=(value)
144
+ @stream_error_mode = normalize_stream_error_mode(value)
145
+ end
146
+
147
+ alias streamErrorMode stream_error_mode
148
+ alias streamErrorMode= stream_error_mode=
149
+ alias streamStatusFlags stream_status_flags
109
150
 
110
151
  private
111
152
 
@@ -138,12 +179,42 @@ module Deftones
138
179
 
139
180
  def pull_realtime_samples(frames)
140
181
  start_frame = @rendered_frames
182
+ next_frame = start_frame + frames
183
+ window_start = start_frame.to_f / sample_rate
184
+ window_end = next_frame.to_f / sample_rate
185
+ scheduler_end = window_end + look_ahead
186
+ scheduler_start = [@scheduler_position, window_start].max
187
+ @transport.prepare_render_window(scheduler_start, scheduler_end)
188
+ Deftones.transport.prepare_render_window(scheduler_start, scheduler_end) unless Deftones.transport.equal?(@transport)
141
189
  chunk = render_block_frames(frames, start_frame).fit_channels(@channels)
142
- @rendered_frames += frames
143
- Deftones.draw.advance_to(@rendered_frames.to_f / sample_rate)
190
+ @rendered_frames = next_frame
191
+ @scheduler_position = scheduler_end
192
+ @draw.advance_to(scheduler_end)
193
+ Deftones.draw.advance_to(scheduler_end) unless Deftones.draw.equal?(@draw)
144
194
  chunk.interleaved
145
195
  end
146
196
 
197
+ def handle_stream_error(error)
198
+ @stream_error ||= error
199
+ @on_stream_error&.call(error)
200
+ @stream_error_mode == :continue ? :continue : :abort
201
+ end
202
+
203
+ def record_stream_status_flags(status_flags)
204
+ return self if status_flags.nil?
205
+ return self if status_flags.respond_to?(:zero?) && status_flags.zero?
206
+
207
+ @stream_status_flags << status_flags
208
+ self
209
+ end
210
+
211
+ def normalize_stream_error_mode(value)
212
+ normalized = value.to_sym
213
+ return normalized if %i[abort continue].include?(normalized)
214
+
215
+ raise ArgumentError, "Unsupported stream error mode: #{value}"
216
+ end
217
+
147
218
  def monotonic_time
148
219
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
149
220
  end
@@ -152,6 +223,7 @@ module Deftones
152
223
  def initialize(context:)
153
224
  @context = context
154
225
  @stream = nil
226
+ @silence_cache = {}
155
227
  end
156
228
 
157
229
  def start
@@ -190,7 +262,12 @@ module Deftones
190
262
  def open_stream
191
263
  Deftones::PortAudioSupport.acquire!
192
264
  @stream = PortAudio::Stream.new(
193
- output: Deftones::PortAudioSupport.output_parameters(@context.channels),
265
+ output: Deftones::PortAudioSupport.output_parameters(
266
+ @context.channels,
267
+ device_id: @context.output_device_id,
268
+ label: @context.output_device_label,
269
+ sample_rate: @context.sample_rate
270
+ ),
194
271
  sample_rate: @context.sample_rate.to_f,
195
272
  frames_per_buffer: @context.buffer_size,
196
273
  &method(:process)
@@ -200,13 +277,17 @@ module Deftones
200
277
  raise
201
278
  end
202
279
 
203
- def process(_input, output, frame_count, _time_info, _status_flags, _user_data)
280
+ def process(_input, output, frame_count, _time_info, status_flags, _user_data)
281
+ @context.send(:record_stream_status_flags, status_flags)
204
282
  output.write_array_of_float(@context.send(:pull_realtime_samples, frame_count))
205
283
  :continue
206
284
  rescue StandardError => error
207
- @context.instance_variable_set(:@stream_error, error) if @context.stream_error.nil?
208
- output.write_array_of_float(Array.new(frame_count * @context.channels, 0.0)) unless output.null?
209
- :abort
285
+ output.write_array_of_float(silence_for(frame_count)) unless output.null?
286
+ @context.send(:handle_stream_error, error)
287
+ end
288
+
289
+ def silence_for(frame_count)
290
+ @silence_cache[frame_count] ||= Array.new(frame_count * @context.channels, 0.0).freeze
210
291
  end
211
292
  end
212
293
  end
@@ -19,6 +19,23 @@ module Deftones
19
19
  from_channel_data(Array.new([channels.to_i, 1].max) { normalized.dup })
20
20
  end
21
21
 
22
+ def self.from_interleaved(samples, channels:)
23
+ channel_count = [channels.to_i, 1].max
24
+ normalized = Array(samples).map(&:to_f)
25
+ frame_count = (normalized.length.to_f / channel_count).ceil
26
+ from_channel_data(
27
+ Array.new(channel_count) do |channel_index|
28
+ Array.new(frame_count) do |frame_index|
29
+ normalized[(frame_index * channel_count) + channel_index] || 0.0
30
+ end
31
+ end
32
+ )
33
+ end
34
+
35
+ def self.from_packed_float32(payload, channels:)
36
+ from_interleaved(payload.unpack("e*"), channels: channels)
37
+ end
38
+
22
39
  def initialize(channel_data)
23
40
  @channel_data = channel_data
24
41
  end
@@ -52,31 +69,73 @@ module Deftones
52
69
  end
53
70
  end
54
71
 
72
+ def packed_float32
73
+ interleaved.pack("e*")
74
+ end
75
+
76
+ def packed_float64
77
+ interleaved.pack("E*")
78
+ end
79
+
55
80
  def channel(index)
56
81
  @channel_data[index] || Array.new(num_frames, 0.0)
57
82
  end
58
83
 
59
- def fit_channels(target_channels)
84
+ def fit_channels(target_channels, downmix: :average, upmix: :wrap)
60
85
  target = [target_channels.to_i, 1].max
61
86
  return dup if target == channels
62
- return self.class.from_channel_data([mono]) if target == 1
87
+ return self.class.from_channel_data([downmixed_channel(downmix)]) if target == 1
63
88
 
64
89
  if channels == 1
65
90
  return self.class.from_channel_data(Array.new(target) { @channel_data.first.dup })
66
91
  end
67
92
 
68
- self.class.from_channel_data(Array.new(target) { |index| channel(index % channels).dup })
93
+ self.class.from_channel_data(Array.new(target) { |index| upmixed_channel(index, upmix) })
69
94
  end
70
95
 
71
- def mix!(other)
96
+ def mix!(other, headroom: :sum, gain: 1.0)
72
97
  incoming = other.fit_channels(channels)
73
98
  channels.times do |channel_index|
74
99
  num_frames.times do |frame_index|
75
- @channel_data[channel_index][frame_index] += incoming.channel_data[channel_index][frame_index]
100
+ mixed = @channel_data[channel_index][frame_index] + (incoming.channel_data[channel_index][frame_index] * gain)
101
+ @channel_data[channel_index][frame_index] = apply_headroom(mixed, headroom)
76
102
  end
77
103
  end
78
104
  self
79
105
  end
106
+
107
+ private
108
+
109
+ def downmixed_channel(policy)
110
+ case policy
111
+ when :average then mono
112
+ when :sum
113
+ Array.new(num_frames) { |frame_index| @channel_data.sum { |channel| channel[frame_index] } }
114
+ when :first
115
+ channel(0).dup
116
+ else
117
+ raise ArgumentError, "Unsupported downmix policy: #{policy}"
118
+ end
119
+ end
120
+
121
+ def upmixed_channel(index, policy)
122
+ case policy
123
+ when :wrap then channel(index % channels).dup
124
+ when :silence then index < channels ? channel(index).dup : Array.new(num_frames, 0.0)
125
+ when :duplicate then channel([index, channels - 1].min).dup
126
+ else
127
+ raise ArgumentError, "Unsupported upmix policy: #{policy}"
128
+ end
129
+ end
130
+
131
+ def apply_headroom(sample, policy)
132
+ case policy
133
+ when :sum then sample
134
+ when :clamp then sample.clamp(-1.0, 1.0)
135
+ else
136
+ raise ArgumentError, "Unsupported headroom policy: #{policy}"
137
+ end
138
+ end
80
139
  end
81
140
  end
82
141
  end
@@ -18,12 +18,14 @@ module Deftones
18
18
  end
19
19
 
20
20
  def connect(destination, output_index: 0, input_index: 0)
21
- _ = output_index
22
- _ = input_index
23
21
  raise ArgumentError, "destination is required" if destination.nil?
24
22
 
25
- destination_node = destination.respond_to?(:input) ? destination.input : destination
26
- output.attach_destination(destination_node)
23
+ source_node = output_for_connection(output_index)
24
+ destination_node = destination_for_connection(destination, input_index)
25
+ validate_connectable!(source_node, destination_node)
26
+ raise ArgumentError, "connection would create a cycle" if destination_node.send(:reaches_node?, source_node)
27
+
28
+ source_node.attach_destination(destination_node)
27
29
  self
28
30
  end
29
31
 
@@ -52,6 +54,21 @@ module Deftones
52
54
  self
53
55
  end
54
56
 
57
+ def inputs
58
+ @sources.dup
59
+ end
60
+
61
+ def outputs
62
+ @destinations.dup
63
+ end
64
+
65
+ def connected?(destination = nil)
66
+ return @destinations.any? if destination.nil?
67
+
68
+ destination_node = destination.respond_to?(:input) ? destination.input : destination
69
+ @destinations.include?(destination_node)
70
+ end
71
+
55
72
  def to_output
56
73
  connect(context.output)
57
74
  self
@@ -121,7 +138,10 @@ module Deftones
121
138
  1
122
139
  end
123
140
 
124
- def set(**params)
141
+ def set(strict: false, **params)
142
+ unknown = params.keys.reject { |key| respond_to?(:"#{key}=") }
143
+ raise ArgumentError, "Unknown parameter(s): #{unknown.join(', ')}" if strict && unknown.any?
144
+
125
145
  params.each do |key, value|
126
146
  writer = :"#{key}="
127
147
  public_send(writer, value) if respond_to?(writer)
@@ -129,11 +149,19 @@ module Deftones
129
149
  self
130
150
  end
131
151
 
132
- def get(*keys)
133
- keys.flatten.each_with_object({}) do |key, values|
152
+ def get(*keys, strict: false)
153
+ unknown = []
154
+ values = keys.flatten.each_with_object({}) do |key, collected|
134
155
  reader = key.to_sym
135
- values[reader] = public_send(reader) if respond_to?(reader)
156
+ if respond_to?(reader)
157
+ collected[reader] = public_send(reader)
158
+ else
159
+ unknown << reader
160
+ end
136
161
  end
162
+ raise ArgumentError, "Unknown parameter(s): #{unknown.join(', ')}" if strict && unknown.any?
163
+
164
+ values
137
165
  end
138
166
 
139
167
  def name
@@ -157,6 +185,7 @@ module Deftones
157
185
  alias channelInterpretation channel_interpretation
158
186
  alias numberOfInputs number_of_inputs
159
187
  alias numberOfOutputs number_of_outputs
188
+ alias connected connected?
160
189
  alias toString to_s
161
190
 
162
191
  def dispose
@@ -177,7 +206,21 @@ module Deftones
177
206
 
178
207
  protected
179
208
 
209
+ def output_for_index(index)
210
+ raise_connection_index_error!(:output_index, index, number_of_outputs) unless index.zero?
211
+
212
+ output
213
+ end
214
+
215
+ def input_for_index(index)
216
+ raise_connection_index_error!(:input_index, index, number_of_inputs) unless index.zero?
217
+
218
+ input
219
+ end
220
+
180
221
  def render_block(num_frames, start_frame = 0, cache = {})
222
+ raise Deftones::Error, "cannot render disposed node: #{name}" if disposed?
223
+
181
224
  cache_key = [object_id, :block, start_frame, num_frames]
182
225
  return cache.fetch(cache_key).dup if cache.key?(cache_key)
183
226
 
@@ -257,6 +300,53 @@ module Deftones
257
300
  def default_output_channels
258
301
  default_input_channels
259
302
  end
303
+
304
+ def reaches_node?(target, visited = {})
305
+ return true if equal?(target)
306
+ return false if visited[object_id]
307
+
308
+ visited[object_id] = true
309
+ @destinations.any? { |destination| destination.send(:reaches_node?, target, visited) }
310
+ end
311
+
312
+ def raise_connection_index_error!(name, index, count)
313
+ raise ArgumentError, "#{name} #{index} is out of range for #{count} #{count == 1 ? 'port' : 'ports'}"
314
+ end
315
+
316
+ private
317
+
318
+ def output_for_connection(index)
319
+ normalized_index = normalize_connection_index(index, :output_index)
320
+ output_for_index(normalized_index)
321
+ end
322
+
323
+ def destination_for_connection(destination, index)
324
+ normalized_index = normalize_connection_index(index, :input_index)
325
+ if destination.respond_to?(:input_for_index)
326
+ destination.input_for_index(normalized_index)
327
+ else
328
+ destination_node = destination.respond_to?(:input) ? destination.input : destination
329
+ validate_connection_index!(normalized_index, destination_node.number_of_inputs, :input_index)
330
+ destination_node
331
+ end
332
+ end
333
+
334
+ def normalize_connection_index(index, name)
335
+ Integer(index).tap do |normalized|
336
+ raise ArgumentError, "#{name} must be greater than or equal to 0" if normalized.negative?
337
+ end
338
+ rescue ArgumentError, TypeError
339
+ raise ArgumentError, "#{name} must be an integer"
340
+ end
341
+
342
+ def validate_connection_index!(index, count, name)
343
+ raise_connection_index_error!(name, index, count) if index >= count
344
+ end
345
+
346
+ def validate_connectable!(source_node, destination_node)
347
+ raise Deftones::Error, "cannot connect disposed source node" if source_node.disposed?
348
+ raise Deftones::Error, "cannot connect disposed destination node" if destination_node.disposed?
349
+ end
260
350
  end
261
351
  end
262
352
  end
@@ -14,14 +14,6 @@ module Deftones
14
14
  @gain.value = value
15
15
  end
16
16
 
17
- def process(input_buffer, num_frames, start_frame, _cache)
18
- gain_values = @gain.process(num_frames, start_frame)
19
-
20
- Array.new(num_frames) do |index|
21
- input_buffer[index] * gain_values[index]
22
- end
23
- end
24
-
25
17
  def multichannel_process?
26
18
  true
27
19
  end
@@ -5,7 +5,7 @@ module Deftones
5
5
  class Instrument < AudioNode
6
6
  class VolumeProxy
7
7
  attr_reader :instrument
8
- attr_accessor :value
8
+ attr_reader :value
9
9
 
10
10
  def initialize(instrument, value: 0.0)
11
11
  @instrument = instrument
@@ -17,17 +17,48 @@ module Deftones
17
17
  instrument.apply_volume!
18
18
  end
19
19
 
20
- def ramp_to(target_value, _duration = nil)
21
- self.value = target_value
20
+ def ramp_to(target_value, duration = nil)
21
+ return assign_immediately(target_value) if duration.nil?
22
+
23
+ resolved_duration = Deftones::Music::Time.parse(duration)
24
+ return assign_immediately(target_value) if resolved_duration <= 0.0
25
+
26
+ @value = target_value.to_f
27
+ instrument.output.gain.linear_ramp_to_value_at_time(
28
+ instrument.mute? ? 0.0 : Deftones.db_to_gain(@value),
29
+ instrument.context.current_time + resolved_duration
30
+ )
22
31
  self
23
32
  end
24
33
 
25
- alias linear_ramp_to ramp_to
26
- alias exponential_ramp_to ramp_to
34
+ def set_value_at_time(target_value, time)
35
+ @value = target_value.to_f
36
+ instrument.output.gain.set_value_at_time(instrument.mute? ? 0.0 : Deftones.db_to_gain(@value), time)
37
+ self
38
+ end
39
+
40
+ def linear_ramp_to(target_value, duration = nil)
41
+ ramp_to(target_value, duration)
42
+ end
43
+
44
+ def exponential_ramp_to(target_value, duration = nil)
45
+ ramp_to(target_value, duration)
46
+ end
47
+
48
+ alias setValueAtTime set_value_at_time
49
+ alias linearRampTo linear_ramp_to
50
+ alias exponentialRampTo exponential_ramp_to
51
+
52
+ private
53
+
54
+ def assign_immediately(target_value)
55
+ self.value = target_value
56
+ self
57
+ end
27
58
  end
28
59
 
29
60
  attr_reader :output, :volume
30
- attr_accessor :mute
61
+ attr_reader :mute
31
62
 
32
63
  def initialize(context: Deftones.context)
33
64
  super(context: context)
@@ -58,7 +89,10 @@ module Deftones
58
89
  @mute
59
90
  end
60
91
 
61
- def set(**params)
92
+ def set(strict: false, **params)
93
+ unknown = params.keys.reject { |key| respond_to?(:"#{key}=") }
94
+ raise ArgumentError, "Unknown parameter(s): #{unknown.join(', ')}" if strict && unknown.any?
95
+
62
96
  params.each do |key, value|
63
97
  writer = :"#{key}="
64
98
  public_send(writer, value) if respond_to?(writer)
@@ -66,12 +100,20 @@ module Deftones
66
100
  self
67
101
  end
68
102
 
69
- def get(*keys)
103
+ def get(*keys, strict: false)
70
104
  requested = keys.flatten
71
- requested.each_with_object({}) do |key, values|
105
+ unknown = []
106
+ values = requested.each_with_object({}) do |key, collected|
72
107
  reader = key.to_sym
73
- values[reader] = public_send(reader) if respond_to?(reader)
108
+ if respond_to?(reader)
109
+ collected[reader] = public_send(reader)
110
+ else
111
+ unknown << reader
112
+ end
74
113
  end
114
+ raise ArgumentError, "Unknown parameter(s): #{unknown.join(', ')}" if strict && unknown.any?
115
+
116
+ values
75
117
  end
76
118
 
77
119
  def release_all(time = nil)
@@ -3,11 +3,13 @@
3
3
  module Deftones
4
4
  module Core
5
5
  class Param < Signal
6
- attr_reader :lfo
6
+ attr_reader :lfo, :audio_source, :modulation_amount
7
7
 
8
8
  def initialize(**options)
9
9
  super
10
10
  @lfo = nil
11
+ @audio_source = nil
12
+ @modulation_amount = 1.0
11
13
  end
12
14
 
13
15
  def set_param(param)
@@ -25,7 +27,55 @@ module Deftones
25
27
  @lfo = source
26
28
  end
27
29
 
30
+ def connect_audio(source, amount: 1.0)
31
+ raise ArgumentError, "audio source is required" if source.nil?
32
+ raise ArgumentError, "audio source must render or process samples" unless modulation_source?(source)
33
+
34
+ @audio_source = source
35
+ @modulation_amount = amount.to_f
36
+ self
37
+ end
38
+
39
+ def disconnect_audio(source = nil)
40
+ return self if source && source != @audio_source
41
+
42
+ @audio_source = nil
43
+ @modulation_amount = 1.0
44
+ self
45
+ end
46
+
47
+ def audio_rate?
48
+ !@audio_source.nil?
49
+ end
50
+
51
+ def process(num_frames, start_frame = 0)
52
+ values = super
53
+ return values unless @audio_source
54
+
55
+ modulation = modulation_samples(num_frames, start_frame)
56
+ values.zip(modulation).map { |base, sample| base + (sample.to_f * @modulation_amount) }
57
+ end
58
+
28
59
  alias setParam set_param
60
+ alias connectAudio connect_audio
61
+ alias disconnectAudio disconnect_audio
62
+ alias audioRate audio_rate?
63
+
64
+ private
65
+
66
+ def modulation_source?(source)
67
+ source.respond_to?(:values) || source.respond_to?(:process) || source.respond_to?(:render)
68
+ end
69
+
70
+ def modulation_samples(num_frames, start_frame)
71
+ if @audio_source.respond_to?(:values)
72
+ @audio_source.values(num_frames, start_frame)
73
+ elsif @audio_source.respond_to?(:render)
74
+ @audio_source.render(num_frames, start_frame)
75
+ else
76
+ @audio_source.process(num_frames, start_frame)
77
+ end
78
+ end
29
79
  end
30
80
  end
31
81
  end