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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -6
- data/README.md +5 -0
- data/Rakefile +50 -1
- data/lib/deftones/analysis/meter.rb +22 -2
- data/lib/deftones/component/channel.rb +1 -1
- data/lib/deftones/component/compressor.rb +127 -22
- data/lib/deftones/component/filter.rb +29 -19
- data/lib/deftones/component/merge.rb +14 -0
- data/lib/deftones/component/multiband_compressor.rb +1 -1
- data/lib/deftones/component/one_pole_filter.rb +10 -3
- data/lib/deftones/component/panner.rb +25 -2
- data/lib/deftones/component/panner3d.rb +0 -10
- data/lib/deftones/component/split.rb +14 -0
- data/lib/deftones/context.rb +90 -9
- data/lib/deftones/core/audio_block.rb +64 -5
- data/lib/deftones/core/audio_node.rb +98 -8
- data/lib/deftones/core/gain.rb +0 -8
- data/lib/deftones/core/instrument.rb +52 -10
- data/lib/deftones/core/param.rb +51 -1
- data/lib/deftones/core/signal.rb +79 -28
- data/lib/deftones/core/source.rb +71 -11
- data/lib/deftones/destination.rb +41 -17
- data/lib/deftones/draw.rb +6 -10
- data/lib/deftones/dsp/biquad.rb +9 -4
- data/lib/deftones/dsp/delay_line.rb +2 -2
- data/lib/deftones/dsp/helpers.rb +7 -0
- data/lib/deftones/effect/bit_crusher.rb +10 -2
- data/lib/deftones/effect/chebyshev.rb +7 -3
- data/lib/deftones/effect/distortion.rb +5 -3
- data/lib/deftones/effect/feedback_delay.rb +2 -1
- data/lib/deftones/effect/oversampling.rb +43 -0
- data/lib/deftones/effect/phaser.rb +2 -1
- data/lib/deftones/effect/pitch_shift.rb +1 -2
- data/lib/deftones/effect/reverb.rb +73 -5
- data/lib/deftones/event/callback_behavior.rb +7 -3
- data/lib/deftones/event/loop.rb +7 -2
- data/lib/deftones/event/part.rb +18 -3
- data/lib/deftones/event/pattern.rb +51 -6
- data/lib/deftones/event/sequence.rb +19 -5
- data/lib/deftones/event/tone_event.rb +7 -2
- data/lib/deftones/event/transport.rb +243 -21
- data/lib/deftones/instrument/poly_synth.rb +81 -15
- data/lib/deftones/instrument/sampler.rb +53 -10
- data/lib/deftones/io/buffer.rb +376 -55
- data/lib/deftones/io/buffers.rb +28 -4
- data/lib/deftones/io/recorder.rb +2 -1
- data/lib/deftones/music/frequency.rb +13 -8
- data/lib/deftones/music/midi.rb +132 -9
- data/lib/deftones/music/note.rb +13 -3
- data/lib/deftones/music/time.rb +42 -4
- data/lib/deftones/offline_context.rb +194 -17
- data/lib/deftones/portaudio_support.rb +68 -9
- data/lib/deftones/source/fat_oscillator.rb +28 -9
- data/lib/deftones/source/grain_player.rb +49 -2
- data/lib/deftones/source/noise.rb +42 -10
- data/lib/deftones/source/omni_oscillator.rb +1 -2
- data/lib/deftones/source/oscillator.rb +83 -19
- data/lib/deftones/source/player.rb +24 -6
- data/lib/deftones/source/players.rb +39 -6
- data/lib/deftones/source/tone_buffer_source.rb +12 -6
- data/lib/deftones/source/tone_oscillator_node.rb +4 -3
- data/lib/deftones/source/user_media.rb +83 -10
- data/lib/deftones/version.rb +1 -1
- data/lib/deftones.rb +108 -31
- metadata +3 -44
data/lib/deftones/io/buffer.rb
CHANGED
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "tempfile"
|
|
5
|
+
require "timeout"
|
|
5
6
|
|
|
6
7
|
module Deftones
|
|
7
8
|
module IO
|
|
8
9
|
class Buffer
|
|
9
10
|
include Enumerable
|
|
10
11
|
|
|
11
|
-
attr_reader :samples, :channels, :sample_rate
|
|
12
|
+
attr_reader :samples, :channels, :sample_rate, :interpolation
|
|
12
13
|
|
|
13
14
|
COMPRESSED_EXTENSIONS = %w[.mp3 .ogg .oga].freeze
|
|
14
15
|
SAVEABLE_FORMATS = %i[wav mp3 ogg].freeze
|
|
16
|
+
DEFAULT_CODEC_TIMEOUT = 30.0
|
|
17
|
+
INTERPOLATION_MODES = %i[linear nearest cubic sinc_lite].freeze
|
|
18
|
+
SINC_LITE_RADIUS = 8
|
|
19
|
+
WAV_BIT_DEPTHS = [16, 24, 32].freeze
|
|
20
|
+
Statistics = Struct.new(:peak, :rms, :clip_count, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
attr_accessor :codec_backend, :codec_timeout
|
|
24
|
+
end
|
|
15
25
|
|
|
16
26
|
def self.interleave(mono_samples, channels)
|
|
17
27
|
return mono_samples.dup if channels == 1
|
|
@@ -19,12 +29,12 @@ module Deftones
|
|
|
19
29
|
mono_samples.flat_map { |sample| Array.new(channels, sample) }
|
|
20
30
|
end
|
|
21
31
|
|
|
22
|
-
def self.from_mono(samples, channels: 1, sample_rate: Context::DEFAULT_SAMPLE_RATE)
|
|
32
|
+
def self.from_mono(samples, channels: 1, sample_rate: Context::DEFAULT_SAMPLE_RATE, interpolation: :linear)
|
|
23
33
|
interleaved = channels == 1 ? samples : interleave(samples, channels)
|
|
24
|
-
new(interleaved, channels: channels, sample_rate: sample_rate)
|
|
34
|
+
new(interleaved, channels: channels, sample_rate: sample_rate, interpolation: interpolation)
|
|
25
35
|
end
|
|
26
36
|
|
|
27
|
-
def self.from_array(samples, sample_rate: Context::DEFAULT_SAMPLE_RATE, channels: nil)
|
|
37
|
+
def self.from_array(samples, sample_rate: Context::DEFAULT_SAMPLE_RATE, channels: nil, interpolation: :linear)
|
|
28
38
|
if samples.first.is_a?(Array)
|
|
29
39
|
channel_count = channels || samples.length
|
|
30
40
|
frame_count = samples.map(&:length).max || 0
|
|
@@ -37,9 +47,9 @@ module Deftones
|
|
|
37
47
|
end
|
|
38
48
|
end
|
|
39
49
|
|
|
40
|
-
new(interleaved, channels: channel_count, sample_rate: sample_rate)
|
|
50
|
+
new(interleaved, channels: channel_count, sample_rate: sample_rate, interpolation: interpolation)
|
|
41
51
|
else
|
|
42
|
-
from_mono(samples, channels: channels || 1, sample_rate: sample_rate)
|
|
52
|
+
from_mono(samples, channels: channels || 1, sample_rate: sample_rate, interpolation: interpolation)
|
|
43
53
|
end
|
|
44
54
|
end
|
|
45
55
|
|
|
@@ -51,24 +61,40 @@ module Deftones
|
|
|
51
61
|
true
|
|
52
62
|
end
|
|
53
63
|
|
|
64
|
+
def self.compressed_audio_available?
|
|
65
|
+
return true if codec_backend&.respond_to?(:decode)
|
|
66
|
+
return true if codec_backend&.respond_to?(:encode)
|
|
67
|
+
|
|
68
|
+
send(:executable_available?, "ffmpeg") || send(:executable_available?, "afconvert")
|
|
69
|
+
end
|
|
70
|
+
|
|
54
71
|
class << self
|
|
55
72
|
alias fromArray from_array
|
|
56
73
|
alias fromUrl from_url
|
|
74
|
+
alias compressedAudioAvailable compressed_audio_available?
|
|
57
75
|
end
|
|
58
76
|
|
|
59
|
-
def self.load(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
def self.load(source, sample_rate: nil, channels: nil)
|
|
78
|
+
return load_io(source) if source.respond_to?(:read) && !source.is_a?(String)
|
|
79
|
+
|
|
80
|
+
validate_path_string!(source, role: "audio source")
|
|
81
|
+
extension = File.extname(source).downcase
|
|
82
|
+
return load_wav(source) if extension == ".wav"
|
|
83
|
+
return load_compressed(source, extension, sample_rate: sample_rate, channels: channels) if COMPRESSED_EXTENSIONS.include?(extension)
|
|
63
84
|
|
|
64
|
-
raise
|
|
85
|
+
raise Deftones::UnsupportedAudioFormatError, "Unsupported audio format: #{extension}"
|
|
65
86
|
end
|
|
66
87
|
|
|
67
|
-
def initialize(samples, channels:, sample_rate:)
|
|
88
|
+
def initialize(samples, channels:, sample_rate:, interpolation: :linear)
|
|
68
89
|
@samples = samples.map(&:to_f)
|
|
69
90
|
@channels = channels
|
|
70
91
|
@sample_rate = sample_rate
|
|
92
|
+
@interpolation = normalize_interpolation(interpolation)
|
|
71
93
|
@disposed = false
|
|
94
|
+
@mono_cache = nil
|
|
95
|
+
@peak_cache = nil
|
|
96
|
+
@rms_cache = nil
|
|
97
|
+
@statistics_cache = {}
|
|
72
98
|
end
|
|
73
99
|
|
|
74
100
|
def each(&block)
|
|
@@ -103,21 +129,42 @@ module Deftones
|
|
|
103
129
|
|
|
104
130
|
def mono
|
|
105
131
|
return @samples if @channels == 1
|
|
132
|
+
return @mono_cache if @mono_cache
|
|
106
133
|
|
|
107
|
-
Array.new(frames) do |frame|
|
|
134
|
+
@mono_cache = Array.new(frames) do |frame|
|
|
108
135
|
offset = frame * @channels
|
|
109
136
|
@samples[offset, @channels].sum / @channels.to_f
|
|
110
137
|
end
|
|
111
138
|
end
|
|
112
139
|
|
|
113
140
|
def peak
|
|
114
|
-
@samples.map(&:abs).max || 0.0
|
|
141
|
+
@peak_cache ||= @samples.map(&:abs).max || 0.0
|
|
115
142
|
end
|
|
116
143
|
|
|
117
144
|
def rms
|
|
118
145
|
return 0.0 if @samples.empty?
|
|
119
146
|
|
|
120
|
-
Math.sqrt(@samples.sum { |sample| sample * sample } / @samples.length)
|
|
147
|
+
@rms_cache ||= Math.sqrt(@samples.sum { |sample| sample * sample } / @samples.length)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def integrated_lufs
|
|
151
|
+
return -Float::INFINITY if rms <= 0.0
|
|
152
|
+
|
|
153
|
+
(20.0 * Math.log10(rms)) - 0.691
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def clip_count(threshold = 1.0)
|
|
157
|
+
limit = threshold.to_f.abs
|
|
158
|
+
@samples.count { |sample| sample.abs >= limit }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def statistics(clip_threshold: 1.0)
|
|
162
|
+
threshold = clip_threshold.to_f.abs
|
|
163
|
+
@statistics_cache[threshold] ||= Statistics.new(
|
|
164
|
+
peak: peak,
|
|
165
|
+
rms: rms,
|
|
166
|
+
clip_count: clip_count(threshold)
|
|
167
|
+
)
|
|
121
168
|
end
|
|
122
169
|
|
|
123
170
|
def [](frame_index, channel = nil)
|
|
@@ -146,43 +193,172 @@ module Deftones
|
|
|
146
193
|
Array.new(@channels) { |channel_index| get_channel_data(channel_index) }
|
|
147
194
|
end
|
|
148
195
|
|
|
149
|
-
def
|
|
196
|
+
def interpolation=(value)
|
|
197
|
+
@interpolation = normalize_interpolation(value)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def sample_at(frame_position, channel = 0, interpolation: @interpolation)
|
|
150
201
|
return 0.0 if @samples.empty?
|
|
151
202
|
|
|
152
203
|
clamped_position = Deftones::DSP::Helpers.clamp(frame_position.to_f, 0.0, [frames - 1, 0].max)
|
|
204
|
+
channel_index = [channel, @channels - 1].min
|
|
205
|
+
case normalize_interpolation(interpolation)
|
|
206
|
+
when :nearest
|
|
207
|
+
self[clamped_position.round, channel_index]
|
|
208
|
+
when :cubic
|
|
209
|
+
cubic_sample_at(clamped_position, channel_index)
|
|
210
|
+
when :sinc_lite
|
|
211
|
+
sinc_lite_sample_at(clamped_position, channel_index)
|
|
212
|
+
else
|
|
213
|
+
linear_sample_at(clamped_position, channel_index)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def sample_at_nearest(frame_position, channel = 0)
|
|
218
|
+
sample_at(frame_position, channel, interpolation: :nearest)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def sample_at_cubic(frame_position, channel = 0)
|
|
222
|
+
sample_at(frame_position, channel, interpolation: :cubic)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def sample_at_sinc_lite(frame_position, channel = 0)
|
|
226
|
+
sample_at(frame_position, channel, interpolation: :sinc_lite)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
alias sampleAt sample_at
|
|
230
|
+
alias sampleAtNearest sample_at_nearest
|
|
231
|
+
alias sampleAtCubic sample_at_cubic
|
|
232
|
+
alias sampleAtSincLite sample_at_sinc_lite
|
|
233
|
+
|
|
234
|
+
def resample(target_sample_rate, interpolation: @interpolation)
|
|
235
|
+
normalized_sample_rate = target_sample_rate.to_f
|
|
236
|
+
raise ArgumentError, "sample rate must be positive" unless normalized_sample_rate.positive? && normalized_sample_rate.finite?
|
|
237
|
+
return new_like(@samples) if normalized_sample_rate == @sample_rate.to_f
|
|
238
|
+
|
|
239
|
+
target_frames = (frames * (normalized_sample_rate / @sample_rate.to_f)).round
|
|
240
|
+
channel_data = Array.new(@channels) do |channel_index|
|
|
241
|
+
Array.new(target_frames) do |frame_index|
|
|
242
|
+
source_position = frame_index * (@sample_rate.to_f / normalized_sample_rate)
|
|
243
|
+
sample_at(source_position, channel_index, interpolation: interpolation)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
self.class.from_array(
|
|
247
|
+
channel_data,
|
|
248
|
+
sample_rate: normalized_sample_rate.round,
|
|
249
|
+
channels: @channels,
|
|
250
|
+
interpolation: normalize_interpolation(interpolation)
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
alias resampleTo resample
|
|
255
|
+
|
|
256
|
+
def linear_sample_at(clamped_position, channel)
|
|
153
257
|
lower = clamped_position.floor
|
|
154
258
|
upper = [lower + 1, frames - 1].min
|
|
155
259
|
fraction = clamped_position - lower
|
|
156
|
-
lower_sample = self[lower,
|
|
157
|
-
upper_sample = self[upper,
|
|
260
|
+
lower_sample = self[lower, channel]
|
|
261
|
+
upper_sample = self[upper, channel]
|
|
158
262
|
Deftones::DSP::Helpers.lerp(lower_sample, upper_sample, fraction)
|
|
159
263
|
end
|
|
160
264
|
|
|
265
|
+
def cubic_sample_at(clamped_position, channel)
|
|
266
|
+
base = clamped_position.floor
|
|
267
|
+
fraction = clamped_position - base
|
|
268
|
+
p0 = self[[base - 1, 0].max, channel]
|
|
269
|
+
p1 = self[base, channel]
|
|
270
|
+
p2 = self[[base + 1, frames - 1].min, channel]
|
|
271
|
+
p3 = self[[base + 2, frames - 1].min, channel]
|
|
272
|
+
a0 = (-0.5 * p0) + (1.5 * p1) - (1.5 * p2) + (0.5 * p3)
|
|
273
|
+
a1 = p0 - (2.5 * p1) + (2.0 * p2) - (0.5 * p3)
|
|
274
|
+
a2 = (-0.5 * p0) + (0.5 * p2)
|
|
275
|
+
a3 = p1
|
|
276
|
+
(((a0 * fraction) + a1) * fraction * fraction) + (a2 * fraction) + a3
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def sinc_lite_sample_at(clamped_position, channel)
|
|
280
|
+
center = clamped_position.floor
|
|
281
|
+
weighted_sum = 0.0
|
|
282
|
+
weight_total = 0.0
|
|
283
|
+
|
|
284
|
+
((center - SINC_LITE_RADIUS + 1)..(center + SINC_LITE_RADIUS)).each do |index|
|
|
285
|
+
next if index.negative? || index >= frames
|
|
286
|
+
|
|
287
|
+
distance = clamped_position - index
|
|
288
|
+
weight = sinc(distance) * hann_window(distance / SINC_LITE_RADIUS)
|
|
289
|
+
weighted_sum += self[index, channel] * weight
|
|
290
|
+
weight_total += weight
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
return linear_sample_at(clamped_position, channel) if weight_total.abs < 1.0e-12
|
|
294
|
+
|
|
295
|
+
weighted_sum / weight_total
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def sinc(value)
|
|
299
|
+
return 1.0 if value.abs < 1.0e-12
|
|
300
|
+
|
|
301
|
+
x = Math::PI * value
|
|
302
|
+
Math.sin(x) / x
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def hann_window(normalized_distance)
|
|
306
|
+
distance = normalized_distance.abs
|
|
307
|
+
return 0.0 if distance >= 1.0
|
|
308
|
+
|
|
309
|
+
0.5 * (1.0 + Math.cos(Math::PI * distance))
|
|
310
|
+
end
|
|
311
|
+
|
|
161
312
|
def slice(start_frame, length)
|
|
162
313
|
frame_count = [length.to_i, 0].max
|
|
163
|
-
|
|
314
|
+
first_frame = [start_frame.to_i, 0].max
|
|
315
|
+
offset = first_frame * @channels
|
|
164
316
|
subset = @samples.slice(offset, frame_count * @channels) || []
|
|
165
|
-
|
|
317
|
+
new_like(subset)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def slice_seconds(start_time, duration)
|
|
321
|
+
start_frame = (Deftones::Music::Time.parse(start_time) * @sample_rate).floor
|
|
322
|
+
frame_count = (Deftones::Music::Time.parse(duration) * @sample_rate).ceil
|
|
323
|
+
slice(start_frame, frame_count)
|
|
166
324
|
end
|
|
167
325
|
|
|
168
326
|
def reverse
|
|
169
327
|
reversed_frames = each_frame.to_a.reverse.flatten
|
|
170
|
-
|
|
328
|
+
new_like(reversed_frames)
|
|
171
329
|
end
|
|
172
330
|
|
|
173
331
|
def normalize(target_peak = 0.99)
|
|
174
|
-
|
|
332
|
+
normalized_target = normalize_level_target(target_peak, "target peak")
|
|
333
|
+
return new_like(@samples) if peak.zero?
|
|
334
|
+
|
|
335
|
+
scale = normalized_target / peak
|
|
336
|
+
new_like(@samples.map { |sample| sample * scale })
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def normalize_rms(target_rms = 0.2)
|
|
340
|
+
normalized_target = normalize_level_target(target_rms, "target RMS")
|
|
341
|
+
return new_like(@samples) if rms.zero?
|
|
175
342
|
|
|
176
|
-
scale =
|
|
177
|
-
|
|
343
|
+
scale = normalized_target / rms
|
|
344
|
+
new_like(@samples.map { |sample| sample * scale })
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def normalize_lufs(target_lufs = -14.0)
|
|
348
|
+
target_gain = 10.0**((target_lufs.to_f + 0.691) / 20.0)
|
|
349
|
+
normalize_rms(target_gain)
|
|
178
350
|
end
|
|
179
351
|
|
|
180
352
|
def mixdown
|
|
181
|
-
self.class.new(mono, channels: 1, sample_rate: @sample_rate)
|
|
353
|
+
self.class.new(mono, channels: 1, sample_rate: @sample_rate, interpolation: @interpolation)
|
|
182
354
|
end
|
|
183
355
|
|
|
184
356
|
def dispose
|
|
185
357
|
@samples = []
|
|
358
|
+
@mono_cache = nil
|
|
359
|
+
@peak_cache = nil
|
|
360
|
+
@rms_cache = nil
|
|
361
|
+
@statistics_cache.clear
|
|
186
362
|
@disposed = true
|
|
187
363
|
self
|
|
188
364
|
end
|
|
@@ -190,46 +366,100 @@ module Deftones
|
|
|
190
366
|
alias numberOfChannels number_of_channels
|
|
191
367
|
alias getChannelData get_channel_data
|
|
192
368
|
alias toArray to_array
|
|
369
|
+
alias sliceSeconds slice_seconds
|
|
370
|
+
alias normalizeRms normalize_rms
|
|
371
|
+
alias normalizeLufs normalize_lufs
|
|
372
|
+
alias integratedLufs integrated_lufs
|
|
373
|
+
alias stats statistics
|
|
374
|
+
|
|
375
|
+
def save(target, format: nil, on_format_mismatch: :error, bit_depth: 16, dither: false, dither_rng: nil)
|
|
376
|
+
if target.respond_to?(:write) && !target.is_a?(String)
|
|
377
|
+
return save_io(target, format: format || :wav, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
|
|
378
|
+
end
|
|
193
379
|
|
|
194
|
-
|
|
195
|
-
resolved_format = self.class.send(:resolve_save_format,
|
|
196
|
-
raise
|
|
380
|
+
self.class.send(:validate_path_string!, target, role: "audio target")
|
|
381
|
+
resolved_format = self.class.send(:resolve_save_format, target, format, on_format_mismatch: on_format_mismatch)
|
|
382
|
+
raise Deftones::UnsupportedAudioFormatError, "Unsupported format: #{resolved_format}" unless SAVEABLE_FORMATS.include?(resolved_format)
|
|
197
383
|
|
|
198
384
|
case resolved_format
|
|
199
385
|
when :wav
|
|
200
|
-
save_wav(
|
|
386
|
+
save_wav(target, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
|
|
201
387
|
when :mp3, :ogg
|
|
202
|
-
save_compressed(
|
|
388
|
+
save_compressed(target, resolved_format, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
|
|
203
389
|
end
|
|
204
|
-
|
|
390
|
+
target
|
|
205
391
|
end
|
|
206
392
|
|
|
207
393
|
private
|
|
208
394
|
|
|
209
|
-
def
|
|
395
|
+
def new_like(samples, channels: @channels, sample_rate: @sample_rate)
|
|
396
|
+
self.class.new(samples, channels: channels, sample_rate: sample_rate, interpolation: @interpolation)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def normalize_interpolation(value)
|
|
400
|
+
normalized = value.to_sym
|
|
401
|
+
return normalized if INTERPOLATION_MODES.include?(normalized)
|
|
402
|
+
|
|
403
|
+
raise ArgumentError, "Unsupported interpolation mode: #{value}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def normalize_level_target(value, name)
|
|
407
|
+
normalized = value.to_f
|
|
408
|
+
raise ArgumentError, "#{name} must be finite and non-negative" unless normalized.finite? && !normalized.negative?
|
|
409
|
+
|
|
410
|
+
normalized
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def save_io(io, format:, bit_depth:, dither:, dither_rng:)
|
|
414
|
+
Tempfile.create(["deftones-buffer-save", ".#{format}"]) do |tempfile|
|
|
415
|
+
tempfile.close
|
|
416
|
+
save(tempfile.path, format: format, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
|
|
417
|
+
io.write(File.binread(tempfile.path))
|
|
418
|
+
end
|
|
419
|
+
io
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def save_wav(path, bit_depth:, dither:, dither_rng:)
|
|
423
|
+
self.class.send(:ensure_wav_backend!)
|
|
424
|
+
normalized_bit_depth = self.class.send(:validate_wav_bit_depth, bit_depth)
|
|
425
|
+
output_samples = dither ? dithered_samples(normalized_bit_depth, dither_rng) : @samples
|
|
426
|
+
|
|
210
427
|
sample_buffer = Wavify::Core::SampleBuffer.new(
|
|
211
|
-
|
|
428
|
+
output_samples,
|
|
212
429
|
self.class.send(:wavify_work_format, @channels, @sample_rate)
|
|
213
430
|
)
|
|
214
431
|
Wavify::Codecs::Wav.write(
|
|
215
432
|
path,
|
|
216
433
|
sample_buffer,
|
|
217
|
-
format: self.class.send(:wavify_wav_format, @channels, @sample_rate)
|
|
434
|
+
format: self.class.send(:wavify_wav_format, @channels, @sample_rate, normalized_bit_depth)
|
|
218
435
|
)
|
|
219
436
|
end
|
|
220
437
|
|
|
221
|
-
def save_compressed(path, format)
|
|
438
|
+
def save_compressed(path, format, bit_depth:, dither:, dither_rng:)
|
|
222
439
|
backend = self.class.send(:encoder_backend_for, format)
|
|
223
|
-
raise
|
|
440
|
+
raise Deftones::MissingCodecBackendError, self.class.send(:missing_encoder_message, format) unless backend
|
|
224
441
|
|
|
225
442
|
Tempfile.create(["deftones-buffer-export", ".wav"]) do |tempfile|
|
|
226
443
|
tempfile.close
|
|
227
|
-
save_wav(tempfile.path)
|
|
228
|
-
|
|
444
|
+
save_wav(tempfile.path, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
|
|
445
|
+
if self.class.send(:custom_codec_backend?, backend)
|
|
446
|
+
backend.encode(tempfile.path, path, format: format, sample_rate: @sample_rate, channels: @channels)
|
|
447
|
+
return
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
command = self.class.send(:encoder_command, backend, tempfile.path, path, format, @sample_rate, @channels)
|
|
451
|
+
stdout, stderr, status = self.class.send(:capture_codec_command, *command)
|
|
229
452
|
return if status.success?
|
|
230
453
|
|
|
231
|
-
|
|
232
|
-
|
|
454
|
+
self.class.send(:raise_codec_command_error, "Failed to encode #{format}", command, stdout, stderr, status)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def dithered_samples(bit_depth, rng)
|
|
459
|
+
random = rng || Random
|
|
460
|
+
step = 1.0 / ((2**(bit_depth - 1)) - 1)
|
|
461
|
+
@samples.map do |sample|
|
|
462
|
+
Deftones::DSP::Helpers.clamp(sample + ((random.rand - random.rand) * step), -1.0, 1.0)
|
|
233
463
|
end
|
|
234
464
|
end
|
|
235
465
|
|
|
@@ -237,27 +467,60 @@ module Deftones
|
|
|
237
467
|
private
|
|
238
468
|
|
|
239
469
|
def load_wav(path)
|
|
470
|
+
validate_path_string!(path, role: "WAV source")
|
|
471
|
+
ensure_wav_backend!
|
|
472
|
+
|
|
240
473
|
sample_buffer = Wavify::Codecs::Wav.read(path)
|
|
241
474
|
float_buffer = sample_buffer.convert(wavify_work_format(sample_buffer.format.channels, sample_buffer.format.sample_rate))
|
|
242
475
|
new(float_buffer.samples, channels: float_buffer.format.channels, sample_rate: float_buffer.format.sample_rate)
|
|
243
|
-
rescue
|
|
476
|
+
rescue StandardError => error
|
|
477
|
+
raise if error.is_a?(Deftones::MissingCodecBackendError)
|
|
478
|
+
raise unless defined?(Wavify::Error) && error.is_a?(Wavify::Error)
|
|
479
|
+
|
|
244
480
|
raise ArgumentError, "Failed to load WAV: #{error.message}"
|
|
245
481
|
end
|
|
246
482
|
|
|
247
|
-
def
|
|
483
|
+
def load_io(io)
|
|
484
|
+
extension = io.respond_to?(:path) ? File.extname(io.path).downcase : ".wav"
|
|
485
|
+
extension = ".wav" if extension.empty?
|
|
486
|
+
Tempfile.create(["deftones-buffer-load", extension]) do |tempfile|
|
|
487
|
+
tempfile.binmode
|
|
488
|
+
tempfile.write(io.read)
|
|
489
|
+
tempfile.close
|
|
490
|
+
load(tempfile.path)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def load_compressed(path, extension, sample_rate: nil, channels: nil)
|
|
495
|
+
validate_path_string!(path, role: "compressed audio source")
|
|
248
496
|
backend = decoder_backend_for(extension)
|
|
249
|
-
raise
|
|
497
|
+
raise Deftones::MissingCodecBackendError, missing_decoder_message(extension) unless backend
|
|
250
498
|
|
|
251
499
|
Tempfile.create(["deftones-buffer", ".wav"]) do |tempfile|
|
|
252
500
|
tempfile.close
|
|
253
|
-
|
|
501
|
+
if custom_codec_backend?(backend)
|
|
502
|
+
decode_options = { extension: extension }
|
|
503
|
+
decode_options[:sample_rate] = sample_rate if sample_rate
|
|
504
|
+
decode_options[:channels] = channels if channels
|
|
505
|
+
backend.decode(path, tempfile.path, **decode_options)
|
|
506
|
+
next load_wav(tempfile.path)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
command = decoder_command(backend, path, tempfile.path, sample_rate: sample_rate, channels: channels)
|
|
510
|
+
stdout, stderr, status = capture_codec_command(*command)
|
|
254
511
|
next load_wav(tempfile.path) if status.success?
|
|
255
512
|
|
|
256
|
-
|
|
257
|
-
raise ArgumentError, "Failed to decode #{extension}: #{message}"
|
|
513
|
+
raise_codec_command_error("Failed to decode #{extension}", command, stdout, stderr, status)
|
|
258
514
|
end
|
|
259
515
|
end
|
|
260
516
|
|
|
517
|
+
def ensure_wav_backend!
|
|
518
|
+
return if Deftones.wavify_available?
|
|
519
|
+
|
|
520
|
+
raise Deftones::MissingCodecBackendError,
|
|
521
|
+
"WAV codec backend is unavailable. Install the wavify gem to load or save WAV audio."
|
|
522
|
+
end
|
|
523
|
+
|
|
261
524
|
def wavify_work_format(channels, sample_rate)
|
|
262
525
|
Wavify::Core::Format.new(
|
|
263
526
|
channels: channels,
|
|
@@ -267,16 +530,24 @@ module Deftones
|
|
|
267
530
|
)
|
|
268
531
|
end
|
|
269
532
|
|
|
270
|
-
def wavify_wav_format(channels, sample_rate)
|
|
533
|
+
def wavify_wav_format(channels, sample_rate, bit_depth)
|
|
271
534
|
Wavify::Core::Format.new(
|
|
272
535
|
channels: channels,
|
|
273
536
|
sample_rate: sample_rate,
|
|
274
|
-
bit_depth:
|
|
537
|
+
bit_depth: bit_depth,
|
|
275
538
|
sample_format: :pcm
|
|
276
539
|
)
|
|
277
540
|
end
|
|
278
541
|
|
|
542
|
+
def validate_wav_bit_depth(bit_depth)
|
|
543
|
+
normalized = bit_depth.to_i
|
|
544
|
+
return normalized if WAV_BIT_DEPTHS.include?(normalized)
|
|
545
|
+
|
|
546
|
+
raise ArgumentError, "Unsupported WAV bit depth: #{bit_depth}"
|
|
547
|
+
end
|
|
548
|
+
|
|
279
549
|
def decoder_backend_for(extension)
|
|
550
|
+
return codec_backend if codec_backend&.respond_to?(:decode)
|
|
280
551
|
return :ffmpeg if executable_available?("ffmpeg")
|
|
281
552
|
return :afconvert if extension == ".mp3" && executable_available?("afconvert")
|
|
282
553
|
|
|
@@ -284,16 +555,20 @@ module Deftones
|
|
|
284
555
|
end
|
|
285
556
|
|
|
286
557
|
def encoder_backend_for(format)
|
|
558
|
+
return codec_backend if codec_backend&.respond_to?(:encode)
|
|
287
559
|
return :ffmpeg if executable_available?("ffmpeg")
|
|
288
560
|
return :afconvert if format == :mp3 && executable_available?("afconvert")
|
|
289
561
|
|
|
290
562
|
nil
|
|
291
563
|
end
|
|
292
564
|
|
|
293
|
-
def decoder_command(backend, input_path, output_path)
|
|
565
|
+
def decoder_command(backend, input_path, output_path, sample_rate: nil, channels: nil)
|
|
294
566
|
case backend
|
|
295
567
|
when :ffmpeg
|
|
296
|
-
["ffmpeg", "-v", "error", "-y", "-i", input_path, "-
|
|
568
|
+
command = ["ffmpeg", "-v", "error", "-y", "-i", input_path, "-vn", "-map", "0:a:0", "-acodec", "pcm_f32le"]
|
|
569
|
+
command += ["-ar", sample_rate.to_i.to_s] if sample_rate
|
|
570
|
+
command += ["-ac", channels.to_i.to_s] if channels
|
|
571
|
+
command + ["-f", "wav", output_path]
|
|
297
572
|
when :afconvert
|
|
298
573
|
["afconvert", "-f", "WAVE", "-d", "LEI16", input_path, output_path]
|
|
299
574
|
else
|
|
@@ -301,11 +576,17 @@ module Deftones
|
|
|
301
576
|
end
|
|
302
577
|
end
|
|
303
578
|
|
|
304
|
-
def encoder_command(backend, input_path, output_path, format)
|
|
579
|
+
def encoder_command(backend, input_path, output_path, format, sample_rate, channels)
|
|
305
580
|
case backend
|
|
306
581
|
when :ffmpeg
|
|
307
582
|
container = format == :ogg ? "ogg" : format.to_s
|
|
308
|
-
|
|
583
|
+
sample_format = format == :mp3 ? "s16p" : "s16"
|
|
584
|
+
codec = format == :mp3 ? "libmp3lame" : "flac"
|
|
585
|
+
[
|
|
586
|
+
"ffmpeg", "-v", "error", "-y", "-i", input_path, "-vn", "-map", "0:a:0",
|
|
587
|
+
"-codec:a", codec, "-sample_fmt", sample_format, "-ar", sample_rate.to_s, "-ac", channels.to_s,
|
|
588
|
+
"-f", container, output_path
|
|
589
|
+
]
|
|
309
590
|
when :afconvert
|
|
310
591
|
raise ArgumentError, "afconvert only supports mp3 export" unless format == :mp3
|
|
311
592
|
|
|
@@ -322,6 +603,30 @@ module Deftones
|
|
|
322
603
|
end
|
|
323
604
|
end
|
|
324
605
|
|
|
606
|
+
def capture_codec_command(*command)
|
|
607
|
+
Timeout.timeout(codec_timeout || DEFAULT_CODEC_TIMEOUT) do
|
|
608
|
+
Open3.capture3(*command)
|
|
609
|
+
end
|
|
610
|
+
rescue Timeout::Error
|
|
611
|
+
raise ArgumentError, "Codec command timed out after #{codec_timeout || DEFAULT_CODEC_TIMEOUT} seconds"
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def custom_codec_backend?(backend)
|
|
615
|
+
!backend.is_a?(Symbol)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def raise_codec_command_error(prefix, command, stdout, stderr, status)
|
|
619
|
+
detail = [stderr, stdout].map(&:to_s).map(&:strip).reject(&:empty?).first || "unknown codec error"
|
|
620
|
+
exit_status = status.respond_to?(:exitstatus) && status.exitstatus ? " (exit #{status.exitstatus})" : ""
|
|
621
|
+
raise Deftones::CodecCommandError.new(
|
|
622
|
+
"#{prefix}: #{detail}#{exit_status}",
|
|
623
|
+
command: command,
|
|
624
|
+
stdout: stdout,
|
|
625
|
+
stderr: stderr,
|
|
626
|
+
status: status
|
|
627
|
+
)
|
|
628
|
+
end
|
|
629
|
+
|
|
325
630
|
def missing_decoder_message(extension)
|
|
326
631
|
"No decoder available for #{extension}. Install ffmpeg to enable compressed audio loading."
|
|
327
632
|
end
|
|
@@ -330,10 +635,18 @@ module Deftones
|
|
|
330
635
|
"No encoder available for #{format}. Install ffmpeg to enable compressed audio export."
|
|
331
636
|
end
|
|
332
637
|
|
|
333
|
-
def resolve_save_format(path, format)
|
|
334
|
-
return normalize_format(format) if format
|
|
335
|
-
|
|
638
|
+
def resolve_save_format(path, format, on_format_mismatch:)
|
|
336
639
|
extension = File.extname(path).downcase
|
|
640
|
+
if format
|
|
641
|
+
normalized = normalize_format(format)
|
|
642
|
+
expected_extension = ".#{normalized}"
|
|
643
|
+
if on_format_mismatch == :error && !extension.empty? && extension != expected_extension
|
|
644
|
+
raise Deftones::UnsupportedAudioFormatError,
|
|
645
|
+
"Format #{normalized} does not match file extension #{extension}"
|
|
646
|
+
end
|
|
647
|
+
return normalized
|
|
648
|
+
end
|
|
649
|
+
|
|
337
650
|
return :mp3 if extension == ".mp3"
|
|
338
651
|
return :ogg if COMPRESSED_EXTENSIONS.include?(extension)
|
|
339
652
|
|
|
@@ -346,6 +659,14 @@ module Deftones
|
|
|
346
659
|
|
|
347
660
|
normalized
|
|
348
661
|
end
|
|
662
|
+
|
|
663
|
+
def validate_path_string!(path, role:)
|
|
664
|
+
string = path.to_s
|
|
665
|
+
raise ArgumentError, "#{role} path must not be empty" if string.empty?
|
|
666
|
+
raise ArgumentError, "#{role} path contains a null byte" if string.include?("\0")
|
|
667
|
+
|
|
668
|
+
true
|
|
669
|
+
end
|
|
349
670
|
end
|
|
350
671
|
end
|
|
351
672
|
end
|