wavify 0.1.0 → 0.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/README.md +204 -22
  4. data/exe/wavify +6 -0
  5. data/lib/wavify/adapters.rb +81 -0
  6. data/lib/wavify/audio.rb +1066 -120
  7. data/lib/wavify/cli.rb +237 -0
  8. data/lib/wavify/codecs/aiff.rb +388 -87
  9. data/lib/wavify/codecs/base.rb +98 -5
  10. data/lib/wavify/codecs/flac.rb +660 -170
  11. data/lib/wavify/codecs/ogg_vorbis.rb +515 -155
  12. data/lib/wavify/codecs/raw.rb +235 -45
  13. data/lib/wavify/codecs/registry.rb +272 -10
  14. data/lib/wavify/codecs/wav.rb +453 -74
  15. data/lib/wavify/core/duration.rb +54 -2
  16. data/lib/wavify/core/format.rb +96 -11
  17. data/lib/wavify/core/sample_buffer.rb +648 -60
  18. data/lib/wavify/core/stream.rb +590 -34
  19. data/lib/wavify/dsl.rb +712 -100
  20. data/lib/wavify/dsp/automation.rb +109 -0
  21. data/lib/wavify/dsp/effects/auto_pan.rb +71 -0
  22. data/lib/wavify/dsp/effects/bitcrusher.rb +74 -0
  23. data/lib/wavify/dsp/effects/chorus.rb +15 -4
  24. data/lib/wavify/dsp/effects/compressor.rb +133 -23
  25. data/lib/wavify/dsp/effects/delay.rb +46 -1
  26. data/lib/wavify/dsp/effects/distortion.rb +3 -2
  27. data/lib/wavify/dsp/effects/effect_base.rb +69 -2
  28. data/lib/wavify/dsp/effects/effect_chain.rb +94 -0
  29. data/lib/wavify/dsp/effects/envelope_controlled_effect.rb +93 -0
  30. data/lib/wavify/dsp/effects/eq.rb +102 -0
  31. data/lib/wavify/dsp/effects/expander.rb +90 -0
  32. data/lib/wavify/dsp/effects/flanger.rb +112 -0
  33. data/lib/wavify/dsp/effects/limiter.rb +259 -0
  34. data/lib/wavify/dsp/effects/mastering_chain.rb +31 -0
  35. data/lib/wavify/dsp/effects/noise_gate.rb +72 -0
  36. data/lib/wavify/dsp/effects/phaser.rb +112 -0
  37. data/lib/wavify/dsp/effects/podcast_chain.rb +32 -0
  38. data/lib/wavify/dsp/effects/reverb.rb +100 -14
  39. data/lib/wavify/dsp/effects/soft_limiter.rb +55 -0
  40. data/lib/wavify/dsp/effects/stereo_widener.rb +73 -0
  41. data/lib/wavify/dsp/effects/tremolo.rb +66 -0
  42. data/lib/wavify/dsp/effects/vibrato.rb +102 -0
  43. data/lib/wavify/dsp/effects.rb +106 -0
  44. data/lib/wavify/dsp/envelope.rb +90 -19
  45. data/lib/wavify/dsp/filter.rb +149 -8
  46. data/lib/wavify/dsp/headroom.rb +57 -0
  47. data/lib/wavify/dsp/lfo.rb +65 -0
  48. data/lib/wavify/dsp/loudness_meter.rb +281 -0
  49. data/lib/wavify/dsp/oscillator.rb +261 -22
  50. data/lib/wavify/dsp/processor.rb +90 -0
  51. data/lib/wavify/errors.rb +2 -2
  52. data/lib/wavify/sequencer/engine.rb +405 -89
  53. data/lib/wavify/sequencer/note_sequence.rb +45 -7
  54. data/lib/wavify/sequencer/pattern.rb +121 -12
  55. data/lib/wavify/sequencer/track.rb +149 -10
  56. data/lib/wavify/version.rb +1 -1
  57. data/lib/wavify.rb +18 -0
  58. data/sig/audio.rbs +86 -0
  59. data/sig/codecs.rbs +77 -0
  60. data/sig/core.rbs +108 -0
  61. data/sig/dsl.rbs +32 -0
  62. data/sig/dsp.rbs +87 -0
  63. data/sig/effects.rbs +129 -0
  64. data/sig/public_api.yml +111 -0
  65. data/sig/sequencer.rbs +126 -0
  66. data/sig/stream.rbs +30 -0
  67. data/sig/wavify.rbs +24 -0
  68. metadata +44 -58
  69. data/.serena/.gitignore +0 -1
  70. data/.serena/memories/project_overview.md +0 -5
  71. data/.serena/memories/style_and_completion.md +0 -5
  72. data/.serena/memories/suggested_commands.md +0 -11
  73. data/.serena/project.yml +0 -126
  74. data/.simplecov +0 -18
  75. data/.yardopts +0 -4
  76. data/Rakefile +0 -190
  77. data/benchmarks/README.md +0 -46
  78. data/benchmarks/benchmark_helper.rb +0 -112
  79. data/benchmarks/dsp_effects_benchmark.rb +0 -46
  80. data/benchmarks/flac_benchmark.rb +0 -74
  81. data/benchmarks/streaming_memory_benchmark.rb +0 -94
  82. data/benchmarks/wav_io_benchmark.rb +0 -110
  83. data/examples/audio_processing.rb +0 -73
  84. data/examples/cinematic_transition.rb +0 -118
  85. data/examples/drum_machine.rb +0 -74
  86. data/examples/format_convert.rb +0 -81
  87. data/examples/hybrid_arrangement.rb +0 -165
  88. data/examples/streaming_master_chain.rb +0 -129
  89. data/examples/synth_pad.rb +0 -42
  90. data/tools/fixture_writer.rb +0 -85
