wavify 0.1.0 → 0.2.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/README.md +204 -22
  4. data/exe/wavify +6 -0
  5. data/lib/wavify/adapters.rb +81 -0
  6. data/lib/wavify/audio.rb +1066 -120
  7. data/lib/wavify/cli.rb +237 -0
  8. data/lib/wavify/codecs/aiff.rb +388 -87
  9. data/lib/wavify/codecs/base.rb +98 -5
  10. data/lib/wavify/codecs/flac.rb +660 -170
  11. data/lib/wavify/codecs/ogg_vorbis.rb +515 -155
  12. data/lib/wavify/codecs/raw.rb +235 -45
  13. data/lib/wavify/codecs/registry.rb +272 -10
  14. data/lib/wavify/codecs/wav.rb +453 -74
  15. data/lib/wavify/core/duration.rb +54 -2
  16. data/lib/wavify/core/format.rb +96 -11
  17. data/lib/wavify/core/sample_buffer.rb +648 -60
  18. data/lib/wavify/core/stream.rb +590 -34
  19. data/lib/wavify/dsl.rb +712 -100
  20. data/lib/wavify/dsp/automation.rb +109 -0
  21. data/lib/wavify/dsp/effects/auto_pan.rb +71 -0
  22. data/lib/wavify/dsp/effects/bitcrusher.rb +74 -0
  23. data/lib/wavify/dsp/effects/chorus.rb +15 -4
  24. data/lib/wavify/dsp/effects/compressor.rb +133 -23
  25. data/lib/wavify/dsp/effects/delay.rb +46 -1
  26. data/lib/wavify/dsp/effects/distortion.rb +3 -2
  27. data/lib/wavify/dsp/effects/effect_base.rb +69 -2
  28. data/lib/wavify/dsp/effects/effect_chain.rb +94 -0
  29. data/lib/wavify/dsp/effects/envelope_controlled_effect.rb +93 -0
  30. data/lib/wavify/dsp/effects/eq.rb +102 -0
  31. data/lib/wavify/dsp/effects/expander.rb +90 -0
  32. data/lib/wavify/dsp/effects/flanger.rb +112 -0
  33. data/lib/wavify/dsp/effects/limiter.rb +259 -0
  34. data/lib/wavify/dsp/effects/mastering_chain.rb +31 -0
  35. data/lib/wavify/dsp/effects/noise_gate.rb +72 -0
  36. data/lib/wavify/dsp/effects/phaser.rb +112 -0
  37. data/lib/wavify/dsp/effects/podcast_chain.rb +32 -0
  38. data/lib/wavify/dsp/effects/reverb.rb +100 -14
  39. data/lib/wavify/dsp/effects/soft_limiter.rb +55 -0
  40. data/lib/wavify/dsp/effects/stereo_widener.rb +73 -0
  41. data/lib/wavify/dsp/effects/tremolo.rb +66 -0
  42. data/lib/wavify/dsp/effects/vibrato.rb +102 -0
  43. data/lib/wavify/dsp/effects.rb +106 -0
  44. data/lib/wavify/dsp/envelope.rb +90 -19
  45. data/lib/wavify/dsp/filter.rb +149 -8
  46. data/lib/wavify/dsp/headroom.rb +57 -0
  47. data/lib/wavify/dsp/lfo.rb +65 -0
  48. data/lib/wavify/dsp/loudness_meter.rb +281 -0
  49. data/lib/wavify/dsp/oscillator.rb +261 -22
  50. data/lib/wavify/dsp/processor.rb +90 -0
  51. data/lib/wavify/errors.rb +2 -2
  52. data/lib/wavify/sequencer/engine.rb +405 -89
  53. data/lib/wavify/sequencer/note_sequence.rb +45 -7
  54. data/lib/wavify/sequencer/pattern.rb +121 -12
  55. data/lib/wavify/sequencer/track.rb +149 -10
  56. data/lib/wavify/version.rb +1 -1
  57. data/lib/wavify.rb +18 -0
  58. data/sig/audio.rbs +86 -0
  59. data/sig/codecs.rbs +77 -0
  60. data/sig/core.rbs +108 -0
  61. data/sig/dsl.rbs +32 -0
  62. data/sig/dsp.rbs +87 -0
  63. data/sig/effects.rbs +129 -0
  64. data/sig/public_api.yml +111 -0
  65. data/sig/sequencer.rbs +126 -0
  66. data/sig/stream.rbs +30 -0
  67. data/sig/wavify.rbs +24 -0
  68. metadata +44 -58
  69. data/.serena/.gitignore +0 -1
  70. data/.serena/memories/project_overview.md +0 -5
  71. data/.serena/memories/style_and_completion.md +0 -5
  72. data/.serena/memories/suggested_commands.md +0 -11
  73. data/.serena/project.yml +0 -126
  74. data/.simplecov +0 -18
  75. data/.yardopts +0 -4
  76. data/Rakefile +0 -190
  77. data/benchmarks/README.md +0 -46
  78. data/benchmarks/benchmark_helper.rb +0 -112
  79. data/benchmarks/dsp_effects_benchmark.rb +0 -46
  80. data/benchmarks/flac_benchmark.rb +0 -74
  81. data/benchmarks/streaming_memory_benchmark.rb +0 -94
  82. data/benchmarks/wav_io_benchmark.rb +0 -110
  83. data/examples/audio_processing.rb +0 -73
  84. data/examples/cinematic_transition.rb +0 -118
  85. data/examples/drum_machine.rb +0 -74
  86. data/examples/format_convert.rb +0 -81
  87. data/examples/hybrid_arrangement.rb +0 -165
  88. data/examples/streaming_master_chain.rb +0 -129
  89. data/examples/synth_pad.rb +0 -42
  90. data/tools/fixture_writer.rb +0 -85
@@ -9,10 +9,13 @@ module Wavify
9
9
  # Recognized filename extensions.
10
10
  EXTENSIONS = %w[.flac].freeze
11
11
  STREAMINFO_BLOCK_TYPE = 0 # :nodoc:
12
+ SEEKTABLE_BLOCK_TYPE = 3 # :nodoc:
13
+ VORBIS_COMMENT_BLOCK_TYPE = 4 # :nodoc:
12
14
  STREAMINFO_LENGTH = 34 # :nodoc:
13
15
  FLAC_SYNC_CODE = 0x3FFE # :nodoc:
14
16
  # Default block size used by the FLAC stream encoder.
15
17
  DEFAULT_ENCODE_BLOCK_SIZE = 4096
18
+ COMPRESSION_BLOCK_SIZES = [1024, 2048, 4096, 4096, 4096, 8192, 8192, 16_384, 16_384].freeze # :nodoc:
16
19
 
17
20
  BLOCK_SIZE_CODES = { # :nodoc:
18
21
  1 => 192,
@@ -52,12 +55,18 @@ module Wavify
52
55
  6 => 24
53
56
  }.freeze
54
57
 
58
+ # Bounds malicious unary/Rice runs before they can consume unbounded CPU.
59
+ MAX_UNARY_ZERO_RUN = 1_048_576
60
+
55
61
  # Internal bit reader used by the FLAC decoder.
56
62
  class BitReader # :nodoc:
63
+ attr_reader :captured_bytes
64
+
57
65
  def initialize(io)
58
66
  @io = io
59
67
  @buffer = 0
60
68
  @bits_available = 0
69
+ @captured_bytes = +"".b
61
70
  end
62
71
 
63
72
  def read_bits(count)
@@ -100,6 +109,7 @@ module Wavify
100
109
  byte = @io.read(1)
101
110
  raise InvalidFormatError, "truncated FLAC frame" if byte.nil?
102
111
 
112
+ @captured_bytes << byte
103
113
  @buffer = byte.getbyte(0)
104
114
  @bits_available = 8
105
115
  end
@@ -108,7 +118,7 @@ module Wavify
108
118
  # Internal bit writer used by the FLAC encoder.
109
119
  class BitWriter # :nodoc:
110
120
  def initialize
111
- @bytes = []
121
+ @bytes = +"".b
112
122
  @buffer = 0
113
123
  @bits_used = 0
114
124
  end
@@ -117,11 +127,13 @@ module Wavify
117
127
  raise InvalidParameterError, "bit count must be a non-negative Integer" unless count.is_a?(Integer) && count >= 0
118
128
  return if count.zero?
119
129
 
