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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -1
- data/README.md +204 -22
- data/exe/wavify +6 -0
- data/lib/wavify/adapters.rb +81 -0
- data/lib/wavify/audio.rb +1066 -120
- data/lib/wavify/cli.rb +237 -0
- data/lib/wavify/codecs/aiff.rb +388 -87
- data/lib/wavify/codecs/base.rb +98 -5
- data/lib/wavify/codecs/flac.rb +660 -170
- data/lib/wavify/codecs/ogg_vorbis.rb +515 -155
- data/lib/wavify/codecs/raw.rb +235 -45
- data/lib/wavify/codecs/registry.rb +272 -10
- data/lib/wavify/codecs/wav.rb +453 -74
- data/lib/wavify/core/duration.rb +54 -2
- data/lib/wavify/core/format.rb +96 -11
- data/lib/wavify/core/sample_buffer.rb +648 -60
- data/lib/wavify/core/stream.rb +590 -34
- data/lib/wavify/dsl.rb +712 -100
- data/lib/wavify/dsp/automation.rb +109 -0
- data/lib/wavify/dsp/effects/auto_pan.rb +71 -0
- data/lib/wavify/dsp/effects/bitcrusher.rb +74 -0
- data/lib/wavify/dsp/effects/chorus.rb +15 -4
- data/lib/wavify/dsp/effects/compressor.rb +133 -23
- data/lib/wavify/dsp/effects/delay.rb +46 -1
- data/lib/wavify/dsp/effects/distortion.rb +3 -2
- data/lib/wavify/dsp/effects/effect_base.rb +69 -2
- data/lib/wavify/dsp/effects/effect_chain.rb +94 -0
- data/lib/wavify/dsp/effects/envelope_controlled_effect.rb +93 -0
- data/lib/wavify/dsp/effects/eq.rb +102 -0
- data/lib/wavify/dsp/effects/expander.rb +90 -0
- data/lib/wavify/dsp/effects/flanger.rb +112 -0
- data/lib/wavify/dsp/effects/limiter.rb +259 -0
- data/lib/wavify/dsp/effects/mastering_chain.rb +31 -0
- data/lib/wavify/dsp/effects/noise_gate.rb +72 -0
- data/lib/wavify/dsp/effects/phaser.rb +112 -0
- data/lib/wavify/dsp/effects/podcast_chain.rb +32 -0
- data/lib/wavify/dsp/effects/reverb.rb +100 -14
- data/lib/wavify/dsp/effects/soft_limiter.rb +55 -0
- data/lib/wavify/dsp/effects/stereo_widener.rb +73 -0
- data/lib/wavify/dsp/effects/tremolo.rb +66 -0
- data/lib/wavify/dsp/effects/vibrato.rb +102 -0
- data/lib/wavify/dsp/effects.rb +106 -0
- data/lib/wavify/dsp/envelope.rb +90 -19
- data/lib/wavify/dsp/filter.rb +149 -8
- data/lib/wavify/dsp/headroom.rb +57 -0
- data/lib/wavify/dsp/lfo.rb +65 -0
- data/lib/wavify/dsp/loudness_meter.rb +281 -0
- data/lib/wavify/dsp/oscillator.rb +261 -22
- data/lib/wavify/dsp/processor.rb +90 -0
- data/lib/wavify/errors.rb +2 -2
- data/lib/wavify/sequencer/engine.rb +405 -89
- data/lib/wavify/sequencer/note_sequence.rb +45 -7
- data/lib/wavify/sequencer/pattern.rb +121 -12
- data/lib/wavify/sequencer/track.rb +149 -10
- data/lib/wavify/version.rb +1 -1
- data/lib/wavify.rb +18 -0
- data/sig/audio.rbs +86 -0
- data/sig/codecs.rbs +77 -0
- data/sig/core.rbs +108 -0
- data/sig/dsl.rbs +32 -0
- data/sig/dsp.rbs +87 -0
- data/sig/effects.rbs +129 -0
- data/sig/public_api.yml +111 -0
- data/sig/sequencer.rbs +126 -0
- data/sig/stream.rbs +30 -0
- data/sig/wavify.rbs +24 -0
- metadata +44 -58
- data/.serena/.gitignore +0 -1
- data/.serena/memories/project_overview.md +0 -5
- data/.serena/memories/style_and_completion.md +0 -5
- data/.serena/memories/suggested_commands.md +0 -11
- data/.serena/project.yml +0 -126
- data/.simplecov +0 -18
- data/.yardopts +0 -4
- data/Rakefile +0 -190
- data/benchmarks/README.md +0 -46
- data/benchmarks/benchmark_helper.rb +0 -112
- data/benchmarks/dsp_effects_benchmark.rb +0 -46
- data/benchmarks/flac_benchmark.rb +0 -74
- data/benchmarks/streaming_memory_benchmark.rb +0 -94
- data/benchmarks/wav_io_benchmark.rb +0 -110
- data/examples/audio_processing.rb +0 -73
- data/examples/cinematic_transition.rb +0 -118
- data/examples/drum_machine.rb +0 -74
- data/examples/format_convert.rb +0 -81
- data/examples/hybrid_arrangement.rb +0 -165
- data/examples/streaming_master_chain.rb +0 -129
- data/examples/synth_pad.rb +0 -42
- 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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
format.channels.
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
23
|
+
# Base class for DSP processing errors.
|
|
24
24
|
class DSPError < Error; end
|
|
25
25
|
# Raised when method parameters are invalid.
|
|
26
|
-
class InvalidParameterError <
|
|
26
|
+
class InvalidParameterError < Error; end
|
|
27
27
|
|
|
28
28
|
# Base class for sequencer and DSL-related errors.
|
|
29
29
|
class SequencerError < Error; end
|