Class: Deftones::IO::Buffer

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/deftones/io/buffer.rb

Defined Under Namespace

Classes: Statistics

Constant Summary collapse

COMPRESSED_EXTENSIONS =
%w[.mp3 .ogg .oga].freeze
SAVEABLE_FORMATS =
%i[wav mp3 ogg].freeze
DEFAULT_CODEC_TIMEOUT =
30.0
INTERPOLATION_MODES =
%i[linear nearest cubic sinc_lite].freeze
SINC_LITE_RADIUS =
8
WAV_BIT_DEPTHS =
[16, 24, 32].freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(samples, channels:, sample_rate:, interpolation: :linear) ⇒ Buffer

Returns a new instance of Buffer.



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/deftones/io/buffer.rb', line 88

def initialize(samples, channels:, sample_rate:, interpolation: :linear)
  @samples = samples.map(&:to_f)
  @channels = channels
  @sample_rate = sample_rate
  @interpolation = normalize_interpolation(interpolation)
  @disposed = false
  @mono_cache = nil
  @peak_cache = nil
  @rms_cache = nil
  @statistics_cache = {}
end

Class Attribute Details

.codec_backendObject

Returns the value of attribute codec_backend.



23
24
25
# File 'lib/deftones/io/buffer.rb', line 23

def codec_backend
  @codec_backend
end

.codec_timeoutObject

Returns the value of attribute codec_timeout.



23
24
25
# File 'lib/deftones/io/buffer.rb', line 23

def codec_timeout
  @codec_timeout
end

Instance Attribute Details

#channelsObject (readonly)

Returns the value of attribute channels.



12
13
14
# File 'lib/deftones/io/buffer.rb', line 12

def channels
  @channels
end

#interpolationObject

Returns the value of attribute interpolation.



12
13
14
# File 'lib/deftones/io/buffer.rb', line 12

def interpolation
  @interpolation
end

#sample_rateObject (readonly)

Returns the value of attribute sample_rate.



12
13
14
# File 'lib/deftones/io/buffer.rb', line 12

def sample_rate
  @sample_rate
end

#samplesObject (readonly)

Returns the value of attribute samples.



12
13
14
# File 'lib/deftones/io/buffer.rb', line 12

def samples
  @samples
end

Class Method Details

.capture_codec_command(*command) ⇒ Object (private)



606
607
608
609
610
611
612
# File 'lib/deftones/io/buffer.rb', line 606

def capture_codec_command(*command)
  Timeout.timeout(codec_timeout || DEFAULT_CODEC_TIMEOUT) do
    Open3.capture3(*command)
  end
rescue Timeout::Error
  raise ArgumentError, "Codec command timed out after #{codec_timeout || DEFAULT_CODEC_TIMEOUT} seconds"
end

.compressed_audio_available?Boolean Also known as: compressedAudioAvailable

Returns:

  • (Boolean)


64
65
66
67
68
69
# File 'lib/deftones/io/buffer.rb', line 64

def self.compressed_audio_available?
  return true if codec_backend&.respond_to?(:decode)
  return true if codec_backend&.respond_to?(:encode)

  send(:executable_available?, "ffmpeg") || send(:executable_available?, "afconvert")
end

.custom_codec_backend?(backend) ⇒ Boolean (private)

Returns:

  • (Boolean)


614
615
616
# File 'lib/deftones/io/buffer.rb', line 614

def custom_codec_backend?(backend)
  !backend.is_a?(Symbol)
end

.decoder_backend_for(extension) ⇒ Object (private)



549
550
551
552
553
554
555
# File 'lib/deftones/io/buffer.rb', line 549

def decoder_backend_for(extension)
  return codec_backend if codec_backend&.respond_to?(:decode)
  return :ffmpeg if executable_available?("ffmpeg")
  return :afconvert if extension == ".mp3" && executable_available?("afconvert")

  nil
end

.decoder_command(backend, input_path, output_path, sample_rate: nil, channels: nil) ⇒ Object (private)



565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'lib/deftones/io/buffer.rb', line 565