120
- count.times do |shift_index|
121
- shift = (count - 1) - shift_index
122
- bit = (value >> shift) & 0x1
123
- @buffer = (@buffer << 1) | bit
124
- @bits_used += 1
130
+ remaining = count
131
+ while remaining.positive?
132
+ take = [8 - @bits_used, remaining].min
133
+ shift = remaining - take
134
+ @buffer = (@buffer << take) | ((value >> shift) & ((1 << take) - 1))
135
+ @bits_used += take
136
+ remaining -= take
125
137
  flush_byte_if_needed
126
138
  end
127
139
  end
@@ -132,7 +144,27 @@ module Wavify
132
144
  end
133
145
 
134
146
  def write_unary_zeros_then_one(zero_count) # :nodoc:
135
- zero_count.times { write_bits(0, 1) }
147
+ unless zero_count.is_a?(Integer) && zero_count.between?(0, MAX_UNARY_ZERO_RUN)
148
+ raise InvalidParameterError, "FLAC unary zero run exceeds resource limit"
149
+ end
150
+
151
+ if @bits_used.positive?
152
+ fill = [8 - @bits_used, zero_count].min
153
+ @buffer <<= fill
154
+ @bits_used += fill
155
+ zero_count -= fill
156
+ flush_byte_if_needed
157
+ end
158
+
159
+ if @bits_used.zero? && zero_count >= 8
160
+ whole_bytes, zero_count = zero_count.divmod(8)
161
+ @bytes << ("\x00".b * whole_bytes)
162
+ end
163
+
164
+ if zero_count.positive?
165
+ @buffer <<= zero_count
166
+ @bits_used += zero_count
167
+ end
136
168
  write_bits(1, 1)
137
169
  end
138
170
 
@@ -156,7 +188,7 @@ module Wavify
156
188
 
157
189
  def to_s # :nodoc:
158
190
  align_to_byte
159
- @bytes.pack("C*")
191
+ @bytes.dup
160
192
  end
161
193
 
162
194
  private
@@ -177,9 +209,10 @@ module Wavify
177
209
  return true if io_or_path.is_a?(String) && EXTENSIONS.include?(File.extname(io_or_path).downcase)
178
210
  return false unless io_or_path.respond_to?(:read)
179
211
 
180
- magic = io_or_path.read(4)
181
- io_or_path.rewind if io_or_path.respond_to?(:rewind)
212
+ magic = probe_bytes(io_or_path, 4)
182
213
  magic == "fLaC"
214
+ rescue StreamError
215
+ false
183
216
  end
184
217
 
185
218
  # Reads a FLAC stream and returns decoded samples.
@@ -189,7 +222,6 @@ module Wavify
189
222
  # @return [Wavify::Core::SampleBuffer]
190
223
  def read(io_or_path, format: nil)
191
224
  io, close_io = open_input(io_or_path)
192
- ensure_seekable!(io)
193
225
 
194
226
  metadata = parse_metadata(io)
195
227
  source_format = metadata.fetch(:format)
@@ -205,22 +237,28 @@ module Wavify
205
237
  # @param io_or_path [String, IO]
206
238
  # @param sample_buffer [Wavify::Core::SampleBuffer]
207
239
  # @param format [Wavify::Core::Format]
240
+ # @param block_size [Integer]
208
241
  # @return [String, IO]
209
- def write(io_or_path, sample_buffer, format:)
242
+ def write(io_or_path, sample_buffer, format:, block_size: DEFAULT_ENCODE_BLOCK_SIZE, compression_level: nil,
243
+ comments: nil, stereo_coding: :auto, predictor: :auto, **codec_options)
244
+ validate_no_codec_options!(codec_options, operation: "FLAC write")
210
245
  raise InvalidParameterError, "sample_buffer must be Core::SampleBuffer" unless sample_buffer.is_a?(Core::SampleBuffer)
211
246
 
212
247
  target_format = validate_encode_format!(format)
213
248
  buffer = sample_buffer.format == target_format ? sample_buffer : sample_buffer.convert(target_format)
214
-
215
- io, close_io = open_output(io_or_path)
216
- io.rewind if io.respond_to?(:rewind)
217
- io.truncate(0) if io.respond_to?(:truncate)
218
- io.write(encode_verbatim_stream(buffer, target_format))
219
- io.flush if io.respond_to?(:flush)
220
- io.rewind if io.respond_to?(:rewind)
221
- io_or_path
222
- ensure
223
- io.close if close_io && io
249
+ target_block_size = normalize_write_block_size(block_size, compression_level)
250
+ vorbis_comments = normalize_vorbis_comments(comments)
251
+ normalized_stereo_coding = normalize_stereo_coding!(stereo_coding)
252
+ normalized_predictor = normalize_predictor!(predictor)
253
+ stream_write(
254
+ io_or_path,
255
+ format: target_format,
256
+ block_size: target_block_size,
257
+ block_size_strategy: :fixed,
258
+ comments: vorbis_comments,
259
+ stereo_coding: normalized_stereo_coding,
260
+ predictor: normalized_predictor
261
+ ) { |writer| writer.call(buffer) }
224
262
  end
225
263
 
226
264
  # Streams FLAC decoding as chunked sample buffers.
@@ -233,22 +271,29 @@ module Wavify
233
271
  raise InvalidParameterError, "chunk_size must be a positive Integer" unless chunk_size.is_a?(Integer) && chunk_size.positive?
234
272
 
235
273
  io, close_io = open_input(io_or_path)
236
- ensure_seekable!(io)
237
274
 
238
275
  metadata = parse_metadata(io)
239
276
  format = metadata.fetch(:format)
240
277
  chunk_sample_count = chunk_size * format.channels
241
278
  pending_samples = []
279
+ pending_offset = 0
242
280
 
243
281
  each_decoded_frame_samples(io, metadata) do |frame_samples|
244
282
  pending_samples.concat(frame_samples)
245
283
 
246
- while pending_samples.length >= chunk_sample_count
247
- yield Core::SampleBuffer.new(pending_samples.shift(chunk_sample_count), format)
284
+ while (pending_samples.length - pending_offset) >= chunk_sample_count
285
+ yield Core::SampleBuffer.new(pending_samples.slice(pending_offset, chunk_sample_count), format)
286
+ pending_offset += chunk_sample_count
287
+ end
288
+
289
+ if pending_offset >= chunk_sample_count * 8
290
+ pending_samples = pending_samples.slice(pending_offset, pending_samples.length - pending_offset) || []
291
+ pending_offset = 0
248
292
  end
249
293
  end
250
294
 
251
- yield Core::SampleBuffer.new(pending_samples, format) unless pending_samples.empty?
295
+ remaining = pending_samples.length - pending_offset
296
+ yield Core::SampleBuffer.new(pending_samples.slice(pending_offset, remaining), format) if remaining.positive?
252
297
  ensure
253
298
  io.close if close_io && io
254
299
  end
@@ -260,71 +305,94 @@ module Wavify
260
305
  # @param block_size [Integer]
261
306
  # @param block_size_strategy [Symbol] `:per_chunk`, `:fixed`, or `:source_chunk`
262
307
  # @return [Enumerator, String, IO]
263
- def stream_write(io_or_path, format:, block_size: DEFAULT_ENCODE_BLOCK_SIZE, block_size_strategy: :per_chunk)
308
+ def stream_write(io_or_path, format:, block_size: DEFAULT_ENCODE_BLOCK_SIZE, block_size_strategy: :per_chunk,
309
+ compression_level: nil, comments: nil, stereo_coding: :auto, predictor: :auto, **codec_options)
310
+ validate_no_codec_options!(codec_options, operation: "FLAC stream_write")
264
311
  unless block_given?
265
312
  return enum_for(
266
313
  __method__,
267
314
  io_or_path,
268
315
  format: format,
269
316
  block_size: block_size,
270
- block_size_strategy: block_size_strategy
317
+ block_size_strategy: block_size_strategy,
318
+ compression_level: compression_level,
319
+ comments: comments,
320
+ stereo_coding: stereo_coding,
321
+ predictor: predictor,
322
+ **codec_options
271
323
  )
272
324
  end
273
325
 
274
326
  target_format = validate_encode_format!(format)
275
- stream_write_options = normalize_stream_write_options(block_size, block_size_strategy)
327
+ stream_write_options = normalize_stream_write_options(block_size, block_size_strategy, compression_level)
328
+ vorbis_comments = normalize_vorbis_comments(comments)
329
+ normalized_stereo_coding = normalize_stereo_coding!(stereo_coding)
330
+ normalized_predictor = normalize_predictor!(predictor)
276
331
  io, close_io = open_output(io_or_path)
