Class: Wavify::Codecs::Flac

Inherits:
Base
  • Object
show all
Defined in:
lib/wavify/codecs/flac.rb,
sig/codecs.rbs

Overview

Pure Ruby FLAC codec (metadata, decode, encode, and streaming support).

Defined Under Namespace

Classes: BitReader, BitWriter

Constant Summary collapse

EXTENSIONS =

Recognized filename extensions.

%w[.flac].freeze
STREAMINFO_BLOCK_TYPE =

:nodoc:

0
SEEKTABLE_BLOCK_TYPE =

:nodoc:

3
VORBIS_COMMENT_BLOCK_TYPE =

:nodoc:

4
STREAMINFO_LENGTH =

:nodoc:

34
FLAC_SYNC_CODE =

:nodoc:

0x3FFE
DEFAULT_ENCODE_BLOCK_SIZE =

Default block size used by the FLAC stream encoder.

4096
COMPRESSION_BLOCK_SIZES =

:nodoc:

[1024, 2048, 4096, 4096, 4096, 8192, 8192, 16_384, 16_384].freeze
BLOCK_SIZE_CODES =

:nodoc:

{ # :nodoc:
  1 => 192,
  2 => 576,
  3 => 1152,
  4 => 2304,
  5 => 4608,
  8 => 256,
  9 => 512,
  10 => 1024,
  11 => 2048,
  12 => 4096,
  13 => 8192,
  14 => 16_384,
  15 => 32_768
}.freeze
SAMPLE_RATE_CODES =

:nodoc:

{ # :nodoc:
  1 => 88_200,
  2 => 176_400,
  3 => 192_000,
  4 => 8_000,
  5 => 16_000,
  6 => 22_050,
  7 => 24_000,
  8 => 32_000,
  9 => 44_100,
  10 => 48_000,
  11 => 96_000
}.freeze
SAMPLE_SIZE_CODES =

:nodoc:

{ # :nodoc:
  1 => 8,
  2 => 12,
  4 => 16,
  5 => 20,
  6 => 24
}.freeze
MAX_UNARY_ZERO_RUN =

Bounds malicious unary/Rice runs before they can consume unbounded CPU.

1_048_576

Class Method Summary collapse

Methods inherited from Base

available?

Class Method Details

.can_read?(io_or_path) ⇒ Boolean

Parameters:

  • io_or_path (String, IO)

Returns:

  • (Boolean)


208
209
210
211
212
213
214
215
216
# File 'lib/wavify/codecs/flac.rb', line 208

def can_read?(io_or_path)
  return true if io_or_path.is_a?(String) && EXTENSIONS.include?(File.extname(io_or_path).downcase)
  return false unless io_or_path.respond_to?(:read)

  magic = probe_bytes(io_or_path, 4)
  magic == "fLaC"
rescue StreamError
  false
end

.metadata(io_or_path) ⇒ Hash

Reads FLAC metadata (including STREAMINFO-derived format/duration).

Parameters:

  • io_or_path (String, IO)

Returns:

  • (Hash)


429
430
431
432
433
434
435
# File 'lib/wavify/codecs/flac.rb', line 429

def (io_or_path)
  io, close_io = open_input(io_or_path)

  (io)
ensure
  io.close if close_io && io
end

.read(io_or_path, format: nil) ⇒ Wavify::Core::SampleBuffer

Reads a FLAC stream and returns decoded samples.

Parameters:

Returns:



223
224
225
226
227
228
229
230
231
232
233
# File 'lib/wavify/codecs/flac.rb', line 223

def read(io_or_path, format: nil)
  io, close_io = open_input(io_or_path)

   = (io)
  source_format = .fetch(:format)
  samples = decode_frames(io, )
  buffer = Core::SampleBuffer.new(samples, source_format)
  format ? buffer.convert(format) : buffer
ensure
  io.close if close_io && io
end

.stream_read(io_or_path, chunk_size: 4096) {|arg0| ... } ⇒ Enumerator

Streams FLAC decoding as chunked sample buffers.

Parameters:

  • io_or_path (String, IO)
  • chunk_size (Integer) (defaults to: 4096)
  • chunk_size: (Integer) (defaults to: 4096)