def decoder_command(backend, input_path, output_path, sample_rate: nil, channels: nil)
  case backend
  when :ffmpeg
    command = ["ffmpeg", "-v", "error", "-y", "-i", input_path, "-vn", "-map", "0:a:0", "-acodec", "pcm_f32le"]
    command += ["-ar", sample_rate.to_i.to_s] if sample_rate
    command += ["-ac", channels.to_i.to_s] if channels
    command + ["-f", "wav", output_path]
  when :afconvert
    ["afconvert", "-f", "WAVE", "-d", "LEI16", input_path, output_path]
  else
    raise ArgumentError, "Unknown decoder backend: #{backend}"
  end
end

.encoder_backend_for(format) ⇒ Object (private)



557
558
559
560
561
562
563
# File 'lib/deftones/io/buffer.rb', line 557

def encoder_backend_for(format)
  return codec_backend if codec_backend&.respond_to?(:encode)
  return :ffmpeg if executable_available?("ffmpeg")
  return :afconvert if format == :mp3 && executable_available?("afconvert")

  nil
end

.encoder_command(backend, input_path, output_path, format, sample_rate, channels) ⇒ Object (private)



579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/deftones/io/buffer.rb', line 579

def encoder_command(backend, input_path, output_path, format, sample_rate, channels)
  case backend
  when :ffmpeg
    container = format == :ogg ? "ogg" : format.to_s
    sample_format = format == :mp3 ? "s16p" : "s16"
    codec = format == :mp3 ? "libmp3lame" : "flac"
    [
      "ffmpeg", "-v", "error", "-y", "-i", input_path, "-vn", "-map", "0:a:0",
      "-codec:a", codec, "-sample_fmt", sample_format, "-ar", sample_rate.to_s, "-ac", channels.to_s,
      "-f", container, output_path
    ]
  when :afconvert
    raise ArgumentError, "afconvert only supports mp3 export" unless format == :mp3

    ["afconvert", "-f", "MPG3", "-d", ".mp3", input_path, output_path]
  else
    raise ArgumentError, "Unknown encoder backend: #{backend}"
  end
end

.ensure_wav_backend!Object (private)



517
518
519
520
521
522
# File 'lib/deftones/io/buffer.rb', line 517

def ensure_wav_backend!
  return if Deftones.wavify_available?

  raise Deftones::MissingCodecBackendError,
        "WAV codec backend is unavailable. Install the wavify gem to load or save WAV audio."
end

.executable_available?(name) ⇒ Boolean (private)

Returns:

  • (Boolean)


599
600
601
602
603
604
# File 'lib/deftones/io/buffer.rb', line 599

def executable_available?(name)
  ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |directory|
    executable = File.join(directory, name)
    File.file?(executable) && File.executable?(executable)
  end
end

.from_array(samples, sample_rate: Context::DEFAULT_SAMPLE_RATE, channels: nil, interpolation: :linear) ⇒ Object Also known as: fromArray



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/deftones/io/buffer.rb', line 37

def self.from_array(samples, sample_rate: Context::DEFAULT_SAMPLE_RATE, channels: nil, interpolation: :linear)
  if samples.first.is_a?(Array)
    channel_count = channels || samples.length
    frame_count = samples.map(&:length).max || 0
    interleaved = Array.new(frame_count * channel_count, 0.0)

    frame_count.times do |frame_index|
      channel_count.times do |channel_index|
        source_channel = samples[channel_index] || []
        interleaved[(frame_index * channel_count) + channel_index] = source_channel[frame_index].to_f
      end
    end

    new(interleaved, channels: channel_count, sample_rate: sample_rate, interpolation: interpolation)
  else
    from_mono(samples, channels: channels || 1, sample_rate: sample_rate, interpolation: interpolation)
  end
end

.from_mono(samples, channels: 1, sample_rate: Context::DEFAULT_SAMPLE_RATE, interpolation: :linear) ⇒ Object



32
33
34
35
# File 'lib/deftones/io/buffer.rb', line 32

def self.from_mono(samples, channels: 1, sample_rate: Context::DEFAULT_SAMPLE_RATE, interpolation: :linear)
  interleaved = channels == 1 ? samples : interleave(samples, channels)
  new(interleaved, channels: channels, sample_rate: sample_rate, interpolation: interpolation)