277
332
  ensure_seekable!(io)
278
- io.rewind if io.respond_to?(:rewind)
279
- io.truncate(0) if io.respond_to?(:truncate)
333
+ prepare_output!(io, owned: close_io)
280
334
 
281
- header = write_stream_header(io)
335
+ header = write_stream_header(io, comments: vorbis_comments)
282
336
  total_sample_frames = 0
283
337
  next_frame_number = 0
284
338
  encode_stats = empty_encode_stats
285
339
  header[:md5] = Digest::MD5.new
286
340
  pending_samples = []
341
+ pending_offset = 0
287
342
 
288
343
  writer = lambda do |chunk|
289
344
  raise InvalidParameterError, "stream chunk must be Core::SampleBuffer" unless chunk.is_a?(Core::SampleBuffer)
290
345
 
291
346
  buffer = chunk.format == target_format ? chunk : chunk.convert(target_format)
292
- header.fetch(:md5).update(pcm_bytes_for_md5(buffer.samples, target_format))
347
+ encoded_samples = unalign_encoded_samples(buffer.samples, target_format)
348
+ header.fetch(:md5).update(flac_md5_bytes(encoded_samples, target_format.valid_bits))
293
349
  total_sample_frames += buffer.sample_frame_count
294
350
 
295
351
  if stream_write_options[:strategy] == :fixed
296
- pending_samples.concat(buffer.samples)
352
+ pending_samples.concat(encoded_samples)
297
353
  fixed_chunk_sample_count = stream_write_options.fetch(:block_size) * target_format.channels
298
354
 
299
- while pending_samples.length >= fixed_chunk_sample_count
355
+ while (pending_samples.length - pending_offset) >= fixed_chunk_sample_count
300
356
  encoded = encode_verbatim_frames(
301
- pending_samples.shift(fixed_chunk_sample_count),
357
+ pending_samples.slice(pending_offset, fixed_chunk_sample_count),
302
358
  target_format,
303
359
  start_frame_number: next_frame_number,
304
- block_size: stream_write_options.fetch(:block_size)
360
+ block_size: stream_write_options.fetch(:block_size),
361
+ stereo_coding: normalized_stereo_coding,
362
+ predictor: normalized_predictor
305
363
  )
306
- io.write(encoded.fetch(:bytes))
364
+ write_all(io, encoded.fetch(:bytes))
307
365
  next_frame_number = encoded.fetch(:next_frame_number)
308
366
  merge_encode_stats!(encode_stats, encoded)
367
+ pending_offset += fixed_chunk_sample_count
368
+ end
369
+
370
+ if pending_offset >= fixed_chunk_sample_count * 8
371
+ pending_samples = pending_samples.slice(pending_offset, pending_samples.length - pending_offset) || []
372
+ pending_offset = 0
309
373
  end
310
374
  elsif stream_write_options[:strategy] == :source_chunk
311
375
  encoded = encode_verbatim_frames(
312
- buffer.samples,
376
+ encoded_samples,
313
377
  target_format,
314
378
  start_frame_number: next_frame_number,
315
- block_size: buffer.sample_frame_count
379
+ block_size: buffer.sample_frame_count,
380
+ stereo_coding: normalized_stereo_coding,
381
+ predictor: normalized_predictor
316
382
  )
317
- io.write(encoded.fetch(:bytes))
383
+ write_all(io, encoded.fetch(:bytes))
318
384
  next_frame_number = encoded.fetch(:next_frame_number)
319
385
  merge_encode_stats!(encode_stats, encoded)
320
386
  else
321
387
  encoded = encode_verbatim_frames(
322
- buffer.samples,
388
+ encoded_samples,
323
389
  target_format,
324
390
  start_frame_number: next_frame_number,
325
- block_size: stream_write_options.fetch(:block_size)
391
+ block_size: stream_write_options.fetch(:block_size),
392
+ stereo_coding: normalized_stereo_coding,
393
+ predictor: normalized_predictor
326
394
  )
327
- io.write(encoded.fetch(:bytes))
395
+ write_all(io, encoded.fetch(:bytes))
328
396
  next_frame_number = encoded.fetch(:next_frame_number)
329
397
  merge_encode_stats!(encode_stats, encoded)
330
398
  end
@@ -332,21 +400,23 @@ module Wavify
332
400
 
333
401
  yield writer
334
402
 
335
- unless pending_samples.empty?
403
+ remaining = pending_samples.length - pending_offset
404
+ if remaining.positive?
336
405
  encoded = encode_verbatim_frames(
337
- pending_samples,
406
+ pending_samples.slice(pending_offset, remaining),
338
407
  target_format,
339
408
  start_frame_number: next_frame_number,
340
- block_size: stream_write_options.fetch(:block_size)
409
+ block_size: stream_write_options.fetch(:block_size),
410
+ stereo_coding: normalized_stereo_coding,
411
+ predictor: normalized_predictor
341
412
  )
342
- io.write(encoded.fetch(:bytes))
413
+ write_all(io, encoded.fetch(:bytes))
343
414
  next_frame_number = encoded.fetch(:next_frame_number)
344
415
  merge_encode_stats!(encode_stats, encoded)
345
416
  end
346
417
 
347
418
  finalize_stream_header(io, header, target_format, total_sample_frames, encode_stats)
348
- io.flush if io.respond_to?(:flush)
349
- io.rewind if io.respond_to?(:rewind)
419
+ finalize_output!(io, owned: close_io)
350
420
  io_or_path
351
421
  ensure
352
422
  io.close if close_io && io
@@ -358,7 +428,6 @@ module Wavify
358
428
  # @return [Hash]
359
429
  def metadata(io_or_path)
360
430
  io, close_io = open_input(io_or_path)
361
- ensure_seekable!(io)
362
431
 
363
432
  parse_metadata(io)
364
433
  ensure
@@ -368,11 +437,12 @@ module Wavify
368
437
  private
369
438
 
370
439
  def parse_metadata(io)
371
- io.rewind
372
440
  marker = read_exact(io, 4, "missing FLAC stream marker")
373
441
  raise InvalidFormatError, "invalid FLAC stream marker" unless marker == "fLaC"
374
442
 
375
443
  streaminfo = nil
444
+ seektable = nil
445
+ vorbis_comment = nil
376
446
  loop do
377
447
  header = read_exact(io, 4, "truncated FLAC metadata block header")
378
448
  byte0 = header.getbyte(0)
@@ -381,13 +451,29 @@ module Wavify
381
451
  length = ((header.getbyte(1) << 16) | (header.getbyte(2) << 8) | header.getbyte(3))
382
452
  data = read_exact(io, length, "truncated FLAC metadata block")
383
453
 
384
- streaminfo = parse_streaminfo(data) if block_type == STREAMINFO_BLOCK_TYPE
454
+ case block_type
455
+ when STREAMINFO_BLOCK_TYPE
456
+ streaminfo = parse_streaminfo(data)
457
+ when SEEKTABLE_BLOCK_TYPE
458
+ seektable = parse_seektable(data)
459
+ when VORBIS_COMMENT_BLOCK_TYPE
460
+ vorbis_comment = parse_vorbis_comment(data)
461
+ end
385
462
  break if last_block
386
463
  end
387
464
 
388
465
  raise InvalidFormatError, "STREAMINFO metadata block missing" unless streaminfo
389
466
 
390
- streaminfo
467
+ streaminfo.merge(
468
+ seektable: seektable,
469
+ seekpoints: seektable&.fetch(:points) || [],
470
+ vorbis_comment: vorbis_comment,
471
+ vendor: vorbis_comment&.fetch(:vendor),
472
+ comments: vorbis_comment&.fetch(:comments) || {},
473
+ comment_values: vorbis_comment&.fetch(:comment_values) || {},
474
+ raw_comments: vorbis_comment&.fetch(:raw) || [],
475
+ raw_comment_bytes: vorbis_comment&.fetch(:raw_bytes) || []
476
+ )
391
477
  end
392
478
 
393
479
  def parse_streaminfo(data)
@@ -400,19 +486,26 @@ module Wavify
400
486
  packed = data[10, 8].unpack1("Q>")
401
487
  sample_rate = (packed >> 44) & 0xFFFFF
402
488
  channels = ((packed >> 41) & 0x7) + 1
403
- bit_depth = ((packed >> 36) & 0x1F) + 1
489
+ encoded_bit_depth = ((packed >> 36) & 0x1F) + 1
404
490
  total_samples = packed & 0xFFFFFFFFF
