Class: Wavify::Core::SampleBuffer

Inherits:
Object
  • Object
show all
Includes:
Enumerable, Enumerable[Numeric]
Defined in:
lib/wavify/core/sample_buffer.rb,
sig/core.rbs

Overview

Immutable interleaved sample container with format metadata.

Samples are stored in interleaved order (L, R, L, R, ...) for multi-channel audio. Packed buffers remain packed when enumerated, compared, hashed, or converted to the same format without dither. Random-access operations (samples, frame_view, view, slice, concat, reverse, and format-changing convert) materialize them. Hashing samples a fixed number of positions, so it is O(1); collisions are resolved by full value equality, whose worst case is O(n).

Defined Under Namespace

Classes: FrameView, MutableFloatWorkspace, StorageState, View

Constant Summary collapse

PACKED_ENUMERATION_CHUNK_SAMPLES =

Number of packed samples unpacked per sequential enumeration batch.

4_096
STORAGE_TYPES =

Supported internal storage backends.

%i[array packed].freeze
HASH_PROBE_COUNT =

Number of representative samples included in the constant-time hash.

8

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(samples, format, storage: :array) ⇒ SampleBuffer

Returns a new instance of SampleBuffer.

Parameters:

  • samples (Array<Numeric>)

    interleaved sample values

  • format (Format)

    sample encoding and channel layout

  • storage: (Symbol) (defaults to: :array)

Raises:



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/wavify/core/sample_buffer.rb', line 244

def initialize(samples, format, storage: :array)
  raise InvalidParameterError, "format must be Core::Format" unless format.is_a?(Format)
  raise InvalidParameterError, "samples must be an Array" unless samples.is_a?(Array)

  validate_samples!(samples)
  validate_interleaving!(samples.length, format.channels)
  storage = normalize_storage!(storage)

  @format = format
  coerced = coerce_samples(samples, format)
  @sample_count = coerced.length
  @value_hash = sample_value_hash(coerced, canonical_float: storage == :packed && format.sample_format == :float)
  @storage_mutex = Mutex.new
  @storage_state = if storage == :packed
                     storage_state(samples: nil, packed_samples: pack_samples(coerced, format).freeze, type: :packed)
                   else
                     storage_state(samples: coerced.freeze, packed_samples: nil, type: :array)
                   end
  @duration = Duration.from_samples(sample_frame_count, format.sample_rate)
end

Instance Attribute Details

#durationDuration (readonly)

Returns the value of attribute duration.

Returns:



240
241
242
# File 'lib/wavify/core/sample_buffer.rb', line 240

def duration
  @duration
end

#formatFormat (readonly)

Returns the value of attribute format.

Returns:



240
241
242
# File 'lib/wavify/core/sample_buffer.rb', line 240

def format
  @format
end

Class Method Details

.from_normalized_samples(samples, format, storage: :array) ⇒ SampleBuffer

Builds a buffer from normalized floating-point samples.

Parameters:

  • samples (Array[Numeric])
  • format (Format)
  • storage: (Symbol) (defaults to: :array)

Returns:



33
34
35
36
37
38
39
40
41
# File 'lib/wavify/core/sample_buffer.rb', line 33

def self.from_normalized_samples(samples, format, storage: :array)
  unless samples.is_a?(Array) && samples.all? do |sample|
    sample.is_a?(Numeric) && sample.real? && sample.respond_to?(:finite?) && sample.finite?
  end
    raise InvalidParameterError, "normalized samples must be finite real Numeric values"
  end

  new(samples.map(&:to_f), format, storage: storage)
end

.from_pcm_samples(samples, format, storage: :array) ⇒ SampleBuffer

Builds integer PCM samples without normalized-float ambiguity.

Parameters:

  • samples (Array[Numeric])
  • format (Format)
  • storage: (Symbol) (defaults to: :array)

Returns:



21
22
23
24
25
26
27
28
29
30
# File 'lib/wavify/core/sample_buffer.rb', line 21