end

.from_url(path) ⇒ Object Also known as: fromUrl



56
57
58
# File 'lib/deftones/io/buffer.rb', line 56

def self.from_url(path)
  load(path)
end

.interleave(mono_samples, channels) ⇒ Object



26
27
28
29
30
# File 'lib/deftones/io/buffer.rb', line 26

def self.interleave(mono_samples, channels)
  return mono_samples.dup if channels == 1

  mono_samples.flat_map { |sample| Array.new(channels, sample) }
end

.load(source, sample_rate: nil, channels: nil) ⇒ Object



77
78
79
80
81
82
83
84
85
86
# File 'lib/deftones/io/buffer.rb', line 77

def self.load(source, sample_rate: nil, channels: nil)
  return load_io(source) if source.respond_to?(:read) && !source.is_a?(String)

  validate_path_string!(source, role: "audio source")
  extension = File.extname(source).downcase
  return load_wav(source) if extension == ".wav"
  return load_compressed(source, extension, sample_rate: sample_rate, channels: channels) if COMPRESSED_EXTENSIONS.include?(extension)

  raise Deftones::UnsupportedAudioFormatError, "Unsupported audio format: #{extension}"
end

.load_compressed(path, extension, sample_rate: nil, channels: nil) ⇒ Object (private)



494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/deftones/io/buffer.rb', line 494

def load_compressed(path, extension, sample_rate: nil, channels: nil)
  validate_path_string!(path, role: "compressed audio source")
  backend = decoder_backend_for(extension)
  raise Deftones::MissingCodecBackendError, missing_decoder_message(extension) unless backend

  Tempfile.create(["deftones-buffer", ".wav"]) do |tempfile|
    tempfile.close
    if custom_codec_backend?(backend)
      decode_options = { extension: extension }
      decode_options[:sample_rate] = sample_rate if sample_rate
      decode_options[:channels] = channels if channels
      backend.decode(path, tempfile.path, **decode_options)
      next load_wav(tempfile.path)
    end

    command = decoder_command(backend, path, tempfile.path, sample_rate: sample_rate, channels: channels)
    stdout, stderr, status = capture_codec_command(*command)
    next load_wav(tempfile.path) if status.success?

    raise_codec_command_error("Failed to decode #{extension}", command, stdout, stderr, status)
  end
end

.load_io(io) ⇒ Object (private)



483
484
485
486
487
488
489
490
491
492
# File 'lib/deftones/io/buffer.rb', line 483

def load_io(io)
  extension = io.respond_to?(:path) ? File.extname(io.path).downcase : ".wav"
  extension = ".wav" if extension.empty?
  Tempfile.create(["deftones-buffer-load", extension]) do |tempfile|
    tempfile.binmode
    tempfile.write(io.read)
    tempfile.close
    load(tempfile.path)
  end
end

.load_wav(path) ⇒ Object (private)



469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/deftones/io/buffer.rb', line 469

def load_wav(path)
  validate_path_string!(path, role: "WAV source")
  ensure_wav_backend!

  sample_buffer = Wavify::Codecs::Wav.read(path)
  float_buffer = sample_buffer.convert(wavify_work_format(sample_buffer.format.channels, sample_buffer.format.sample_rate))
  new(float_buffer.samples, channels: float_buffer.format.channels, sample_rate: float_buffer.format.sample_rate)
rescue StandardError => error
  raise if error.is_a?(Deftones::MissingCodecBackendError)
  raise unless defined?(Wavify::Error) && error.is_a?(Wavify::Error)

  raise ArgumentError, "Failed to load WAV: #{error.message}"
end

.loadedObject



60
61
62
# File 'lib/deftones/io/buffer.rb', line 60

def self.loaded
  true
end

.missing_decoder_message(extension) ⇒ Object (private)



630
631
632
# File 'lib/deftones/io/buffer.rb', line 630

def missing_decoder_message(extension)
  "No decoder available for #{extension}. Install ffmpeg to enable compressed audio loading."
end

.missing_encoder_message(format) ⇒ Object (private)



634
635
636
# File 'lib/deftones/io/buffer.rb', line 634