405
491
  md5 = data[18, 16].unpack1("H*")
492
+ container_bit_depth = [8, 16, 24, 32].find { |depth| depth >= encoded_bit_depth }
493
+ unless container_bit_depth
494
+ raise UnsupportedFormatError, "unsupported FLAC sample size: #{encoded_bit_depth} bits"
495
+ end
406
496
 
407
497
  format = Core::Format.new(
408
498
  channels: channels,
409
499
  sample_rate: sample_rate,
410
- bit_depth: bit_depth,
411
- sample_format: :pcm
500
+ bit_depth: container_bit_depth,
501
+ sample_format: :pcm,
502
+ valid_bits: encoded_bit_depth
412
503
  )
413
504
 
414
505
  {
415
506
  format: format,
507
+ encoded_bit_depth: encoded_bit_depth,
508
+ container_bit_depth: container_bit_depth,
416
509
  sample_frame_count: total_samples,
417
510
  duration: Core::Duration.from_samples(total_samples, sample_rate),
418
511
  min_block_size: min_block_size,
@@ -423,12 +516,75 @@ module Wavify
423
516
  }
424
517
  end
425
518
 
426
- def encode_verbatim_stream(buffer, format)
519
+ def parse_seektable(data)
520
+ raise InvalidFormatError, "SEEKTABLE block size must be a multiple of 18" unless (data.bytesize % 18).zero?
521
+
522
+ points = data.bytes.each_slice(18).map do |bytes|
523
+ block = bytes.pack("C*")
524
+ sample_number = block[0, 8].unpack1("Q>")
525
+ {
526
+ sample_number: sample_number,
527
+ stream_offset: block[8, 8].unpack1("Q>"),
528
+ frame_samples: block[16, 2].unpack1("n"),
529
+ placeholder: sample_number == 0xFFFF_FFFF_FFFF_FFFF
530
+ }
531
+ end
532
+ { points: points }
533
+ end
534
+
535
+ def parse_vorbis_comment(data)
536
+ offset = 0
537
+ vendor, offset = read_vorbis_comment_string(data, offset, "vendor")
538
+ comment_count, offset = read_vorbis_comment_uint32(data, offset, "comment count")
539
+
540
+ raw = []
541
+ raw_bytes = []
542
+ comments = {}
543
+ comment_values = Hash.new { |hash, key| hash[key] = [] }
544
+ comment_count.times do
545
+ text, offset = read_vorbis_comment_string(data, offset, "comment")
546
+ raw_bytes << text.b.dup.freeze
547
+ display_text = text.scrub
548
+ raw << display_text
549
+ key, value = display_text.split("=", 2)
550
+ if key && value
551
+ normalized_key = key.downcase
552
+ comments[normalized_key] ||= value
553
+ comment_values[normalized_key] << value
554
+ end
555
+ end
556
+
557
+ {
558
+ vendor: vendor.scrub,
559
+ raw: raw.freeze,
560
+ raw_bytes: raw_bytes.freeze,
561
+ comments: comments.freeze,
562
+ comment_values: comment_values.transform_values(&:freeze).freeze
563
+ }
564
+ end
565
+
566
+ def read_vorbis_comment_uint32(data, offset, label)
567
+ raise InvalidFormatError, "truncated Vorbis Comment #{label}" if offset + 4 > data.bytesize
568
+
569
+ [data.byteslice(offset, 4).unpack1("V"), offset + 4]
570
+ end
571
+
572
+ def read_vorbis_comment_string(data, offset, label)
573
+ length, offset = read_vorbis_comment_uint32(data, offset, "#{label} length")
574
+ raise InvalidFormatError, "truncated Vorbis Comment #{label}" if offset + length > data.bytesize
575
+
576
+ [data.byteslice(offset, length).force_encoding(Encoding::UTF_8), offset + length]
577
+ end
578
+
579
+ def encode_verbatim_stream(buffer, format, block_size: DEFAULT_ENCODE_BLOCK_SIZE, comments: nil,
580
+ stereo_coding: :auto, predictor: :auto)
427
581
  encoded_frames = encode_verbatim_frames(
428
582
  buffer.samples,
429
583
  format,
430
584
  start_frame_number: 0,
431
- block_size: DEFAULT_ENCODE_BLOCK_SIZE
585
+ block_size: block_size,
586
+ stereo_coding: stereo_coding,
587
+ predictor: predictor
432
588
  )
433
589
  md5_hex = pcm_md5_hex(buffer.samples, format)
434
590
 
@@ -440,24 +596,35 @@ module Wavify
440
596
  )
441
597
 
442
598
  bytes = +"fLaC"
443
- bytes << [0x80, 0x00, 0x00, STREAMINFO_LENGTH].pack("C4")
599
+ bytes << metadata_block_header(STREAMINFO_BLOCK_TYPE, STREAMINFO_LENGTH, last: comments.nil?)
444
600
  bytes << streaminfo
601
+ if comments
602
+ comment_block = build_vorbis_comment_block(comments)
603
+ bytes << metadata_block_header(VORBIS_COMMENT_BLOCK_TYPE, comment_block.bytesize, last: true)
604
+ bytes << comment_block
605
+ end
445
606
  bytes << encoded_frames.fetch(:bytes)
446
607
  bytes
447
608
  end
448
609
 
449
- def write_stream_header(io)
450
- io.write("fLaC")
451
- io.write([0x80, 0x00, 0x00, STREAMINFO_LENGTH].pack("C4"))
610
+ def write_stream_header(io, comments: nil)
611
+ write_all(io, "fLaC")
612
+ write_all(io, metadata_block_header(STREAMINFO_BLOCK_TYPE, STREAMINFO_LENGTH, last: comments.nil?))
452
613
  streaminfo_offset = io.pos
453
- io.write("\x00" * STREAMINFO_LENGTH)
614
+ write_all(io, "\x00" * STREAMINFO_LENGTH)
615
+ if comments
616
+ comment_block = build_vorbis_comment_block(comments)
617
+ write_all(io, metadata_block_header(VORBIS_COMMENT_BLOCK_TYPE, comment_block.bytesize, last: true))
618
+ write_all(io, comment_block)
619
+ end
454
620
  { streaminfo_offset: streaminfo_offset }
455
621
  end
456
622
 
457
623
  def finalize_stream_header(io, header, format, total_sample_frames, encode_stats)
458
624
  file_end = io.pos
459
625
  io.seek(header.fetch(:streaminfo_offset), IO::SEEK_SET)