@@ -5,58 +5,162 @@ module Wavify
5
5
  # Oscillator and noise source generator.
6
6
  class Oscillator
7
7
  # Supported waveform symbols.
8
- WAVEFORMS = %i[sine square sawtooth triangle white_noise pink_noise].freeze
8
+ WAVEFORMS = %i[sine square sawtooth triangle pulse white_noise pink_noise].freeze
9
+ # Sample count in the interpolated sine wavetable.
10
+ SINE_TABLE_SIZE = 2_048
11
+ # Shared sine cycle used by the eager generation hot path.
12
+ SINE_WAVETABLE = Array.new(SINE_TABLE_SIZE) do |index|
13
+ Math.sin(2.0 * Math::PI * index / SINE_TABLE_SIZE)
14
+ end.freeze
15
+ # Sample count in each band-limited triangle wavetable.
16
+ TRIANGLE_TABLE_SIZE = 2_048
17
+ # Highest odd harmonic considered when building triangle tables.
18
+ TRIANGLE_MAX_HARMONIC = 255
19
+ # Maximum number of sample-rate/frequency-specific tables retained.
20
+ TRIANGLE_TABLE_CACHE_LIMIT = 8
21
+ # Maximum duration accepted by one eager generation call.
22
+ MAX_DURATION_SECONDS = 3_600.0
23
+ # Maximum number of frames allocated by one eager generation call.
24
+ MAX_GENERATE_FRAMES = 50_000_000
25
+ # Absolute input guard before output-format Nyquist validation.
26
+ MAX_FREQUENCY = 1_000_000.0
27
+ # Maximum number of detuned oscillator voices.
28
+ MAX_UNISON = 64
9
29
 
10
- def initialize(waveform:, frequency:, amplitude: 1.0, phase: 0.0, random: Random.new)
30
+ # @param phase [Numeric] initial phase in cycles (`0.0..1.0` wraps)
31
+ def initialize(waveform:, frequency:, amplitude: 1.0, phase: 0.0, pulse_width: 0.5, detune: 0.0, unison: 1,
32
+ random: Random.new)
11
33
  @waveform = validate_waveform!(waveform)