def missing_encoder_message(format)
  "No encoder available for #{format}. Install ffmpeg to enable compressed audio export."
end

.normalize_format(format) ⇒ Object (private)



656
657
658
659
660
661
# File 'lib/deftones/io/buffer.rb', line 656

def normalize_format(format)
  normalized = format.to_sym
  return :ogg if normalized == :oga

  normalized
end

.raise_codec_command_error(prefix, command, stdout, stderr, status) ⇒ Object (private)



618
619
620
621
622
623
624
625
626
627
628
# File 'lib/deftones/io/buffer.rb', line 618

def raise_codec_command_error(prefix, command, stdout, stderr, status)
  detail = [stderr, stdout].map(&:to_s).map(&:strip).reject(&:empty?).first || "unknown codec error"
  exit_status = status.respond_to?(:exitstatus) && status.exitstatus ? " (exit #{status.exitstatus})" : ""
  raise Deftones::CodecCommandError.new(
    "#{prefix}: #{detail}#{exit_status}",
    command: command,
    stdout: stdout,
    stderr: stderr,
    status: status
  )
end

.resolve_save_format(path, format, on_format_mismatch:) ⇒ Object (private)



638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'lib/deftones/io/buffer.rb', line 638

def resolve_save_format(path, format, on_format_mismatch:)
  extension = File.extname(path).downcase
  if format
    normalized = normalize_format(format)
    expected_extension = ".#{normalized}"
    if on_format_mismatch == :error && !extension.empty? && extension != expected_extension
      raise Deftones::UnsupportedAudioFormatError,
            "Format #{normalized} does not match file extension #{extension}"
    end
    return normalized
  end

  return :mp3 if extension == ".mp3"
  return :ogg if COMPRESSED_EXTENSIONS.include?(extension)

  :wav
end

.validate_path_string!(path, role:) ⇒ Object (private)

Raises:

  • (ArgumentError)


663
664
665
666
667
668
669
# File 'lib/deftones/io/buffer.rb', line 663

def validate_path_string!(path, role:)
  string = path.to_s
  raise ArgumentError, "#{role} path must not be empty" if string.empty?
  raise ArgumentError, "#{role} path contains a null byte" if string.include?("\0")

  true
end

.validate_wav_bit_depth(bit_depth) ⇒ Object (private)

Raises:

  • (ArgumentError)


542
543
544
545
546
547
# File 'lib/deftones/io/buffer.rb', line 542

def validate_wav_bit_depth(bit_depth)
  normalized = bit_depth.to_i
  return normalized if WAV_BIT_DEPTHS.include?(normalized)

  raise ArgumentError, "Unsupported WAV bit depth: #{bit_depth}"
end

.wavify_wav_format(channels, sample_rate, bit_depth) ⇒ Object (private)



533
534
535
536
537
538
539
540
# File 'lib/deftones/io/buffer.rb', line 533

def wavify_wav_format(channels, sample_rate, bit_depth)
  Wavify::Core::Format.new(
    channels: channels,
    sample_rate: sample_rate,
    bit_depth: bit_depth,
    sample_format: :pcm
  )
end

.wavify_work_format(channels, sample_rate) ⇒ Object (private)



524
525
526
527
528
529
530
531
# File 'lib/deftones/io/buffer.rb', line 524

def wavify_work_format(channels, sample_rate)
  Wavify::Core::Format.new(
    channels: channels,
    sample_rate: sample_rate,
    bit_depth: 32,
    sample_format: :float
  )
end

Instance Method Details

#[](frame_index, channel = nil) ⇒ Object



170
171
172
173
174
# File 'lib/deftones/io/buffer.rb', line 170

def [](frame_index, channel = nil)
  return mono[frame_index] if channel.nil?

  @samples[(frame_index * @channels) + channel]
end

#clip_count(threshold = 1.0) ⇒ Object



156
157
158
159
# File 'lib/deftones/io/buffer.rb', line 156

def clip_count(threshold = 1.0)
  limit = threshold.to_f.abs
  @samples.count { |sample| sample.abs >= limit }
end

#cubic_sample_at(clamped_position, channel) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/deftones/io/buffer.rb', line 265