Yields:

Yield Parameters:

Yield Returns:

  • (void)

Returns:

  • (Enumerator)


269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/wavify/codecs/flac.rb', line 269

def stream_read(io_or_path, chunk_size: 4096)
  return enum_for(__method__, io_or_path, chunk_size: chunk_size) unless block_given?
  raise InvalidParameterError, "chunk_size must be a positive Integer" unless chunk_size.is_a?(Integer) && chunk_size.positive?

  io, close_io = open_input(io_or_path)

   = (io)
  format = .fetch(:format)
  chunk_sample_count = chunk_size * format.channels
  pending_samples = []
  pending_offset = 0

  each_decoded_frame_samples(io, ) do |frame_samples|
    pending_samples.concat(frame_samples)

    while (pending_samples.length - pending_offset) >= chunk_sample_count
      yield Core::SampleBuffer.new(pending_samples.slice(pending_offset, chunk_sample_count), format)
      pending_offset += chunk_sample_count
    end

    if pending_offset >= chunk_sample_count * 8
      pending_samples = pending_samples.slice(pending_offset, pending_samples.length - pending_offset) || []
      pending_offset = 0
    end
  end

  remaining = pending_samples.length - pending_offset
  yield Core::SampleBuffer.new(pending_samples.slice(pending_offset, remaining), format) if remaining.positive?
ensure
  io.close if close_io && io
end

.stream_write(io_or_path, format:, block_size: DEFAULT_ENCODE_BLOCK_SIZE, block_size_strategy: :per_chunk, compression_level: nil, comments: nil, stereo_coding: :auto, predictor: :auto, **codec_options) {|arg0| ... } ⇒ Enumerator, ...

Streams FLAC encoding and finalizes STREAMINFO on completion.

Parameters:

  • io_or_path (String, IO)
  • format (Wavify::Core::Format)
  • block_size (Integer) (defaults to: DEFAULT_ENCODE_BLOCK_SIZE)
  • block_size_strategy (Symbol) (defaults to: :per_chunk)

    :per_chunk, :fixed, or :source_chunk

  • format: (Core::Format)
  • options (Object)

Yields:

Yield Parameters:

  • arg0 (Object)

Yield Returns:

  • (void)

Returns:

  • (Enumerator, String, IO)


308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/wavify/codecs/flac.rb', line 308