12
34
  @frequency = validate_frequency!(frequency)
13
35
  @amplitude = validate_amplitude!(amplitude)
14
- @phase = phase.to_f
36
+ @initial_phase = validate_phase!(phase)
37
+ @voice_phases = nil
38
+ @voice_frequencies = nil
39
+ @sample_rate = nil
40
+ @pulse_width = validate_pulse_width!(pulse_width)
41
+ @detune = validate_detune!(detune)
42
+ @unison = validate_unison!(unison)
15
43
  @random = random
44
+ @triangle_tables = {}
16
45
  reset_pink_noise!
17
46
  end
18
47
 
19
- # Generates a finite sample buffer in the requested format.
48
+ def reset_phase(phase = @initial_phase)
49
+ @initial_phase = validate_phase!(phase)
50
+ @voice_phases = nil
51
+ @voice_frequencies = nil
52
+ @sample_rate = nil
53
+ reset_pink_noise!
54
+ self
55
+ end
56
+
57
+ # Generates the next finite sample buffer in the requested format.
58
+ # Calls are stateful and continue oscillator phase. Call {#reset_phase}
59
+ # before changing sample rate or when repeatable output is required.
20
60
  #
21
61
  # @param duration_seconds [Numeric]
22
62
  # @param format [Wavify::Core::Format]
23
63
  # @return [Wavify::Core::SampleBuffer]
24
64
  def generate(duration_seconds, format:)
25
65
  validate_format!(format)
26
- unless duration_seconds.is_a?(Numeric) && duration_seconds >= 0
27
- raise InvalidParameterError, "duration_seconds must be a non-negative Numeric"
66
+ prepare_sample_rate!(format.sample_rate)
67
+ unless duration_seconds.is_a?(Numeric) && duration_seconds.respond_to?(:finite?) && duration_seconds.finite? &&
68
+ duration_seconds.between?(0.0, MAX_DURATION_SECONDS)
69
+ raise InvalidParameterError, "duration_seconds must be a finite Numeric in 0.0..#{MAX_DURATION_SECONDS}"
28
70
  end
29
71
 
30
72
  sample_frames = (duration_seconds.to_f * format.sample_rate).round
73
+ if sample_frames > MAX_GENERATE_FRAMES
74
+ raise InvalidParameterError, "generation is limited to #{MAX_GENERATE_FRAMES} sample frames"
75
+ end
31
76
  samples = Array.new(sample_frames * format.channels)
32
77
 
33
- sample_frames.times do |frame_index|
34
- value = sample_at(frame_index, format.sample_rate)
35
- base_index = frame_index * format.channels
36
- format.channels.times { |channel| samples[base_index + channel] = value }
78
+ if @waveform == :sine
79
+ generate_sine_samples!(samples, sample_frames, format.channels, format.sample_rate)
80
+ else
81
+ generate_samples!(samples, sample_frames, format.channels, format.sample_rate)
37
82
  end
38
83
 
39
84
  Core::SampleBuffer.new(samples, format)
40
85
  end
41
86
 
42
- # Returns an infinite enumerator of mono sample values.
87
+ # Returns a stateful infinite enumerator of mono sample values.
43
88
  #
44
89
  # @param format [Wavify::Core::Format]
45
90
  # @return [Enumerator<Float>]
46
91
  def each_sample(format:)
47
92
  validate_format!(format)
93
+ prepare_sample_rate!(format.sample_rate)
48
94
 
49
95
  Enumerator.new do |yielder|
50
- index = 0
51
96
  loop do
52
- yielder << sample_at(index, format.sample_rate)
53
- index += 1
97
+ value = noise_waveform? ? scaled_noise_sample : next_periodic_sample(format.sample_rate)
98
+ yielder << value
54
99
  end