def cubic_sample_at(clamped_position, channel)
  base = clamped_position.floor
  fraction = clamped_position - base
  p0 = self[[base - 1, 0].max, channel]
  p1 = self[base, channel]
  p2 = self[[base + 1, frames - 1].min, channel]
  p3 = self[[base + 2, frames - 1].min, channel]
  a0 = (-0.5 * p0) + (1.5 * p1) - (1.5 * p2) + (0.5 * p3)
  a1 = p0 - (2.5 * p1) + (2.0 * p2) - (0.5 * p3)
  a2 = (-0.5 * p0) + (0.5 * p2)
  a3 = p1
  (((a0 * fraction) + a1) * fraction * fraction) + (a2 * fraction) + a3
end

#disposeObject



356
357
358
359
360
361
362
363
364
# File 'lib/deftones/io/buffer.rb', line 356

def dispose
  @samples = []
  @mono_cache = nil
  @peak_cache = nil
  @rms_cache = nil
  @statistics_cache.clear
  @disposed = true
  self
end

#dithered_samples(bit_depth, rng) ⇒ Object (private)



458
459
460
461
462
463
464
# File 'lib/deftones/io/buffer.rb', line 458

def dithered_samples(bit_depth, rng)
  random = rng || Random
  step = 1.0 / ((2**(bit_depth - 1)) - 1)
  @samples.map do |sample|
    Deftones::DSP::Helpers.clamp(sample + ((random.rand - random.rand) * step), -1.0, 1.0)
  end
end

#durationObject



118
119
120
# File 'lib/deftones/io/buffer.rb', line 118

def duration
  frames.to_f / @sample_rate
end

#each(&block) ⇒ Object



100
101
102
103
104
# File 'lib/deftones/io/buffer.rb', line 100

def each(&block)
  return enum_for(:each) unless block

  mono.each(&block)
end

#each_frameObject



106
107
108
109
110
111
112
# File 'lib/deftones/io/buffer.rb', line 106

def each_frame
  return enum_for(:each_frame) unless block_given?

  frames.times do |frame_index|
    yield frame(frame_index)
  end
end

#frame(frame_index) ⇒ Object



176
177
178
179
# File 'lib/deftones/io/buffer.rb', line 176

def frame(frame_index)
  offset = frame_index * @channels
  @samples[offset, @channels]
end

#framesObject



114
115
116
# File 'lib/deftones/io/buffer.rb', line 114

def frames
  @samples.length / @channels
end

#get_channel_data(channel) ⇒ Object Also known as: getChannelData

Raises:

  • (ArgumentError)


185
186
187
188
189
190
# File 'lib/deftones/io/buffer.rb', line 185

def get_channel_data(channel)
  channel_index = channel.to_i
  raise ArgumentError, "channel is out of range" if channel_index.negative? || channel_index >= @channels

  Array.new(frames) { |frame_index| self[frame_index, channel_index] }
end

#hann_window(normalized_distance) ⇒ Object



305
306
307
308
309
310
# File 'lib/deftones/io/buffer.rb', line 305

def hann_window(normalized_distance)
  distance = normalized_distance.abs
  return 0.0 if distance >= 1.0

  0.5 * (1.0 + Math.cos(Math::PI * distance))
end

#integrated_lufsObject Also known as: integratedLufs



150
151
152
153
154
# File 'lib/deftones/io/buffer.rb', line 150

def integrated_lufs
  return -Float::INFINITY if rms <= 0.0

  (20.0 * Math.log10(rms)) - 0.691
end

#lengthObject



122
123
124
# File 'lib/deftones/io/buffer.rb', line 122

def length
  frames
end

#linear_sample_at(clamped_position, channel) ⇒ Object



256
257
258
259
260
261
262
263
# File 'lib/deftones/io/buffer.rb', line 256

def linear_sample_at(clamped_position, channel)
  lower = clamped_position.floor
  upper = [lower + 1, frames - 1].min
  fraction = clamped_position - lower
  lower_sample = self[lower, channel]
  upper_sample = self[upper, channel]
  Deftones::DSP::Helpers.lerp(lower_sample, upper_sample, fraction)
end