460
- io.write(
626
+ write_all(
627
+ io,
461
628
  build_streaminfo_bytes(
462
629
  format: format,
463
630
  sample_frame_count: total_sample_frames,
@@ -477,7 +644,96 @@ module Wavify
477
644
  }
478
645
  end
479
646
 
480
- def normalize_stream_write_options(block_size, block_size_strategy)
647
+ def metadata_block_header(block_type, length, last:)
648
+ [block_type | (last ? 0x80 : 0), (length >> 16) & 0xFF, (length >> 8) & 0xFF, length & 0xFF].pack("C4")
649
+ end
650
+
651
+ def build_vorbis_comment_block(comments)
652
+ vendor = "Wavify"
653
+ entries = comments.map do |entry|
654
+ key, value = entry.split("=", 2)
655
+ "#{key.upcase}=#{value}"
656
+ end
657
+ bytes = [vendor.bytesize].pack("V") + vendor + [entries.length].pack("V")
658
+ entries.each do |entry|
659
+ bytes << [entry.bytesize].pack("V")
660
+ bytes << entry
661
+ end
662
+ bytes
663
+ end
664
+
665
+ def normalize_vorbis_comments(comments)
666
+ return nil if comments.nil?
667
+
668
+ normalized = case comments
669
+ when Hash
670
+ comments.flat_map do |key, value|
671
+ Array(value).map { |entry| normalize_vorbis_comment_entry(key, entry) }
672
+ end
673
+ when Array
674
+ comments.map do |entry|
675
+ key, value = entry.to_s.split("=", 2)
676
+ raise InvalidParameterError, "FLAC comments must be KEY=VALUE strings" unless key && value
677
+
678
+ normalize_vorbis_comment_entry(key, value)
679
+ end
680
+ else
681
+ raise InvalidParameterError, "comments must be a Hash or Array"
682
+ end
683
+ normalized.empty? ? nil : normalized
684
+ end
685
+
686
+ def normalize_vorbis_comment_entry(key, value)
687
+ field_name = key.to_s
688
+ unless field_name.match?(/\A[\x20-\x3C\x3E-\x7D]+\z/)
689
+ raise InvalidParameterError, "FLAC comment field names must be printable ASCII without '='"
690
+ end
691
+
692
+ text = value.to_s.encode(Encoding::UTF_8)
693
+ raise InvalidParameterError, "FLAC comment values must be valid UTF-8" unless text.valid_encoding?
694
+
695
+ "#{field_name}=#{text}"
696
+ rescue EncodingError
697
+ raise InvalidParameterError, "FLAC comment values must be valid UTF-8"
698
+ end
699
+
700
+ def normalize_stereo_coding!(stereo_coding)
701
+ value = stereo_coding.to_sym
702
+ return value if %i[auto independent mid_side].include?(value)
703
+
704
+ raise InvalidParameterError, "stereo_coding must be :auto, :independent, or :mid_side"
705
+ rescue NoMethodError
706
+ raise InvalidParameterError, "stereo_coding must be Symbol/String"
707
+ end
708
+
709
+ def normalize_predictor!(predictor)
710
+ value = predictor.to_sym
711
+ return value if %i[auto fixed lpc verbatim].include?(value)
712
+
713
+ raise InvalidParameterError, "predictor must be :auto, :fixed, :lpc, or :verbatim"
714
+ rescue NoMethodError
715
+ raise InvalidParameterError, "predictor must be Symbol/String"
716
+ end
717
+
718
+ def normalize_write_block_size(block_size, compression_level)
719
+ level = normalize_compression_level(compression_level)
720
+ return normalize_encode_block_size(block_size) unless level && block_size == DEFAULT_ENCODE_BLOCK_SIZE
721
+
722
+ normalize_encode_block_size(COMPRESSION_BLOCK_SIZES.fetch(level))
723
+ end
724
+
725
+ def normalize_compression_level(compression_level)
726
+ return nil if compression_level.nil?
727
+
728
+ level = Integer(compression_level)
729
+ return level if level.between?(0, 8)
730
+
731
+ raise InvalidParameterError, "compression_level must be in 0..8"
732
+ rescue ArgumentError, TypeError
733
+ raise InvalidParameterError, "compression_level must be an Integer"
734
+ end
735
+
736
+ def normalize_stream_write_options(block_size, block_size_strategy, compression_level = nil)
481
737
  strategy = block_size_strategy.to_sym
482
738
  supported = %i[per_chunk source_chunk fixed]
483
739
  unless supported.include?(strategy)
@@ -486,7 +742,7 @@ module Wavify
486
742
 
487
743
  {
488
744
  strategy: strategy,
489
- block_size: normalize_encode_block_size(block_size)
745
+ block_size: normalize_write_block_size(block_size, compression_level)
490
746
  }
491
747
  rescue NoMethodError
492
748
  raise InvalidParameterError, "block_size_strategy must be Symbol/String: #{block_size_strategy.inspect}"
@@ -507,7 +763,8 @@ module Wavify
507
763
  aggregate[:max_frame_size] = [aggregate[:max_frame_size], current_max_frame].max
508
764
  end
509
765
 
510
- def encode_verbatim_frames(interleaved_samples, format, start_frame_number:, block_size:)
766
+ def encode_verbatim_frames(interleaved_samples, format, start_frame_number:, block_size:, stereo_coding: :auto,
767
+ predictor: :auto)
511
768
  channels = format.channels
512
769
  samples_per_frame = channels * normalize_encode_block_size(block_size)
513
770
  bytes = +""
@@ -516,7 +773,13 @@ module Wavify
516
773
  frame_sizes = []
517
774
 
518
775
  interleaved_samples.each_slice(samples_per_frame) do |frame_samples|
519
- encoded_frame = encode_pcm_frame(frame_samples, format, frame_number: frame_number)
776
+ encoded_frame = encode_pcm_frame(
777
+ frame_samples,
778
+ format,
779
+ frame_number: frame_number,
780
+ stereo_coding: stereo_coding,
781
+ predictor: predictor
782
+ )
520
783
  bytes << encoded_frame
521
784
  block_sizes << (frame_samples.length / channels)
522
785
  frame_sizes << encoded_frame.bytesize
@@ -533,25 +796,47 @@ module Wavify
533
796
  }
534
797
  end
535
798
 
536
- def encode_pcm_frame(interleaved_samples, format, frame_number:)
799
+ def encode_pcm_frame(interleaved_samples, format, frame_number:, stereo_coding:, predictor:)
537
800
  channels = format.channels
538
801
  block_size = interleaved_samples.length / channels
539
802
  raise InvalidParameterError, "FLAC frame block size must be positive" if block_size <= 0
540
803
 
541
804
  block_size_code, block_size_extra_bits = encode_block_size_descriptor(block_size)
542
805
  channel_samples = deinterleave_samples(interleaved_samples, channels)
806
+ candidates = frame_channel_candidates(channel_samples, format, stereo_coding)
807
+
808
+ encoded_candidates = candidates.map do |candidate|
809
+ encode_pcm_frame_candidate(
810
+ candidate,
811
+ block_size: block_size,
812
+ block_size_code: block_size_code,
813
+ block_size_extra_bits: block_size_extra_bits,
814
+ frame_number: frame_number,
815
+ predictor: predictor
816
+ )
817
+ end
818
+ selected = if stereo_coding == :auto
819
+ encoded_candidates.min_by(&:bytesize)
820
+ else
821
+ encoded_candidates.fetch(0)
822
+ end
823
+ selected
824
+ end
825
+
826
+ def encode_pcm_frame_candidate(candidate, block_size:, block_size_code:, block_size_extra_bits:,
827
+ frame_number:, predictor:)
543
828
  header_without_crc8 = build_frame_header_bytes(
544
829
  block_size: block_size,
545
830
  block_size_code: block_size_code,
546
831
  block_size_extra_bits: block_size_extra_bits,
547
- channels: channels,
832
+ channel_assignment: candidate.fetch(:channel_assignment),
548
833
  frame_number: frame_number
549
834
  )
550
835
  header_crc8 = flac_crc8(header_without_crc8)
551
836
 
552
837
  payload_writer = BitWriter.new
553
- channel_samples.each do |channel|
554
- write_best_subframe(payload_writer, channel, format.bit_depth)
838
+ candidate.fetch(:channel_samples).each_with_index do |channel, channel_index|
839
+ write_best_subframe(payload_writer, channel, candidate.fetch(:sample_sizes).fetch(channel_index), predictor: predictor)
555
840
  end
556
841
  payload_writer.align_to_byte
557
842
  payload_bytes = payload_writer.to_s
@@ -562,8 +847,29 @@ module Wavify
562
847
  crc16_input + [crc16].pack("n")
563
848
  end
564
849
 
565
- def write_best_subframe(writer, channel_samples, sample_size)
566
- selection = select_subframe_encoding(channel_samples, sample_size)
850
+ def frame_channel_candidates(channel_samples, format, stereo_coding)
851
+ independent = {
852
+ channel_assignment: format.channels - 1,
853
+ channel_samples: channel_samples,
854
+ sample_sizes: Array.new(format.channels, format.valid_bits)
855
+ }
856
+ return [independent] unless format.channels == 2
857
+ return [independent] if stereo_coding == :independent
858
+
859
+ left = channel_samples.fetch(0)
860
+ right = channel_samples.fetch(1)
861
+ side = left.zip(right).map { |l, r| l - r }
862
+ mid = left.zip(right).map { |l, r| (l + r) >> 1 }
863
+ mid_side = {
864
+ channel_assignment: 10,
865
+ channel_samples: [mid, side],
866
+ sample_sizes: [format.valid_bits, format.valid_bits + 1]
867
+ }
868
+ stereo_coding == :mid_side ? [mid_side] : [independent, mid_side]
869
+ end
870
+
871
+ def write_best_subframe(writer, channel_samples, sample_size, predictor:)
872
+ selection = select_subframe_encoding(channel_samples, sample_size, predictor: predictor)
567
873
 
568
874
  if selection[:kind] == :fixed
569
875
  write_fixed_subframe(
@@ -575,22 +881,45 @@ module Wavify
575
881
  return
576
882
  end
577
883
 
884
+ if selection[:kind] == :lpc
885
+ write_lpc_subframe(
886
+ writer,
887
+ channel_samples,
888
+ sample_size,
889
+ selection: selection
890
+ )
891
+ return
892
+ end
893
+
578
894
  write_verbatim_subframe(writer, channel_samples, sample_size)
579
895
  end
580
896
 
581
- def select_subframe_encoding(channel_samples, sample_size)
897
+ def select_subframe_encoding(channel_samples, sample_size, predictor:)
582
898
  best = {
583
899
  kind: :verbatim,
584
900
  bit_length: verbatim_subframe_bit_length(channel_samples.length, sample_size)
585
901
  }
902
+ return best if predictor == :verbatim
586
903
 
587
- max_predictor_order = [4, channel_samples.length - 1].min
588
- (0..max_predictor_order).each do |predictor_order|
589
- candidate = build_fixed_subframe_encoding(channel_samples, sample_size, predictor_order)
590
- next unless candidate
591
- next unless candidate.fetch(:bit_length) < best.fetch(:bit_length)
904
+ if %i[auto fixed].include?(predictor)
905
+ max_predictor_order = [4, channel_samples.length - 1].min
906
+ (0..max_predictor_order).each do |predictor_order|
907
+ candidate = build_fixed_subframe_encoding(channel_samples, sample_size, predictor_order)
908
+ next unless candidate
909
+ next unless candidate.fetch(:bit_length) < best.fetch(:bit_length)
592
910
 
593
- best = candidate
911
+ best = candidate
912
+ end
913
+ end
914
+
915
+ if %i[auto lpc].include?(predictor)
916
+ (1..[8, channel_samples.length - 1].min).each do |order|
917
+ candidate = build_lpc_subframe_encoding(channel_samples, sample_size, order)
918
+ next unless candidate
919
+ next unless candidate.fetch(:bit_length) < best.fetch(:bit_length)
920
+
921
+ best = candidate
922
+ end
594
923
  end
595
924
 
596
925
  best
@@ -602,7 +931,11 @@ module Wavify
602
931
 
603
932
  def build_fixed_subframe_encoding(channel_samples, sample_size, predictor_order)
604
933
  residuals = fixed_subframe_residuals(channel_samples, predictor_order)
605
- residual_encoding = choose_residual_encoding(residuals)
934
+ residual_encoding = choose_residual_encoding(
935
+ residuals,
936
+ block_size: channel_samples.length,
937
+ predictor_order: predictor_order
938
+ )
606
939
  return nil unless residual_encoding
607
940
 
608
941
  {
@@ -614,6 +947,93 @@ module Wavify
614
947
  }
615
948
  end
616
949
 
950
+ def build_lpc_subframe_encoding(channel_samples, sample_size, predictor_order)
951
+ return nil if predictor_order >= channel_samples.length
952
+
953
+ coefficient_data = quantized_lpc_coefficients(channel_samples, predictor_order, sample_size)
954
+ return nil unless coefficient_data
955
+
956
+ coefficients = coefficient_data.fetch(:coefficients)
957
+ qlp_shift = coefficient_data.fetch(:qlp_shift)
958
+ coefficient_precision = coefficient_data.fetch(:precision)
959
+ residuals = lpc_subframe_residuals(channel_samples, coefficients, qlp_shift)
960
+ residual_encoding = choose_residual_encoding(
961
+ residuals,
962
+ block_size: channel_samples.length,
963
+ predictor_order: predictor_order
964
+ )
965
+ return nil unless residual_encoding
966
+
967
+ {
968
+ kind: :lpc,
969
+ predictor_order: predictor_order,
970
+ coefficients: coefficients,
971
+ coefficient_precision: coefficient_precision,
972
+ qlp_shift: qlp_shift,
973
+ residuals: residuals,
974
+ residual_encoding: residual_encoding,
975
+ bit_length: 8 + (predictor_order * sample_size) + 4 + 5 +
976
+ (predictor_order * coefficient_precision) + residual_encoding.fetch(:bit_length)
977
+ }
978
+ end
979
+
980
+ def quantized_lpc_coefficients(samples, predictor_order, sample_size)
981
+ coefficients = levinson_durbin_coefficients(samples, predictor_order)
982
+ return nil unless coefficients
983
+
984
+ precision = [sample_size, 15].min
985
+ maximum = coefficients.map(&:abs).max
986
+ return nil unless maximum&.positive?
987
+
988
+ max_integer = (1 << (precision - 1)) - 1
989
+ qlp_shift = Math.log2(max_integer / maximum).floor.clamp(-16, 15)
990
+ quantized = coefficients.map { |coefficient| (coefficient * (2.0**qlp_shift)).round }
991
+ return nil unless quantized.all? { |coefficient| signed_bit_width(coefficient) <= precision }
992
+
993
+ { coefficients: quantized, precision: precision, qlp_shift: qlp_shift }
994
+ end
995
+
996
+ def levinson_durbin_coefficients(samples, predictor_order)
997
+ return nil if samples.length <= predictor_order
998
+
999
+ autocorrelation = Array.new(predictor_order + 1) do |lag|
1000
+ (lag...samples.length).sum { |index| samples.fetch(index).to_f * samples.fetch(index - lag) }
1001
+ end
1002
+ error = autocorrelation.fetch(0)
1003
+ return nil unless error.positive?
1004
+
1005
+ coefficients = []
1006
+ predictor_order.times do |index|
1007
+ correction = index.times.sum do |coefficient_index|
1008
+ coefficients.fetch(coefficient_index) * autocorrelation.fetch(index - coefficient_index)
1009
+ end
1010
+ reflection = (autocorrelation.fetch(index + 1) - correction) / error
1011
+ return nil unless reflection.finite? && reflection.abs < 1.0
1012
+
1013
+ previous = coefficients.dup
1014
+ index.times do |coefficient_index|
1015
+ coefficients[coefficient_index] = previous.fetch(coefficient_index) -
1016
+ (reflection * previous.fetch(index - coefficient_index - 1))
1017
+ end
1018
+ coefficients[index] = reflection
1019
+ error *= 1.0 - (reflection * reflection)
1020
+ return nil unless error.positive?
1021
+ end
1022
+ coefficients
1023
+ end
1024
+
1025
+ def lpc_subframe_residuals(samples, coefficients, qlp_shift)
1026
+ predictor_order = coefficients.length
1027
+ samples.drop(predictor_order).each_with_index.map do |sample, index|
1028
+ history_index = predictor_order + index
1029
+ sum = coefficients.each_with_index.sum do |coefficient, coefficient_index|
1030
+ coefficient * samples.fetch(history_index - coefficient_index - 1)
1031
+ end
1032
+ predicted = qlp_shift.negative? ? (sum << -qlp_shift) : (sum >> qlp_shift)
1033
+ sample - predicted
1034
+ end
1035
+ end
1036
+
617
1037
  def fixed_subframe_residuals(samples, predictor_order)
618
1038
  history = samples.first(predictor_order).dup
619
1039
 
@@ -625,27 +1045,67 @@ module Wavify
625
1045
  end
626
1046
  end
627
1047
 
628
- def choose_residual_encoding(residuals)
629
- rice_candidates = (0..14).map do |parameter|
1048
+ def choose_residual_encoding(residuals, block_size:, predictor_order:)
1049
+ max_partition_order = if block_size < 256
1050
+ 0
1051
+ else
1052
+ [Math.log2(block_size / 256).floor, 6].min
1053
+ end
1054
+ candidates = (0..max_partition_order).filter_map do |partition_order|
1055
+ partition_count = 1 << partition_order
1056
+ next unless (block_size % partition_count).zero?
1057
+
1058
+ partition_size = block_size / partition_count
1059
+ next if partition_size < predictor_order
1060
+
1061
+ partitions = residual_partitions(
1062
+ residuals,
1063
+ partition_count: partition_count,
1064
+ partition_size: partition_size,
1065
+ predictor_order: predictor_order
1066
+ )
1067
+ next unless partitions
1068
+
1069
+ encodings = partitions.map { |partition| choose_rice_partition_encoding(partition) }
630
1070
  {
631
- kind: :rice,
632
- parameter: parameter,
633
- bit_length: rice_partition0_bit_length(residuals, parameter)
1071
+ kind: :partitioned,
1072
+ partition_order: partition_order,
1073
+ partitions: partitions.zip(encodings),
1074
+ bit_length: 6 + encodings.sum { |encoding| encoding.fetch(:bit_length) }
634
1075
  }
635
1076
  end
636
-
637
- escape_candidate = escape_residual_encoding(residuals)
638
- candidates = rice_candidates
639
- candidates << escape_candidate if escape_candidate
640
1077
  candidates.min_by { |candidate| candidate.fetch(:bit_length) }
641
1078
  end
642
1079
 
643
- def rice_partition0_bit_length(residuals, parameter)
644
- residuals.reduce(6 + 4) do |bits, residual|
645
- unsigned = residual >= 0 ? (residual << 1) : ((-residual << 1) - 1)
646
- quotient = unsigned >> parameter
647
- bits + quotient + 1 + parameter
1080
+ def residual_partitions(residuals, partition_count:, partition_size:, predictor_order:)
1081
+ offset = 0
1082
+ partitions = Array.new(partition_count) do |partition_index|
1083
+ length = partition_size - (partition_index.zero? ? predictor_order : 0)
1084
+ partition = residuals.slice(offset, length)
1085
+ return nil unless partition&.length == length
1086
+
1087
+ offset += length
1088
+ partition
1089
+ end
1090
+ offset == residuals.length ? partitions : nil
1091
+ end
1092
+
1093
+ def choose_rice_partition_encoding(residuals)
1094
+ unsigned = residuals.map { |residual| residual >= 0 ? (residual << 1) : ((-residual << 1) - 1) }
1095
+ mean = unsigned.empty? ? 0.0 : unsigned.sum.to_f / unsigned.length
1096
+ estimate = mean.positive? ? Math.log2(mean).floor.clamp(0, 14) : 0
1097
+ parameters = [estimate - 1, estimate, estimate + 1].select { |value| value.between?(0, 14) }.uniq
1098
+ rice_candidates = parameters.map do |parameter|
1099
+ { kind: :rice, parameter: parameter, bit_length: 4 + rice_data_bit_length(unsigned, parameter) }
648
1100
  end
1101
+ escape = escape_residual_encoding(residuals)
1102
+ rice_candidates << escape if escape
1103
+ rice_candidates.min_by { |candidate| candidate.fetch(:bit_length) }
1104
+ end
1105
+
1106
+ def rice_data_bit_length(unsigned_residuals, parameter)
1107
+ unsigned_residuals.sum { |value| value >> parameter } +
1108
+ (unsigned_residuals.length * (parameter + 1))
649
1109
  end
650
1110
 
651
1111
  def escape_residual_encoding(residuals)
@@ -655,7 +1115,7 @@ module Wavify
655
1115
  {
656
1116
  kind: :escape,
657
1117
  raw_bits: raw_bits,
658
- bit_length: 6 + 4 + 5 + (residuals.length * raw_bits)
1118
+ bit_length: 4 + 5 + (residuals.length * raw_bits)
659
1119
  }
660
1120
  end
661
1121
 
@@ -670,41 +1130,40 @@ module Wavify
670
1130
  def write_fixed_subframe(writer, channel_samples, sample_size, selection:)
671
1131
  predictor_order = selection.fetch(:predictor_order)
672
1132
  residual_encoding = selection.fetch(:residual_encoding)
673
- residuals = selection.fetch(:residuals)
674
1133
 
675
1134
  writer.write_bits(0, 1) # padding bit
676
1135
  writer.write_bits(8 + predictor_order, 6)
677
1136
  writer.write_bits(0, 1) # no wasted bits
678
1137
 
679
1138
  channel_samples.first(predictor_order).each { |sample| writer.write_signed_bits(sample, sample_size) }
680
- write_partition0_residuals(writer, residual_encoding, residuals)
1139
+ write_residuals(writer, residual_encoding)
681
1140
  end
682
1141
 
683
- def write_partition0_residuals(writer, residual_encoding, residuals)
684
- if residual_encoding[:kind] == :rice
685
- writer.write_bits(0, 2) # Rice
686
- writer.write_bits(0, 4) # partition order = 0
687
- writer.write_bits(residual_encoding.fetch(:parameter), 4)
688
- residuals.each { |residual| writer.write_rice_signed(residual, residual_encoding.fetch(:parameter)) }
689
- return
690
- end
691
-
1142
+ def write_residuals(writer, residual_encoding)
692
1143
  writer.write_bits(0, 2) # Rice coding method family
693
- writer.write_bits(0, 4) # partition order = 0
694
- writer.write_bits(0xF, 4) # escape code
695
- raw_bits = residual_encoding.fetch(:raw_bits)
696
- writer.write_bits(raw_bits, 5)
697
- residuals.each { |residual| writer.write_signed_bits(residual, raw_bits) } if raw_bits.positive?
1144
+ writer.write_bits(residual_encoding.fetch(:partition_order), 4)
1145
+ residual_encoding.fetch(:partitions).each do |residuals, partition|
1146
+ if partition[:kind] == :rice
1147
+ writer.write_bits(partition.fetch(:parameter), 4)
1148
+ residuals.each { |residual| writer.write_rice_signed(residual, partition.fetch(:parameter)) }
1149
+ next
1150
+ end
1151
+
1152
+ writer.write_bits(0xF, 4) # escape code
1153
+ raw_bits = partition.fetch(:raw_bits)
1154
+ writer.write_bits(raw_bits, 5)
1155
+ residuals.each { |residual| writer.write_signed_bits(residual, raw_bits) } if raw_bits.positive?
1156
+ end
698
1157
  end
699
1158
 
700
- def build_frame_header_bytes(block_size:, block_size_code:, block_size_extra_bits:, channels:, frame_number:)
1159
+ def build_frame_header_bytes(block_size:, block_size_code:, block_size_extra_bits:, channel_assignment:, frame_number:)
701
1160
  writer = BitWriter.new
702
1161
  writer.write_bits(FLAC_SYNC_CODE, 14)
703
1162
  writer.write_bits(0, 1) # reserved
704
1163
  writer.write_bits(0, 1) # fixed-blocksize stream
705
1164
  writer.write_bits(block_size_code, 4)
706
1165
  writer.write_bits(0, 4) # sample rate from STREAMINFO
707
- writer.write_bits(channels - 1, 4) # independent channels
1166
+ writer.write_bits(channel_assignment, 4)
708
1167
  writer.write_bits(0, 3) # sample size from STREAMINFO
709
1168
  writer.write_bits(0, 1) # reserved
710
1169
  write_utf8_uint(writer, frame_number)
@@ -720,6 +1179,21 @@ module Wavify
720
1179
  channel_samples.each { |sample| writer.write_signed_bits(sample, sample_size) }
721
1180
  end
722
1181
 
1182
+ def write_lpc_subframe(writer, channel_samples, sample_size, selection:)
1183
+ predictor_order = selection.fetch(:predictor_order)
1184
+ writer.write_bits(0, 1) # padding bit
1185
+ writer.write_bits(32 + predictor_order - 1, 6)
1186
+ writer.write_bits(0, 1) # no wasted bits
1187
+
1188
+ channel_samples.first(predictor_order).each { |sample| writer.write_signed_bits(sample, sample_size) }
1189
+ writer.write_bits(selection.fetch(:coefficient_precision) - 1, 4)
1190
+ writer.write_signed_bits(selection.fetch(:qlp_shift), 5)
1191
+ selection.fetch(:coefficients).each do |coefficient|
1192
+ writer.write_signed_bits(coefficient, selection.fetch(:coefficient_precision))
1193
+ end
1194
+ write_residuals(writer, selection.fetch(:residual_encoding))
1195
+ end
1196
+
723
1197
  def deinterleave_samples(interleaved_samples, channels)
724
1198
  channel_samples = Array.new(channels) { [] }
725
1199
 
@@ -776,7 +1250,7 @@ module Wavify
776
1250
  size = block_size.to_i
777
1251
  raise InvalidParameterError, "FLAC block_size must be a positive Integer" unless size.positive?
778
1252
 
779
- [size, 65_536].min
1253
+ [size, 65_535].min
780
1254
  end
781
1255
 
782
1256
  def build_streaminfo_bytes(format:, sample_frame_count:, stats:, md5_hex:)
@@ -791,7 +1265,7 @@ module Wavify
791
1265
 
792
1266
  packed = ((format.sample_rate & 0xFFFFF) << 44) |
793
1267
  (((format.channels - 1) & 0x7) << 41) |
794
- (((format.bit_depth - 1) & 0x1F) << 36) |
1268
+ (((format.valid_bits - 1) & 0x1F) << 36) |
795
1269
  (sample_frame_count & 0xFFFFFFFFF)
796
1270
 
797
1271
  [min_block_size, max_block_size].pack("n2") +
@@ -802,22 +1276,41 @@ module Wavify
802
1276
  end
803
1277
 
804
1278
  def pcm_md5_hex(samples, format)
805
- Digest::MD5.hexdigest(pcm_bytes_for_md5(samples, format))
1279
+ encoded_samples = unalign_encoded_samples(samples, format)
1280
+ Digest::MD5.hexdigest(flac_md5_bytes(encoded_samples, format.valid_bits))
806
1281
  end
807
1282
 
808
1283
  def pcm_bytes_for_md5(samples, format)
809
- case format.bit_depth
810
- when 8
811
- samples.pack("c*")
812
- when 16
813
- samples.pack("s<*")
814
- when 24
815
- encode_pcm24_le(samples)
816
- when 32
817
- samples.pack("l<*")
818
- else
819
- raise UnsupportedFormatError, "unsupported FLAC MD5 PCM bit depth: #{format.bit_depth}"
1284
+ encoded_samples = unalign_encoded_samples(samples, format)
1285
+ flac_md5_bytes(encoded_samples, format.valid_bits)
1286
+ end
1287
+
1288
+ def flac_md5_bytes(samples, valid_bits)
1289
+ byte_width = (valid_bits + 7) / 8
1290
+ mask = (1 << (byte_width * 8)) - 1
1291
+ bytes = String.new(capacity: samples.length * byte_width, encoding: Encoding::BINARY)
1292
+ samples.each do |sample|
1293
+ value = sample & mask
1294
+ byte_width.times do
1295
+ bytes << (value & 0xFF)
1296
+ value >>= 8
1297
+ end
820
1298
  end
1299
+ bytes
1300
+ end
1301
+
1302
+ def unalign_encoded_samples(samples, format)
1303
+ shift = format.bit_depth - format.valid_bits
1304
+ return samples if shift.zero?
1305
+
1306
+ samples.map { |sample| sample >> shift }
1307
+ end
1308
+
1309
+ def align_decoded_samples(samples, format)
1310
+ shift = format.bit_depth - format.valid_bits
1311
+ return samples if shift.zero?
1312
+
1313
+ samples.map { |sample| sample << shift }
821
1314
  end
822
1315
 
823
1316
  def encode_pcm24_le(samples)
@@ -880,14 +1373,11 @@ module Wavify
880
1373
  format = metadata.fetch(:format)
881
1374
  remaining_frames = metadata[:sample_frame_count]
882
1375
  bounded_total = remaining_frames.is_a?(Integer) && remaining_frames.positive?
1376
+ md5 = decoded_pcm_md5(metadata)
883
1377
 
884
1378
  until io.eof?
885
1379
  break if bounded_total && remaining_frames <= 0
886
1380
 
887
- next_byte = io.read(1)
888
- break if next_byte.nil?
889
-
890
- io.seek(-1, IO::SEEK_CUR)
891
1381
  frame_samples = decode_frame(io, metadata)
892
1382
 
893
1383
  if bounded_total
@@ -898,12 +1388,16 @@ module Wavify
898
1388
  remaining_frames -= decoded_frame_count
899
1389
  end
900
1390
 
901
- yield frame_samples unless frame_samples.empty?
1391
+ md5&.update(flac_md5_bytes(frame_samples, metadata.fetch(:encoded_bit_depth)))
1392
+ aligned_samples = align_decoded_samples(frame_samples, format)
1393
+ yield aligned_samples unless aligned_samples.empty?
902
1394
  end
903
1395
 
904
- return unless bounded_total && remaining_frames.positive?
1396
+ if bounded_total && remaining_frames.positive?
1397
+ raise InvalidFormatError, "decoded FLAC samples are shorter than STREAMINFO total sample count"
1398
+ end
905
1399
 
906
- raise InvalidFormatError, "decoded FLAC samples are shorter than STREAMINFO total sample count"
1400
+ verify_decoded_pcm_md5!(md5, metadata[:md5])
907
1401
  end
908
1402
 
909
1403
  def decode_frame(io, metadata)
@@ -912,7 +1406,9 @@ module Wavify
912
1406
  channel_samples = decode_subframes(bit_reader, frame_header)
913
1407
  channel_samples = restore_channel_assignment(channel_samples, frame_header)
914
1408
  bit_reader.align_to_byte
915
- _crc16 = bit_reader.read_bits(16)
1409
+ actual_crc16 = flac_crc16(bit_reader.captured_bytes)
1410
+ expected_crc16 = bit_reader.read_bits(16)
1411
+ raise InvalidFormatError, "FLAC frame CRC-16 mismatch" unless expected_crc16 == actual_crc16
916
1412
 
917
1413
  interleave_channels(channel_samples, frame_header.fetch(:block_size), frame_header.fetch(:channels))
918
1414
  end
@@ -937,8 +1433,10 @@ module Wavify
937
1433
  _frame_number = read_utf8_uint(bit_reader)
938
1434
  block_size = decode_block_size(block_size_code, bit_reader)
939
1435
  sample_rate = decode_sample_rate(sample_rate_code, bit_reader, metadata.fetch(:format).sample_rate)
940
- sample_size = decode_sample_size(sample_size_code, metadata.fetch(:format).bit_depth)
941
- _crc8 = bit_reader.read_bits(8)
1436
+ sample_size = decode_sample_size(sample_size_code, metadata.fetch(:encoded_bit_depth))
1437
+ actual_crc8 = flac_crc8(bit_reader.captured_bytes)
1438
+ expected_crc8 = bit_reader.read_bits(8)
1439
+ raise InvalidFormatError, "FLAC frame header CRC-8 mismatch" unless expected_crc8 == actual_crc8
942
1440
 
943
1441
  channels = decode_channel_count(channel_assignment, metadata.fetch(:format).channels)
944
1442
 
@@ -951,6 +1449,20 @@ module Wavify
951
1449
  }
952
1450
  end
953
1451
 
1452
+ def decoded_pcm_md5(metadata)
1453
+ expected = metadata[:md5]
1454
+ return if expected.nil? || expected == ("0" * 32)
1455
+
1456
+ Digest::MD5.new
1457
+ end
1458
+
1459
+ def verify_decoded_pcm_md5!(md5, expected)
1460
+ return unless md5
1461
+ return if md5.hexdigest == expected
1462
+
1463
+ raise InvalidFormatError, "FLAC STREAMINFO MD5 mismatch"
1464
+ end
1465
+
954
1466
  def read_utf8_uint(bit_reader)
955
1467
  first = bit_reader.read_bits(8)
956
1468
  return first if first.nobits?(0x80)
@@ -1189,8 +1701,14 @@ module Wavify
1189
1701
 
1190
1702
  def read_unary_zero_run(bit_reader)
1191
1703
  count = 0
1192
- count += 1 while bit_reader.read_bits(1).zero?
1193
- count
1704
+ loop do
1705
+ return count unless bit_reader.read_bits(1).zero?
1706
+
1707
+ count += 1
1708
+ if count > MAX_UNARY_ZERO_RUN
1709
+ raise InvalidFormatError, "FLAC unary zero run exceeds resource limit"
1710
+ end
1711
+ end
1194
1712
  end
1195
1713
 
1196
1714
  def reconstruct_fixed_subframe(warmup, residuals, predictor_order)
@@ -1288,34 +1806,6 @@ module Wavify
1288
1806
  bytes.unpack("C3").then { |b0, b1, b2| (b0 << 16) | (b1 << 8) | b2 }
1289
1807
  end
1290
1808
 
1291
- def read_exact(io, size, message)
1292
- data = io.read(size)
1293
- raise InvalidFormatError, message if data.nil? || data.bytesize != size
1294
-
1295
- data
1296
- end
1297
-
1298
- def ensure_seekable!(io)
1299
- return if io.respond_to?(:seek) && io.respond_to?(:rewind)
1300
-
1301
- raise StreamError, "FLAC codec requires seekable IO"
1302
- end
1303
-
1304
- def open_input(io_or_path)
1305
- return [io_or_path, false] if io_or_path.respond_to?(:read)
1306
- raise InvalidParameterError, "input path must be String or IO: #{io_or_path.inspect}" unless io_or_path.is_a?(String)
1307
-
1308
- [File.open(io_or_path, "rb"), true]
1309
- rescue Errno::ENOENT
1310
- raise InvalidFormatError, "input file not found: #{io_or_path}"
1311
- end
1312
-
1313
- def open_output(io_or_path)
1314
- return [io_or_path, false] if io_or_path.respond_to?(:write)
1315
- raise InvalidParameterError, "output path must be String or IO: #{io_or_path.inspect}" unless io_or_path.is_a?(String)
1316
-
1317
- [File.open(io_or_path, "wb"), true]
1318
- end
1319
1809
  end
1320
1810
  end
1321
1811
  end