55
100
  end
56
101
  end
57
102
 
58
103
  private
59
104
 
105
+ def generate_sine_samples!(samples, sample_frames, channels, sample_rate)
106
+ if @voice_phases.one?
107
+ generate_single_sine_voice!(samples, sample_frames, channels, sample_rate)
108
+ return
109
+ end
110
+
111
+ phase_steps = @voice_frequencies.map { |frequency| frequency / sample_rate.to_f }
112
+ voice_count = @voice_phases.length
113
+ sample_frames.times do |frame_index|
114
+ raw = 0.0
115
+ voice = 0
116
+ while voice < voice_count
117
+ phase = @voice_phases.fetch(voice)
118
+ raw += interpolated_sine(phase)
119
+ next_phase = phase + phase_steps.fetch(voice)
120
+ @voice_phases[voice] = next_phase >= 1.0 ? next_phase - 1.0 : next_phase
121
+ voice += 1
122
+ end
123
+ write_frame!(samples, frame_index, channels, (raw / voice_count) * @amplitude)
124
+ end
125
+ end
126
+
127
+ def generate_single_sine_voice!(samples, sample_frames, channels, sample_rate)
128
+ phase = @voice_phases.fetch(0)
129
+ phase_step = @voice_frequencies.fetch(0) / sample_rate.to_f
130
+ sample_frames.times do |frame_index|
131
+ write_frame!(samples, frame_index, channels, interpolated_sine(phase) * @amplitude)
132
+ phase += phase_step
133
+ phase -= 1.0 if phase >= 1.0
134
+ end
135
+ @voice_phases[0] = phase
136
+ end
137
+
138
+ def interpolated_sine(phase)
139
+ position = phase * SINE_TABLE_SIZE
140
+ left_index = position.to_i
141
+ right_index = left_index + 1
142
+ right_index = 0 if right_index == SINE_TABLE_SIZE
143
+ fraction = position - left_index
144
+ left = SINE_WAVETABLE.fetch(left_index)
145
+ left + ((SINE_WAVETABLE.fetch(right_index) - left) * fraction)
146
+ end
147
+
148
+ def generate_samples!(samples, sample_frames, channels, sample_rate)
149
+ sample_frames.times do |frame_index|
150
+ if noise_waveform?
151
+ base_index = frame_index * channels
152
+ channels.times { |channel| samples[base_index + channel] = scaled_noise_sample }
153
+ else
154
+ write_frame!(samples, frame_index, channels, next_periodic_sample(sample_rate))
155
+ end
156
+ end
157
+ end
158
+
159
+ def write_frame!(samples, frame_index, channels, value)
160
+ base_index = frame_index * channels
161
+ channels.times { |channel| samples[base_index + channel] = value }
162
+ end
163
+
60
164
  def validate_waveform!(waveform)
61
165
  value = waveform.to_sym
62
166
  raise InvalidParameterError, "unsupported waveform: #{waveform.inspect}" unless WAVEFORMS.include?(value)
@@ -67,7 +171,10 @@ module Wavify
67
171
  end
68
172
 
69
173
  def validate_frequency!(frequency)
70
- raise InvalidParameterError, "frequency must be a positive Numeric" unless frequency.is_a?(Numeric) && frequency.positive?
174
+ unless frequency.is_a?(Numeric) && frequency.respond_to?(:finite?) && frequency.finite? &&
175
+ frequency.positive? && frequency <= MAX_FREQUENCY
176
+ raise InvalidParameterError, "frequency must be a positive finite Numeric <= #{MAX_FREQUENCY}"
177
+ end
71
178
 
72
179
  frequency.to_f
73
180
  end
@@ -78,21 +185,153 @@ module Wavify
78
185
  amplitude.to_f
79
186
  end
80
187
 