#loaded?Boolean

Returns:

  • (Boolean)


126
127
128
# File 'lib/deftones/io/buffer.rb', line 126

def loaded?
  !@disposed
end

#mixdownObject



352
353
354
# File 'lib/deftones/io/buffer.rb', line 352

def mixdown
  self.class.new(mono, channels: 1, sample_rate: @sample_rate, interpolation: @interpolation)
end

#monoObject



130
131
132
133
134
135
136
137
138
# File 'lib/deftones/io/buffer.rb', line 130

def mono
  return @samples if @channels == 1
  return @mono_cache if @mono_cache

  @mono_cache = Array.new(frames) do |frame|
    offset = frame * @channels
    @samples[offset, @channels].sum / @channels.to_f
  end
end

#new_like(samples, channels: @channels, sample_rate: @sample_rate) ⇒ Object (private)



395
396
397
# File 'lib/deftones/io/buffer.rb', line 395

def new_like(samples, channels: @channels, sample_rate: @sample_rate)
  self.class.new(samples, channels: channels, sample_rate: sample_rate, interpolation: @interpolation)
end

#normalize(target_peak = 0.99) ⇒ Object



331
332
333
334
335
336
337
# File 'lib/deftones/io/buffer.rb', line 331

def normalize(target_peak = 0.99)
  normalized_target = normalize_level_target(target_peak, "target peak")
  return new_like(@samples) if peak.zero?

  scale = normalized_target / peak
  new_like(@samples.map { |sample| sample * scale })
end

#normalize_interpolation(value) ⇒ Object (private)

Raises:

  • (ArgumentError)


399
400
401
402
403
404
# File 'lib/deftones/io/buffer.rb', line 399

def normalize_interpolation(value)
  normalized = value.to_sym
  return normalized if INTERPOLATION_MODES.include?(normalized)

  raise ArgumentError, "Unsupported interpolation mode: #{value}"
end

#normalize_level_target(value, name) ⇒ Object (private)

Raises:

  • (ArgumentError)


406
407
408
409
410
411
# File 'lib/deftones/io/buffer.rb', line 406

def normalize_level_target(value, name)
  normalized = value.to_f
  raise ArgumentError, "#{name} must be finite and non-negative" unless normalized.finite? && !normalized.negative?

  normalized
end

#normalize_lufs(target_lufs = -14.0)) ⇒ Object Also known as: normalizeLufs



347
348
349
350
# File 'lib/deftones/io/buffer.rb', line 347

def normalize_lufs(target_lufs = -14.0)
  target_gain = 10.0**((target_lufs.to_f + 0.691) / 20.0)
  normalize_rms(target_gain)
end

#normalize_rms(target_rms = 0.2) ⇒ Object Also known as: normalizeRms



339
340
341
342
343
344
345
# File 'lib/deftones/io/buffer.rb', line 339

def normalize_rms(target_rms = 0.2)
  normalized_target = normalize_level_target(target_rms, "target RMS")
  return new_like(@samples) if rms.zero?

  scale = normalized_target / rms
  new_like(@samples.map { |sample| sample * scale })
end

#number_of_channelsObject Also known as: numberOfChannels



181
182
183
# File 'lib/deftones/io/buffer.rb', line 181

def number_of_channels
  @channels
end

#peakObject



140
141
142
# File 'lib/deftones/io/buffer.rb', line 140

def peak
  @peak_cache ||= @samples.map(&:abs).max || 0.0
end

#resample(target_sample_rate, interpolation: @interpolation) ⇒ Object Also known as: resampleTo

Raises:

  • (ArgumentError)


234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/deftones/io/buffer.rb', line 234

def resample(target_sample_rate, interpolation: @interpolation)
  normalized_sample_rate = target_sample_rate.to_f
  raise ArgumentError, "sample rate must be positive" unless normalized_sample_rate.positive? && normalized_sample_rate.finite?
  return new_like(@samples) if normalized_sample_rate == @sample_rate.to_f

  target_frames = (frames * (normalized_sample_rate / @sample_rate.to_f)).round
  channel_data = Array.new(@channels) do |channel_index|
    Array.new(target_frames) do |frame_index|
      source_position = frame_index * (@sample_rate.to_f / normalized_sample_rate)
      sample_at(source_position, channel_index, interpolation: interpolation)
    end
  end
  self.class.from_array(
    channel_data,
    sample_rate: normalized_sample_rate.round,
    channels: @channels,
    interpolation: normalize_interpolation(interpolation)
  )