def self.from_pcm_samples(samples, format, storage: :array)
  unless format.is_a?(Format) && format.sample_format == :pcm
    raise InvalidParameterError, "format must be a PCM Core::Format"
  end
  unless samples.is_a?(Array) && samples.all? { |sample| sample.is_a?(Integer) }
    raise InvalidParameterError, "PCM samples must be Integers"
  end

  new(samples, format, storage: storage)
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Value equality for immutable samples and format metadata.

Parameters:

  • other (Object)

Returns:

  • (Boolean)


303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/wavify/core/sample_buffer.rb', line 303

def ==(other)
  return true if equal?(other)
  return false unless other.is_a?(SampleBuffer) && @format == other.format && @sample_count == other.length

  array_samples, packed_bytes = storage_snapshot
  other_array_samples, other_packed_bytes = other.send(:storage_snapshot)
  return array_samples == other_array_samples if array_samples && other_array_samples

  if packed_bytes && other_packed_bytes
    return packed_bytes == other_packed_bytes if @format.sample_format == :pcm || packed_bytes == other_packed_bytes
  end

  sample_values_equal?(other)
end

#concat(other) ⇒ SampleBuffer Also known as: +

Parameters:

Returns:

Raises:



436
437
438
439
440
441
# File 'lib/wavify/core/sample_buffer.rb', line 436

def concat(other)
  raise InvalidParameterError, "other must be Core::SampleBuffer" unless other.is_a?(SampleBuffer)

  rhs = other.format == @format ? other : other.convert(@format)
  self.class.new(samples + rhs.samples, @format)
end

#convert(new_format, dither: false, dither_seed: nil, resampler: :linear) ⇒ SampleBuffer

Converts the buffer to another audio format/channels.

Parameters:

  • new_format (Format)
  • dither: (Boolean) (defaults to: false)
  • dither_seed: (Integer, nil) (defaults to: nil)
  • resampler: (Symbol) (defaults to: :linear)

Returns:



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/wavify/core/sample_buffer.rb', line 369

def convert(new_format, dither: false, dither_seed: nil, resampler: :linear)
  raise InvalidParameterError, "new_format must be Core::Format" unless new_format.is_a?(Format)
  return self if new_format == @format && !dither

  resampler = normalize_resampler!(resampler)
  dither_rng = dither_applicable?(new_format, dither) ? Random.new(dither_seed) : nil
  if @format.channels == new_format.channels && @format.sample_rate == new_format.sample_rate
    converted_samples = samples.map do |sample|
      normalized = to_normalized_float(sample, @format)
      from_normalized_float(normalized, new_format, dither_rng: dither_rng)
    end
    return self.class.new(converted_samples, new_format)
  end

  normalized_samples = samples.map { |sample| to_normalized_float(sample, @format) }
  converted_samples = convert_channels_interleaved(
    normalized_samples,
    source_channels: @format.channels,
    target_channels: new_format.channels,
    source_layout: @format.channel_layout
  )
  converted_samples = resample_interleaved(
    converted_samples,
    channels: new_format.channels,
    target_sample_rate: new_format.sample_rate,
    resampler: resampler
  )
  converted_samples.map! do |sample|
    from_normalized_float(sample, new_format, dither_rng: dither_rng)
  end

  self.class.new(converted_samples, new_format)
rescue InvalidParameterError, InvalidFormatError
  raise
rescue StandardError => e
  raise BufferConversionError, "failed to convert sample buffer: #{e.message}"
end

#eachSampleBuffer #eachEnumerator[Numeric, SampleBuffer]

Enumerates sample values in interleaved order.

Overloads:

Yields:

  • (sample)

Yield Parameters:

  • sample (Numeric)

Returns:

  • (Enumerator, Array<Numeric>)


329
330
331
332
333
334
335
# File 'lib/wavify/core/sample_buffer.rb', line 329

def each(&)
  return enum_for(:each) unless block_given?

  return each_packed_sample(&) if packed?

  @storage_state.samples.each(&)
end

#each_frame_sampleSampleBuffer #each_frame_sampleEnumerator[Numeric, SampleBuffer]

Iterates interleaved values without allocating a frame Array.

Overloads:

Yields:

Yield Parameters:

  • arg0 (Numeric)
  • arg1 (Integer)
  • arg2 (Integer)

Yield Returns:

  • (void)