188
+ def validate_phase!(phase)
189
+ unless phase.is_a?(Numeric) && phase.respond_to?(:finite?) && phase.finite?
190
+ raise InvalidParameterError, "phase must be a finite Numeric"
191
+ end
192
+
193
+ phase.to_f % 1.0
194
+ end
195
+
196
+ def validate_pulse_width!(pulse_width)
197
+ unless pulse_width.is_a?(Numeric) && pulse_width.respond_to?(:finite?) && pulse_width.finite? && pulse_width.between?(0.01, 0.99)
198
+ raise InvalidParameterError, "pulse_width must be a finite Numeric in 0.01..0.99"
199
+ end
200
+
201
+ pulse_width.to_f
202
+ end
203
+
204
+ def validate_detune!(detune)
205
+ raise InvalidParameterError, "detune must be a finite Numeric" unless detune.is_a?(Numeric) && detune.respond_to?(:finite?) && detune.finite?
206
+
207
+ detune.to_f
208
+ end
209
+
210
+ def validate_unison!(unison)
211
+ unless unison.is_a?(Integer) && unison.between?(1, MAX_UNISON)
212
+ raise InvalidParameterError, "unison must be an Integer in 1..#{MAX_UNISON}"
213
+ end
214
+
215
+ unison
216
+ end
217
+
81
218
  def validate_format!(format)
82
219
  raise InvalidParameterError, "format must be Core::Format" unless format.is_a?(Core::Format)
83
220
  end
84
221
 
85
- def sample_at(index, sample_rate)
86
- t = @phase + (index.to_f / sample_rate)
222
+ def prepare_sample_rate!(sample_rate)
223
+ if @sample_rate && @sample_rate != sample_rate
224
+ raise InvalidParameterError, "sample_rate cannot change while phase is active; call reset_phase first"
225
+ end
226
+
227
+ if !noise_waveform? && oscillator_voice_frequencies.any? { |frequency| frequency > (sample_rate / 2.0) }
228
+ raise InvalidParameterError, "oscillator frequency must not exceed Nyquist for the output format"
229
+ end
230
+
231
+ @sample_rate = sample_rate
232
+ @voice_frequencies ||= oscillator_voice_frequencies.freeze
233
+ @voice_phases ||= Array.new(@voice_frequencies.length, @initial_phase)
234
+ end
235
+
236
+ def next_periodic_sample(sample_rate)
237
+ raw = @voice_frequencies.each_with_index.sum do |frequency, voice|
238
+ oscillator_sample(@voice_phases.fetch(voice), sample_rate, frequency)
239
+ end / @voice_frequencies.length
240
+ advance_voice_phases!(sample_rate)
241
+ (raw * @amplitude).clamp(-1.0, 1.0)
242
+ end
243
+
244
+ def oscillator_sample(phase, sample_rate, frequency)
245
+ phase_step = frequency / sample_rate.to_f
246
+ phase_step = 0.5 if phase_step > 0.5
247
+
248
+ case @waveform
249
+ when :sine then Math.sin(2.0 * Math::PI * phase)
250
+ when :square then polyblep_square(phase, phase_step, 0.5)
251
+ when :pulse then polyblep_square(phase, phase_step, @pulse_width)
252
+ when :sawtooth then polyblep_saw(phase, phase_step)
253
+ when :triangle then bandlimited_triangle(phase, frequency, sample_rate)
254
+ end
255
+ end
256
+
257
+ def advance_voice_phases!(sample_rate)
258
+ @voice_phases.map!.with_index do |phase, voice|
259
+ (phase + (@voice_frequencies.fetch(voice) / sample_rate.to_f)) % 1.0
260
+ end
261
+ end
262
+
263
+ def noise_waveform?
264
+ @waveform == :white_noise || @waveform == :pink_noise
265
+ end
266
+
267
+ def noise_sample
87
268
  raw = case @waveform