end

#reverseObject



326
327
328
329
# File 'lib/deftones/io/buffer.rb', line 326

def reverse
  reversed_frames = each_frame.to_a.reverse.flatten
  new_like(reversed_frames)
end

#rmsObject



144
145
146
147
148
# File 'lib/deftones/io/buffer.rb', line 144

def rms
  return 0.0 if @samples.empty?

  @rms_cache ||= Math.sqrt(@samples.sum { |sample| sample * sample } / @samples.length)
end

#sample_at(frame_position, channel = 0, interpolation: @interpolation) ⇒ Object Also known as: sampleAt



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/deftones/io/buffer.rb', line 200

def sample_at(frame_position, channel = 0, interpolation: @interpolation)
  return 0.0 if @samples.empty?

  clamped_position = Deftones::DSP::Helpers.clamp(frame_position.to_f, 0.0, [frames - 1, 0].max)
  channel_index = [channel, @channels - 1].min
  case normalize_interpolation(interpolation)
  when :nearest
    self[clamped_position.round, channel_index]
  when :cubic
    cubic_sample_at(clamped_position, channel_index)
  when :sinc_lite
    sinc_lite_sample_at(clamped_position, channel_index)
  else
    linear_sample_at(clamped_position, channel_index)
  end
end

#sample_at_cubic(frame_position, channel = 0) ⇒ Object Also known as: sampleAtCubic



221
222
223
# File 'lib/deftones/io/buffer.rb', line 221

def sample_at_cubic(frame_position, channel = 0)
  sample_at(frame_position, channel, interpolation: :cubic)
end

#sample_at_nearest(frame_position, channel = 0) ⇒ Object Also known as: sampleAtNearest



217
218
219
# File 'lib/deftones/io/buffer.rb', line 217

def sample_at_nearest(frame_position, channel = 0)
  sample_at(frame_position, channel, interpolation: :nearest)
end

#sample_at_sinc_lite(frame_position, channel = 0) ⇒ Object Also known as: sampleAtSincLite



225
226
227
# File 'lib/deftones/io/buffer.rb', line 225

def sample_at_sinc_lite(frame_position, channel = 0)
  sample_at(frame_position, channel, interpolation: :sinc_lite)
end

#save(target, format: nil, on_format_mismatch: :error, bit_depth: 16, dither: false, dither_rng: nil) ⇒ Object



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/deftones/io/buffer.rb', line 375