281
282
283
284
285
286
287
288
# File 'lib/wavify/core/sample_buffer.rb', line 281

def each_frame_sample
  return enum_for(:each_frame_sample) unless block_given?

  each.with_index do |sample, sample_index|
    yield sample, sample_index / @format.channels, sample_index % @format.channels
  end
  self
end

#frame_viewFrameView

Returns a lazy sample-frame view without copying the full buffer.

Returns:



352
353
354
# File 'lib/wavify/core/sample_buffer.rb', line 352

def frame_view
  FrameView.new(samples, @format.channels)
end

#hashInteger

Returns:

  • (Integer)


320
321
322
# File 'lib/wavify/core/sample_buffer.rb', line 320

def hash
  @value_hash
end

#lengthInteger Also known as: size

Returns number of interleaved samples.

Returns:

  • (Integer)

    number of interleaved samples



338
339
340
# File 'lib/wavify/core/sample_buffer.rb', line 338

def length
  @sample_count
end

#packed?Boolean

Returns:

  • (Boolean)


294
295
296
# File 'lib/wavify/core/sample_buffer.rb', line 294

def packed?
  @storage_state.type == :packed
end

#packed_bytesizeInteger

Returns:

  • (Integer)


298
299
300
# File 'lib/wavify/core/sample_buffer.rb', line 298

def packed_bytesize
  @storage_state.packed_samples&.bytesize || 0
end

#reverseSampleBuffer

Reverses sample frame order while preserving channel ordering per frame.

Returns:



410
411
412
413
414
415
416
417
418
# File 'lib/wavify/core/sample_buffer.rb', line 410

def reverse
  reversed = []
  channels = @format.channels
  source_samples = samples
  (source_samples.length - channels).step(0, -channels) do |sample_index|
    reversed.concat(source_samples.slice(sample_index, channels))
  end
  self.class.new(reversed, @format)
end

#sample_frame_countInteger

Returns number of audio frames.

Returns:

  • (Integer)

    number of audio frames



345
346
347
# File 'lib/wavify/core/sample_buffer.rb', line 345

def sample_frame_count
  @sample_count / @format.channels
end

#samplesArray[Numeric]

Materializes packed storage on first random-access use.

Returns:

  • (Array[Numeric])


266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/wavify/core/sample_buffer.rb', line 266

def samples
  state = @storage_state
  return state.samples if state.samples

  @storage_mutex.synchronize do
    state = @storage_state
    return state.samples if state.samples

    materialized = unpack_samples(state.packed_samples, @format).freeze
    @storage_state = storage_state(samples: materialized, packed_samples: nil, type: :array)
  end
  @storage_state.samples
end

#slice(start_frame, frame_length) ⇒ SampleBuffer

Slices the buffer by frame index and frame count.

Parameters:

  • start_frame (Integer)
  • frame_length (Integer)

Returns:



425
426
427
428
429
430
431
432
433
434
# File 'lib/wavify/core/sample_buffer.rb', line 425

def slice(start_frame, frame_length)
  unless start_frame.is_a?(Integer) && start_frame >= 0
    raise InvalidParameterError, "start_frame must be a non-negative Integer: #{start_frame.inspect}"
  end
  unless frame_length.is_a?(Integer) && frame_length >= 0
    raise InvalidParameterError, "frame_length must be a non-negative Integer: #{frame_length.inspect}"
  end

  view(start_frame: start_frame, frame_length: frame_length).to_sample_buffer
end

#storageSymbol

Returns:

  • (Symbol)


290
291
292
# File 'lib/wavify/core/sample_buffer.rb', line 290

def storage
  @storage_state.type
end

#view(start_frame: 0, frame_length: nil) ⇒ View

Returns a lazy sample-buffer view without copying the selected frames.

Parameters:

  • start_frame (Integer) (defaults to: 0)
  • frame_length (Integer, nil) (defaults to: nil)
  • start_frame: (Integer) (defaults to: 0)
  • frame_length: (Integer, nil) (defaults to: nil)

Returns:



361
362
363
# File 'lib/wavify/core/sample_buffer.rb', line 361

def view(start_frame: 0, frame_length: nil)
  View.new(samples, @format, start_frame: start_frame, frame_count: frame_length)
end