88
- when :sine then Math.sin(2.0 * Math::PI * @frequency * t)
89
- when :square then Math.sin(2.0 * Math::PI * @frequency * t) >= 0 ? 1.0 : -1.0
90
- when :sawtooth then (2.0 * ((@frequency * t) % 1.0)) - 1.0
91
- when :triangle then (2.0 * ((2.0 * ((@frequency * t) % 1.0)) - 1.0).abs) - 1.0
92
269
  when :white_noise then @random.rand(-1.0..1.0)
93
270
  when :pink_noise then next_pink_noise
94
271
  end
95
- (raw * @amplitude).clamp(-1.0, 1.0)
272
+ raw || 0.0
273
+ end
274
+
275
+ def scaled_noise_sample
276
+ (noise_sample * @amplitude).clamp(-1.0, 1.0)
277
+ end
278
+
279
+ def oscillator_voice_frequencies
280
+ return [@frequency] if @unison == 1 || @detune.zero?
281
+
282
+ center = (@unison - 1) / 2.0
283
+ Array.new(@unison) do |voice|
284
+ cents = (voice - center) * @detune
285
+ @frequency * (2.0**(cents / 1200.0))
286
+ end
287
+ end
288
+
289
+ def polyblep_saw(phase, phase_step)
290
+ ((2.0 * phase) - 1.0) - polyblep(phase, phase_step)
291
+ end
292
+
293
+ def polyblep_square(phase, phase_step, pulse_width)
294
+ value = phase < pulse_width ? 1.0 : -1.0
295
+ value += polyblep(phase, phase_step)
296
+ value -= polyblep((phase - pulse_width) % 1.0, phase_step)
297
+ value
298
+ end
299
+
300
+ def bandlimited_triangle(phase, frequency, sample_rate)
301
+ table = triangle_wavetable(frequency, sample_rate)
302
+ position = phase * TRIANGLE_TABLE_SIZE
303
+ left_index = position.floor % TRIANGLE_TABLE_SIZE
304
+ right_index = (left_index + 1) % TRIANGLE_TABLE_SIZE
305
+ fraction = position - position.floor
306
+ table.fetch(left_index) + ((table.fetch(right_index) - table.fetch(left_index)) * fraction)
307
+ end
308
+
309
+ def triangle_wavetable(frequency, sample_rate)
310
+ key = [frequency, sample_rate]
311
+ return @triangle_tables.fetch(key) if @triangle_tables.key?(key)
312
+
313
+ @triangle_tables.shift if @triangle_tables.length >= TRIANGLE_TABLE_CACHE_LIMIT
314
+ highest = [((sample_rate / 2.0) / frequency).floor, TRIANGLE_MAX_HARMONIC].min
315
+ harmonics = (1..highest).step(2).to_a
316
+ scale = 8.0 / (Math::PI * Math::PI)
317
+ @triangle_tables[key] = Array.new(TRIANGLE_TABLE_SIZE) do |index|
318
+ phase = index.to_f / TRIANGLE_TABLE_SIZE
319
+ scale * harmonics.sum { |harmonic| Math.cos(2.0 * Math::PI * harmonic * phase) / (harmonic * harmonic) }
320
+ end.freeze
321
+ end
322
+
323
+ def polyblep(phase, phase_step)
324
+ return 0.0 if phase_step <= 0.0
325
+
326
+ if phase < phase_step
327
+ t = phase / phase_step
328
+ (t + t) - (t * t) - 1.0
329
+ elsif phase > 1.0 - phase_step
330
+ t = (phase - 1.0) / phase_step
331
+ (t * t) + (t + t) + 1.0
332
+ else
333
+ 0.0
334
+ end
96
335
  end
97
336
 