def stream_write(io_or_path, format:, block_size: DEFAULT_ENCODE_BLOCK_SIZE, block_size_strategy: :per_chunk,
                 compression_level: nil, comments: nil, stereo_coding: :auto, predictor: :auto, **codec_options)
  validate_no_codec_options!(codec_options, operation: "FLAC stream_write")
  unless block_given?
    return enum_for(
      __method__,
      io_or_path,
      format: format,
      block_size: block_size,
      block_size_strategy: block_size_strategy,
      compression_level: compression_level,
      comments: comments,
      stereo_coding: stereo_coding,
      predictor: predictor,
      **codec_options
    )
  end

  target_format = validate_encode_format!(format)
  stream_write_options = normalize_stream_write_options(block_size, block_size_strategy, compression_level)
  vorbis_comments = normalize_vorbis_comments(comments)
  normalized_stereo_coding = normalize_stereo_coding!(stereo_coding)
  normalized_predictor = normalize_predictor!(predictor)
  io, close_io = open_output(io_or_path)
  ensure_seekable!(io)
  prepare_output!(io, owned: close_io)

  header = write_stream_header(io, comments: vorbis_comments)
  total_sample_frames = 0
  next_frame_number = 0
  encode_stats = empty_encode_stats
  header[:md5] = Digest::MD5.new
  pending_samples = []
  pending_offset = 0

  writer = lambda do |chunk|
    raise InvalidParameterError, "stream chunk must be Core::SampleBuffer" unless chunk.is_a?(Core::SampleBuffer)

    buffer = chunk.format == target_format ? chunk : chunk.convert(target_format)
    encoded_samples = unalign_encoded_samples(buffer.samples, target_format)
    header.fetch(:md5).update(flac_md5_bytes(encoded_samples, target_format.valid_bits))
    total_sample_frames += buffer.sample_frame_count

    if stream_write_options[:strategy] == :fixed
      pending_samples.concat(encoded_samples)
      fixed_chunk_sample_count = stream_write_options.fetch(:block_size) * target_format.channels

      while (pending_samples.length - pending_offset) >= fixed_chunk_sample_count
        encoded = encode_verbatim_frames(
          pending_samples.slice(pending_offset, fixed_chunk_sample_count),
          target_format,
          start_frame_number: next_frame_number,
          block_size: stream_write_options.fetch(:block_size),
          stereo_coding: normalized_stereo_coding,
          predictor: normalized_predictor
        )
        write_all(io, encoded.fetch(:bytes))
        next_frame_number = encoded.fetch(:next_frame_number)
        merge_encode_stats!(encode_stats, encoded)
        pending_offset += fixed_chunk_sample_count
      end

      if pending_offset >= fixed_chunk_sample_count * 8
        pending_samples = pending_samples.slice(pending_offset, pending_samples.length - pending_offset) || []
        pending_offset = 0
      end
    elsif stream_write_options[:strategy] == :source_chunk
      encoded = encode_verbatim_frames(
        encoded_samples,
        target_format,
        start_frame_number: next_frame_number,
        block_size: buffer.sample_frame_count,
        stereo_coding: normalized_stereo_coding,
        predictor: normalized_predictor
      )
      write_all(io, encoded.fetch(:bytes))
      next_frame_number = encoded.fetch(:next_frame_number)
      merge_encode_stats!(encode_stats, encoded)
    else
      encoded = encode_verbatim_frames(
        encoded_samples,
        target_format,
        start_frame_number: next_frame_number,
        block_size: stream_write_options.fetch(:block_size),
        stereo_coding: normalized_stereo_coding,
        predictor: normalized_predictor
      )
      write_all(io, encoded.fetch(:bytes))
      next_frame_number = encoded.fetch(:next_frame_number)
      merge_encode_stats!(encode_stats, encoded)
    end
  end

  yield writer

  remaining = pending_samples.length - pending_offset
  if remaining.positive?
    encoded = encode_verbatim_frames(
      pending_samples.slice(pending_offset, remaining),
      target_format,
      start_frame_number: next_frame_number,
      block_size: stream_write_options.fetch(:block_size),
      stereo_coding: normalized_stereo_coding,
      predictor: normalized_predictor
    )
    write_all(io, encoded.fetch(:bytes))
    next_frame_number = encoded.fetch(:next_frame_number)
    merge_encode_stats!(encode_stats, encoded)
  end

  finalize_stream_header(io, header, target_format, total_sample_frames, encode_stats)
  finalize_output!(io, owned: close_io)
  io_or_path
ensure
  io.close if close_io && io
end

.write(io_or_path, sample_buffer, format:, block_size: DEFAULT_ENCODE_BLOCK_SIZE, compression_level: nil, comments: nil, stereo_coding: :auto, predictor: :auto, **codec_options) ⇒ String, IO

Writes a sample buffer as FLAC.

Parameters:

Returns:

  • (String, IO)

Raises:



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

def write(io_or_path, sample_buffer, format:, block_size: DEFAULT_ENCODE_BLOCK_SIZE, compression_level: nil,
          comments: nil, stereo_coding: :auto, predictor: :auto, **codec_options)
  validate_no_codec_options!(codec_options, operation: "FLAC write")
  raise InvalidParameterError, "sample_buffer must be Core::SampleBuffer" unless sample_buffer.is_a?(Core::SampleBuffer)

  target_format = validate_encode_format!(format)
  buffer = sample_buffer.format == target_format ? sample_buffer : sample_buffer.convert(target_format)
  target_block_size = normalize_write_block_size(block_size, compression_level)
  vorbis_comments = normalize_vorbis_comments(comments)
  normalized_stereo_coding = normalize_stereo_coding!(stereo_coding)
  normalized_predictor = normalize_predictor!(predictor)
  stream_write(
    io_or_path,
    format: target_format,
    block_size: target_block_size,
    block_size_strategy: :fixed,
    comments: vorbis_comments,
    stereo_coding: normalized_stereo_coding,
    predictor: normalized_predictor
  ) { |writer| writer.call(buffer) }
end