Class: Wavify::Core::SampleBuffer
- Inherits:
-
Object
- Object
- Wavify::Core::SampleBuffer
- 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
-
#duration ⇒ Duration
readonly
Returns the value of attribute duration.
-
#format ⇒ Format
readonly
Returns the value of attribute format.
Class Method Summary collapse
-
.from_normalized_samples(samples, format, storage: :array) ⇒ SampleBuffer
Builds a buffer from normalized floating-point samples.
-
.from_pcm_samples(samples, format, storage: :array) ⇒ SampleBuffer
Builds integer PCM samples without normalized-float ambiguity.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
Value equality for immutable samples and format metadata.
- #concat(other) ⇒ SampleBuffer (also: #+)
-
#convert(new_format, dither: false, dither_seed: nil, resampler: :linear) ⇒ SampleBuffer
Converts the buffer to another audio format/channels.
-
#each {|sample| ... } ⇒ Enumerator, Array<Numeric>
Enumerates sample values in interleaved order.
-
#each_frame_sample {|arg0, arg1, arg2| ... } ⇒ Object
Iterates interleaved values without allocating a frame Array.
-
#frame_view ⇒ FrameView
Returns a lazy sample-frame view without copying the full buffer.
- #hash ⇒ Integer
-
#initialize(samples, format, storage: :array) ⇒ SampleBuffer
constructor
A new instance of SampleBuffer.
-
#length ⇒ Integer
(also: #size)
Number of interleaved samples.
- #packed? ⇒ Boolean
- #packed_bytesize ⇒ Integer
-
#reverse ⇒ SampleBuffer
Reverses sample frame order while preserving channel ordering per frame.
-
#sample_frame_count ⇒ Integer
Number of audio frames.
-
#samples ⇒ Array[Numeric]
Materializes packed storage on first random-access use.
-
#slice(start_frame, frame_length) ⇒ SampleBuffer
Slices the buffer by frame index and frame count.
- #storage ⇒ Symbol
-
#view(start_frame: 0, frame_length: nil) ⇒ View
Returns a lazy sample-buffer view without copying the selected frames.
Constructor Details
#initialize(samples, format, storage: :array) ⇒ SampleBuffer
Returns a new instance of SampleBuffer.
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
#duration ⇒ Duration (readonly)
Returns the value of attribute duration.
240 241 242 |
# File 'lib/wavify/core/sample_buffer.rb', line 240 def duration @duration end |
#format ⇒ Format (readonly)
Returns the value of attribute format.
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.
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.
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.
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: +
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.
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.}" end |
#each ⇒ SampleBuffer #each ⇒ Enumerator[Numeric, SampleBuffer]
Enumerates sample values in interleaved order.
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_sample ⇒ SampleBuffer #each_frame_sample ⇒ Enumerator[Numeric, SampleBuffer]
Iterates interleaved values without allocating a frame Array.
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_view ⇒ FrameView
Returns a lazy sample-frame view without copying the full buffer.
352 353 354 |
# File 'lib/wavify/core/sample_buffer.rb', line 352 def frame_view FrameView.new(samples, @format.channels) end |
#hash ⇒ Integer
320 321 322 |
# File 'lib/wavify/core/sample_buffer.rb', line 320 def hash @value_hash end |
#length ⇒ Integer Also known as: size
Returns number of interleaved samples.
338 339 340 |
# File 'lib/wavify/core/sample_buffer.rb', line 338 def length @sample_count end |
#packed? ⇒ Boolean
294 295 296 |
# File 'lib/wavify/core/sample_buffer.rb', line 294 def packed? @storage_state.type == :packed end |
#packed_bytesize ⇒ Integer
298 299 300 |
# File 'lib/wavify/core/sample_buffer.rb', line 298 def packed_bytesize @storage_state.packed_samples&.bytesize || 0 end |
#reverse ⇒ SampleBuffer
Reverses sample frame order while preserving channel ordering per frame.
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_count ⇒ Integer
Returns 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 |
#samples ⇒ Array[Numeric]
Materializes packed storage on first random-access use.
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.
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 |
#storage ⇒ 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.
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 |