98
337
  # Lightweight pink-noise approximation (Paul Kellet filter).
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wavify
4
+ module DSP
5
+ # Shared invocation and result contract for stateful audio processors.
6
+ module Processor
7
+ # Maximum number of frames requested from a processor tail at once.
8
+ TAIL_CHUNK_FRAMES = 4_096
9
+
10
+ module_function
11
+
12
+ def process(processor, buffer)
13
+ result = if processor.respond_to?(:process)
14
+ processor.process(buffer)
15
+ elsif processor.respond_to?(:call)
16
+ processor.call(buffer)
17
+ elsif processor.respond_to?(:apply)
18
+ processor.apply(buffer)
19
+ else
20
+ raise InvalidParameterError, "processor must respond to :process, :call, or :apply"
21
+ end
22
+ coerce_buffer(result, "processor")
23
+ end
24
+
25
+ def flush(processor, format: nil)
26
+ return [].each unless processor.respond_to?(:flush)
27
+
28
+ result = processor.flush(format: format)
29
+ each_buffer(result, "processor flush")
30
+ end
31
+
32
+ def each_buffer(result, context = "processor")
33
+ return [].each if result.nil?
34
+ return [coerce_buffer(result, context)].each if buffer?(result)
35
+ unless result.respond_to?(:each)
36
+ raise ProcessingError, "#{context} must return Core::SampleBuffer, Audio, an Enumerable of them, or nil"
37
+ end
38
+
39
+ result.lazy.map { |item| coerce_buffer(item, context) }
40
+ end
41
+
42
+ def coerce_buffer(result, context = "processor")
43
+ return result.buffer if defined?(Wavify::Audio) && result.is_a?(Wavify::Audio)
44
+ return result if result.is_a?(Core::SampleBuffer)
45
+
46
+ raise ProcessingError, "#{context} must return Core::SampleBuffer or Audio"
47
+ end
48
+
49
+ def build_runtime(processor)
50
+ return processor.build_runtime if processor.respond_to?(:build_runtime)
51
+
52
+ runtime = processor.dup
53
+ runtime.reset if runtime.respond_to?(:reset)
54
+ runtime
55
+ rescue TypeError
56
+ raise InvalidParameterError, "stateful processor must implement #build_runtime or support #dup"
57
+ end
58
+
59
+ def duration(processor, method_name)
60
+ return 0.0 unless processor.respond_to?(method_name)
61
+
62
+ value = processor.public_send(method_name)
63
+ return 0.0 if value.nil?
64
+ unless value.is_a?(Numeric) && value.respond_to?(:finite?) && value.finite? && value >= 0.0
65
+ raise InvalidParameterError, "#{method_name} must return a non-negative finite Numeric"
66
+ end
67
+
68
+ value.to_f
69
+ end
70
+
71
+ def render(processor, buffer)
72
+ runtime = build_runtime(processor)
73
+ processed = process(runtime, buffer)
74
+ chunks = [processed]
75
+ chunks.concat(flush(runtime, format: processed.format).to_a)
76
+ combined = chunks.reduce { |left, right| left.concat(right) }
77
+ latency_frames = (duration(runtime, :latency) * combined.format.sample_rate).round
78
+ return combined if latency_frames.zero?
79
+
80
+ combined.slice(latency_frames, [combined.sample_frame_count - latency_frames, 0].max)
81
+ end
82
+
83
+ def buffer?(result)
84
+ result.is_a?(Core::SampleBuffer) || (defined?(Wavify::Audio) && result.is_a?(Wavify::Audio))
85
+ end
86
+ private_class_method :buffer?
87
+
88
+ end
89
+ end
90
+ end
data/lib/wavify/errors.rb CHANGED
@@ -20,10 +20,10 @@ module Wavify
20
20
  # Raised when a streaming operation cannot proceed.
21
21
  class StreamError < ProcessingError; end
22
22
 
23
- # Base class for DSP parameter/processing errors.
23
+ # Base class for DSP processing errors.
24
24
  class DSPError < Error; end
25
25
  # Raised when method parameters are invalid.
26
- class InvalidParameterError < DSPError; end
26
+ class InvalidParameterError < Error; end
27
27
 
28
28
  # Base class for sequencer and DSL-related errors.
29
29
  class SequencerError < Error; end