def save(target, format: nil, on_format_mismatch: :error, bit_depth: 16, dither: false, dither_rng: nil)
  if target.respond_to?(:write) && !target.is_a?(String)
    return save_io(target, format: format || :wav, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
  end

  self.class.send(:validate_path_string!, target, role: "audio target")
  resolved_format = self.class.send(:resolve_save_format, target, format, on_format_mismatch: on_format_mismatch)
  raise Deftones::UnsupportedAudioFormatError, "Unsupported format: #{resolved_format}" unless SAVEABLE_FORMATS.include?(resolved_format)

  case resolved_format
  when :wav
    save_wav(target, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
  when :mp3, :ogg
    save_compressed(target, resolved_format, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
  end
  target
end

#save_compressed(path, format, bit_depth:, dither:, dither_rng:) ⇒ Object (private)



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/deftones/io/buffer.rb', line 438

def save_compressed(path, format, bit_depth:, dither:, dither_rng:)
  backend = self.class.send(:encoder_backend_for, format)
  raise Deftones::MissingCodecBackendError, self.class.send(:missing_encoder_message, format) unless backend

  Tempfile.create(["deftones-buffer-export", ".wav"]) do |tempfile|
    tempfile.close
    save_wav(tempfile.path, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
    if self.class.send(:custom_codec_backend?, backend)
      backend.encode(tempfile.path, path, format: format, sample_rate: @sample_rate, channels: @channels)
      return
    end

    command = self.class.send(:encoder_command, backend, tempfile.path, path, format, @sample_rate, @channels)
    stdout, stderr, status = self.class.send(:capture_codec_command, *command)
    return if status.success?

    self.class.send(:raise_codec_command_error, "Failed to encode #{format}", command, stdout, stderr, status)
  end
end

#save_io(io, format:, bit_depth:, dither:, dither_rng:) ⇒ Object (private)



413
414
415
416
417
418
419
420
# File 'lib/deftones/io/buffer.rb', line 413

def save_io(io, format:, bit_depth:, dither:, dither_rng:)
  Tempfile.create(["deftones-buffer-save", ".#{format}"]) do |tempfile|
    tempfile.close
    save(tempfile.path, format: format, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
    io.write(File.binread(tempfile.path))
  end
  io
end

#save_wav(path, bit_depth:, dither:, dither_rng:) ⇒ Object (private)



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/deftones/io/buffer.rb', line 422

def save_wav(path, bit_depth:, dither:, dither_rng:)
  self.class.send(:ensure_wav_backend!)
  normalized_bit_depth = self.class.send(:validate_wav_bit_depth, bit_depth)
  output_samples = dither ? dithered_samples(normalized_bit_depth, dither_rng) : @samples

  sample_buffer = Wavify::Core::SampleBuffer.new(
    output_samples,
    self.class.send(:wavify_work_format, @channels, @sample_rate)
  )
  Wavify::Codecs::Wav.write(
    path,
    sample_buffer,
    format: self.class.send(:wavify_wav_format, @channels, @sample_rate, normalized_bit_depth)
  )
end

#sinc(value) ⇒ Object



298
299
300
301
302
303
# File 'lib/deftones/io/buffer.rb', line 298

def sinc(value)
  return 1.0 if value.abs < 1.0e-12

  x = Math::PI * value
  Math.sin(x) / x
end

#sinc_lite_sample_at(clamped_position, channel) ⇒ Object



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/deftones/io/buffer.rb', line 279

def sinc_lite_sample_at(clamped_position, channel)
  center = clamped_position.floor
  weighted_sum = 0.0
  weight_total = 0.0

  ((center - SINC_LITE_RADIUS + 1)..(center + SINC_LITE_RADIUS)).each do |index|
    next if index.negative? || index >= frames

    distance = clamped_position - index
    weight = sinc(distance) * hann_window(distance / SINC_LITE_RADIUS)
    weighted_sum += self[index, channel] * weight
    weight_total += weight
  end

  return linear_sample_at(clamped_position, channel) if weight_total.abs < 1.0e-12

  weighted_sum / weight_total
end

#slice(start_frame, length) ⇒ Object



312
313
314
315
316
317
318
# File 'lib/deftones/io/buffer.rb', line 312

def slice(start_frame, length)
  frame_count = [length.to_i, 0].max
  first_frame = [start_frame.to_i, 0].max
  offset = first_frame * @channels
  subset = @samples.slice(offset, frame_count * @channels) || []
  new_like(subset)
end

#slice_seconds(start_time, duration) ⇒ Object Also known as: sliceSeconds



320
321
322
323
324
# File 'lib/deftones/io/buffer.rb', line 320

def slice_seconds(start_time, duration)
  start_frame = (Deftones::Music::Time.parse(start_time) * @sample_rate).floor
  frame_count = (Deftones::Music::Time.parse(duration) * @sample_rate).ceil
  slice(start_frame, frame_count)
end

#statistics(clip_threshold: 1.0) ⇒ Object Also known as: stats



161
162
163
164
165
166
167
168
# File 'lib/deftones/io/buffer.rb', line 161

def statistics(clip_threshold: 1.0)
  threshold = clip_threshold.to_f.abs
  @statistics_cache[threshold] ||= Statistics.new(
    peak: peak,
    rms: rms,
    clip_count: clip_count(threshold)
  )
end

#to_arrayObject Also known as: toArray



192
193
194
# File 'lib/deftones/io/buffer.rb', line 192

def to_array
  Array.new(@channels) { |channel_index| get_channel_data(channel_index) }
end