wavify 0.1.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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/.serena/.gitignore +1 -0
  3. data/.serena/memories/project_overview.md +5 -0
  4. data/.serena/memories/style_and_completion.md +5 -0
  5. data/.serena/memories/suggested_commands.md +11 -0
  6. data/.serena/project.yml +126 -0
  7. data/.simplecov +18 -0
  8. data/.yardopts +4 -0
  9. data/CHANGELOG.md +11 -0
  10. data/LICENSE +21 -0
  11. data/README.md +196 -0
  12. data/Rakefile +190 -0
  13. data/benchmarks/README.md +46 -0
  14. data/benchmarks/benchmark_helper.rb +112 -0
  15. data/benchmarks/dsp_effects_benchmark.rb +46 -0
  16. data/benchmarks/flac_benchmark.rb +74 -0
  17. data/benchmarks/streaming_memory_benchmark.rb +94 -0
  18. data/benchmarks/wav_io_benchmark.rb +110 -0
  19. data/examples/audio_processing.rb +73 -0
  20. data/examples/cinematic_transition.rb +118 -0
  21. data/examples/drum_machine.rb +74 -0
  22. data/examples/format_convert.rb +81 -0
  23. data/examples/hybrid_arrangement.rb +165 -0
  24. data/examples/streaming_master_chain.rb +129 -0
  25. data/examples/synth_pad.rb +42 -0
  26. data/lib/wavify/audio.rb +483 -0
  27. data/lib/wavify/codecs/aiff.rb +338 -0
  28. data/lib/wavify/codecs/base.rb +108 -0
  29. data/lib/wavify/codecs/flac.rb +1322 -0
  30. data/lib/wavify/codecs/ogg_vorbis.rb +1447 -0
  31. data/lib/wavify/codecs/raw.rb +193 -0
  32. data/lib/wavify/codecs/registry.rb +87 -0
  33. data/lib/wavify/codecs/wav.rb +459 -0
  34. data/lib/wavify/core/duration.rb +99 -0
  35. data/lib/wavify/core/format.rb +133 -0
  36. data/lib/wavify/core/sample_buffer.rb +216 -0
  37. data/lib/wavify/core/stream.rb +129 -0
  38. data/lib/wavify/dsl.rb +537 -0
  39. data/lib/wavify/dsp/effects/chorus.rb +98 -0
  40. data/lib/wavify/dsp/effects/compressor.rb +85 -0
  41. data/lib/wavify/dsp/effects/delay.rb +69 -0
  42. data/lib/wavify/dsp/effects/distortion.rb +64 -0
  43. data/lib/wavify/dsp/effects/effect_base.rb +68 -0
  44. data/lib/wavify/dsp/effects/reverb.rb +112 -0
  45. data/lib/wavify/dsp/effects.rb +21 -0
  46. data/lib/wavify/dsp/envelope.rb +97 -0
  47. data/lib/wavify/dsp/filter.rb +271 -0
  48. data/lib/wavify/dsp/oscillator.rb +123 -0
  49. data/lib/wavify/errors.rb +34 -0
  50. data/lib/wavify/sequencer/engine.rb +278 -0
  51. data/lib/wavify/sequencer/note_sequence.rb +132 -0
  52. data/lib/wavify/sequencer/pattern.rb +102 -0
  53. data/lib/wavify/sequencer/track.rb +298 -0
  54. data/lib/wavify/sequencer.rb +12 -0
  55. data/lib/wavify/version.rb +6 -0
  56. data/lib/wavify.rb +28 -0
  57. data/tools/fixture_writer.rb +85 -0
  58. metadata +129 -0
@@ -0,0 +1,1322 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Wavify
6
+ module Codecs
7
+ # Pure Ruby FLAC codec (metadata, decode, encode, and streaming support).
8
+ class Flac < Base
9
+ # Recognized filename extensions.
10
+ EXTENSIONS = %w[.flac].freeze
11
+ STREAMINFO_BLOCK_TYPE = 0 # :nodoc:
12
+ STREAMINFO_LENGTH = 34 # :nodoc:
13
+ FLAC_SYNC_CODE = 0x3FFE # :nodoc:
14
+ # Default block size used by the FLAC stream encoder.
15
+ DEFAULT_ENCODE_BLOCK_SIZE = 4096
16
+
17
+ BLOCK_SIZE_CODES = { # :nodoc:
18
+ 1 => 192,
19
+ 2 => 576,
20
+ 3 => 1152,
21
+ 4 => 2304,
22
+ 5 => 4608,
23
+ 8 => 256,
24
+ 9 => 512,
25
+ 10 => 1024,
26
+ 11 => 2048,
27
+ 12 => 4096,
28
+ 13 => 8192,
29
+ 14 => 16_384,
30
+ 15 => 32_768
31
+ }.freeze
32
+
33
+ SAMPLE_RATE_CODES = { # :nodoc:
34
+ 1 => 88_200,
35
+ 2 => 176_400,
36
+ 3 => 192_000,
37
+ 4 => 8_000,
38
+ 5 => 16_000,
39
+ 6 => 22_050,
40
+ 7 => 24_000,
41
+ 8 => 32_000,
42
+ 9 => 44_100,
43
+ 10 => 48_000,
44
+ 11 => 96_000
45
+ }.freeze
46
+
47
+ SAMPLE_SIZE_CODES = { # :nodoc:
48
+ 1 => 8,
49
+ 2 => 12,
50
+ 4 => 16,
51
+ 5 => 20,
52
+ 6 => 24
53
+ }.freeze
54
+
55
+ # Internal bit reader used by the FLAC decoder.
56
+ class BitReader # :nodoc:
57
+ def initialize(io)
58
+ @io = io
59
+ @buffer = 0
60
+ @bits_available = 0
61
+ end
62
+
63
+ def read_bits(count)
64
+ raise InvalidFormatError, "bit count must be non-negative" unless count.is_a?(Integer) && count >= 0
65
+ return 0 if count.zero?
66
+
67
+ value = 0
68
+ remaining = count
69
+ while remaining.positive?
70
+ fill_buffer_if_needed!
71
+
72
+ take = [remaining, @bits_available].min
73
+ shift = @bits_available - take
74
+ chunk = (@buffer >> shift) & ((1 << take) - 1)
75
+ value = (value << take) | chunk
76
+ @bits_available -= take
77
+ @buffer &= ((1 << @bits_available) - 1)
78
+ remaining -= take
79
+ end
80
+
81
+ value
82
+ end
83
+
84
+ def read_signed_bits(count) # :nodoc:
85
+ value = read_bits(count)
86
+ sign_bit = 1 << (count - 1)
87
+ value.nobits?(sign_bit) ? value : (value - (1 << count))
88
+ end
89
+
90
+ def align_to_byte # :nodoc:
91
+ @buffer = 0
92
+ @bits_available = 0
93
+ end
94
+
95
+ private
96
+
97
+ def fill_buffer_if_needed!
98
+ return unless @bits_available.zero?
99
+
100
+ byte = @io.read(1)
101
+ raise InvalidFormatError, "truncated FLAC frame" if byte.nil?
102
+
103
+ @buffer = byte.getbyte(0)
104
+ @bits_available = 8
105
+ end
106
+ end
107
+
108
+ # Internal bit writer used by the FLAC encoder.
109
+ class BitWriter # :nodoc:
110
+ def initialize
111
+ @bytes = []
112
+ @buffer = 0
113
+ @bits_used = 0
114
+ end
115
+
116
+ def write_bits(value, count)
117
+ raise InvalidParameterError, "bit count must be a non-negative Integer" unless count.is_a?(Integer) && count >= 0
118
+ return if count.zero?
119
+
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
125
+ flush_byte_if_needed
126
+ end
127
+ end
128
+
129
+ def write_signed_bits(value, count) # :nodoc:
130
+ mask = (1 << count) - 1
131
+ write_bits(value & mask, count)
132
+ end
133
+
134
+ def write_unary_zeros_then_one(zero_count) # :nodoc:
135
+ zero_count.times { write_bits(0, 1) }
136
+ write_bits(1, 1)
137
+ end
138
+
139
+ def write_rice_signed(value, parameter) # :nodoc:
140
+ unsigned = value >= 0 ? (value << 1) : ((-value << 1) - 1)
141
+ quotient = unsigned >> parameter
142
+ remainder = parameter.zero? ? 0 : (unsigned & ((1 << parameter) - 1))
143
+
144
+ write_unary_zeros_then_one(quotient)
145
+ write_bits(remainder, parameter) if parameter.positive?
146
+ end
147
+
148
+ def align_to_byte # :nodoc:
149
+ return if @bits_used.zero?
150
+
151
+ @buffer <<= (8 - @bits_used)
152
+ @bytes << @buffer
153
+ @buffer = 0
154
+ @bits_used = 0
155
+ end
156
+
157
+ def to_s # :nodoc:
158
+ align_to_byte
159
+ @bytes.pack("C*")
160
+ end
161
+
162
+ private
163
+
164
+ def flush_byte_if_needed
165
+ return unless @bits_used == 8
166
+
167
+ @bytes << @buffer
168
+ @buffer = 0
169
+ @bits_used = 0
170
+ end
171
+ end
172
+
173
+ class << self
174
+ # @param io_or_path [String, IO]
175
+ # @return [Boolean]
176
+ def can_read?(io_or_path)
177
+ return true if io_or_path.is_a?(String) && EXTENSIONS.include?(File.extname(io_or_path).downcase)
178
+ return false unless io_or_path.respond_to?(:read)
179
+
180
+ magic = io_or_path.read(4)
181
+ io_or_path.rewind if io_or_path.respond_to?(:rewind)
182
+ magic == "fLaC"
183
+ end
184
+
185
+ # Reads a FLAC stream and returns decoded samples.
186
+ #
187
+ # @param io_or_path [String, IO]
188
+ # @param format [Wavify::Core::Format, nil]
189
+ # @return [Wavify::Core::SampleBuffer]
190
+ def read(io_or_path, format: nil)
191
+ io, close_io = open_input(io_or_path)
192
+ ensure_seekable!(io)
193
+
194
+ metadata = parse_metadata(io)
195
+ source_format = metadata.fetch(:format)
196
+ samples = decode_frames(io, metadata)
197
+ buffer = Core::SampleBuffer.new(samples, source_format)
198
+ format ? buffer.convert(format) : buffer
199
+ ensure
200
+ io.close if close_io && io
201
+ end
202
+
203
+ # Writes a sample buffer as FLAC.
204
+ #
205
+ # @param io_or_path [String, IO]
206
+ # @param sample_buffer [Wavify::Core::SampleBuffer]
207
+ # @param format [Wavify::Core::Format]
208
+ # @return [String, IO]
209
+ def write(io_or_path, sample_buffer, format:)
210
+ raise InvalidParameterError, "sample_buffer must be Core::SampleBuffer" unless sample_buffer.is_a?(Core::SampleBuffer)
211
+
212
+ target_format = validate_encode_format!(format)
213
+ 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
224
+ end
225
+
226
+ # Streams FLAC decoding as chunked sample buffers.
227
+ #
228
+ # @param io_or_path [String, IO]
229
+ # @param chunk_size [Integer]
230
+ # @return [Enumerator]
231
+ def stream_read(io_or_path, chunk_size: 4096)
232
+ return enum_for(__method__, io_or_path, chunk_size: chunk_size) unless block_given?
233
+ raise InvalidParameterError, "chunk_size must be a positive Integer" unless chunk_size.is_a?(Integer) && chunk_size.positive?
234
+
235
+ io, close_io = open_input(io_or_path)
236
+ ensure_seekable!(io)
237
+
238
+ metadata = parse_metadata(io)
239
+ format = metadata.fetch(:format)
240
+ chunk_sample_count = chunk_size * format.channels
241
+ pending_samples = []
242
+
243
+ each_decoded_frame_samples(io, metadata) do |frame_samples|
244
+ pending_samples.concat(frame_samples)
245
+
246
+ while pending_samples.length >= chunk_sample_count
247
+ yield Core::SampleBuffer.new(pending_samples.shift(chunk_sample_count), format)
248
+ end
249
+ end
250
+
251
+ yield Core::SampleBuffer.new(pending_samples, format) unless pending_samples.empty?
252
+ ensure
253
+ io.close if close_io && io
254
+ end
255
+
256
+ # Streams FLAC encoding and finalizes STREAMINFO on completion.
257
+ #
258
+ # @param io_or_path [String, IO]
259
+ # @param format [Wavify::Core::Format]
260
+ # @param block_size [Integer]
261
+ # @param block_size_strategy [Symbol] `:per_chunk`, `:fixed`, or `:source_chunk`
262
+ # @return [Enumerator, String, IO]
263
+ def stream_write(io_or_path, format:, block_size: DEFAULT_ENCODE_BLOCK_SIZE, block_size_strategy: :per_chunk)
264
+ unless block_given?
265
+ return enum_for(
266
+ __method__,
267
+ io_or_path,
268
+ format: format,
269
+ block_size: block_size,
270
+ block_size_strategy: block_size_strategy
271
+ )
272
+ end
273
+
274
+ target_format = validate_encode_format!(format)
275
+ stream_write_options = normalize_stream_write_options(block_size, block_size_strategy)
276
+ io, close_io = open_output(io_or_path)
277
+ ensure_seekable!(io)
278
+ io.rewind if io.respond_to?(:rewind)
279
+ io.truncate(0) if io.respond_to?(:truncate)
280
+
281
+ header = write_stream_header(io)
282
+ total_sample_frames = 0
283
+ next_frame_number = 0
284
+ encode_stats = empty_encode_stats
285
+ header[:md5] = Digest::MD5.new
286
+ pending_samples = []
287
+
288
+ writer = lambda do |chunk|
289
+ raise InvalidParameterError, "stream chunk must be Core::SampleBuffer" unless chunk.is_a?(Core::SampleBuffer)
290
+
291
+ buffer = chunk.format == target_format ? chunk : chunk.convert(target_format)
292
+ header.fetch(:md5).update(pcm_bytes_for_md5(buffer.samples, target_format))
293
+ total_sample_frames += buffer.sample_frame_count
294
+
295
+ if stream_write_options[:strategy] == :fixed
296
+ pending_samples.concat(buffer.samples)
297
+ fixed_chunk_sample_count = stream_write_options.fetch(:block_size) * target_format.channels
298
+
299
+ while pending_samples.length >= fixed_chunk_sample_count
300
+ encoded = encode_verbatim_frames(
301
+ pending_samples.shift(fixed_chunk_sample_count),
302
+ target_format,
303
+ start_frame_number: next_frame_number,
304
+ block_size: stream_write_options.fetch(:block_size)
305
+ )
306
+ io.write(encoded.fetch(:bytes))
307
+ next_frame_number = encoded.fetch(:next_frame_number)
308
+ merge_encode_stats!(encode_stats, encoded)
309
+ end
310
+ elsif stream_write_options[:strategy] == :source_chunk
311
+ encoded = encode_verbatim_frames(
312
+ buffer.samples,
313
+ target_format,
314
+ start_frame_number: next_frame_number,
315
+ block_size: buffer.sample_frame_count
316
+ )
317
+ io.write(encoded.fetch(:bytes))
318
+ next_frame_number = encoded.fetch(:next_frame_number)
319
+ merge_encode_stats!(encode_stats, encoded)
320
+ else
321
+ encoded = encode_verbatim_frames(
322
+ buffer.samples,
323
+ target_format,
324
+ start_frame_number: next_frame_number,
325
+ block_size: stream_write_options.fetch(:block_size)
326
+ )
327
+ io.write(encoded.fetch(:bytes))
328
+ next_frame_number = encoded.fetch(:next_frame_number)
329
+ merge_encode_stats!(encode_stats, encoded)
330
+ end
331
+ end
332
+
333
+ yield writer
334
+
335
+ unless pending_samples.empty?
336
+ encoded = encode_verbatim_frames(
337
+ pending_samples,
338
+ target_format,
339
+ start_frame_number: next_frame_number,
340
+ block_size: stream_write_options.fetch(:block_size)
341
+ )
342
+ io.write(encoded.fetch(:bytes))
343
+ next_frame_number = encoded.fetch(:next_frame_number)
344
+ merge_encode_stats!(encode_stats, encoded)
345
+ end
346
+
347
+ 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)
350
+ io_or_path
351
+ ensure
352
+ io.close if close_io && io
353
+ end
354
+
355
+ # Reads FLAC metadata (including STREAMINFO-derived format/duration).
356
+ #
357
+ # @param io_or_path [String, IO]
358
+ # @return [Hash]
359
+ def metadata(io_or_path)
360
+ io, close_io = open_input(io_or_path)
361
+ ensure_seekable!(io)
362
+
363
+ parse_metadata(io)
364
+ ensure
365
+ io.close if close_io && io
366
+ end
367
+
368
+ private
369
+
370
+ def parse_metadata(io)
371
+ io.rewind
372
+ marker = read_exact(io, 4, "missing FLAC stream marker")
373
+ raise InvalidFormatError, "invalid FLAC stream marker" unless marker == "fLaC"
374
+
375
+ streaminfo = nil
376
+ loop do
377
+ header = read_exact(io, 4, "truncated FLAC metadata block header")
378
+ byte0 = header.getbyte(0)
379
+ last_block = byte0.anybits?(0x80)
380
+ block_type = byte0 & 0x7F
381
+ length = ((header.getbyte(1) << 16) | (header.getbyte(2) << 8) | header.getbyte(3))
382
+ data = read_exact(io, length, "truncated FLAC metadata block")
383
+
384
+ streaminfo = parse_streaminfo(data) if block_type == STREAMINFO_BLOCK_TYPE
385
+ break if last_block
386
+ end
387
+
388
+ raise InvalidFormatError, "STREAMINFO metadata block missing" unless streaminfo
389
+
390
+ streaminfo
391
+ end
392
+
393
+ def parse_streaminfo(data)
394
+ raise InvalidFormatError, "STREAMINFO block must be 34 bytes" unless data.bytesize == STREAMINFO_LENGTH
395
+
396
+ min_block_size, max_block_size = data[0, 4].unpack("n2")
397
+ min_frame_size = unpack_uint24(data[4, 3])
398
+ max_frame_size = unpack_uint24(data[7, 3])
399
+
400
+ packed = data[10, 8].unpack1("Q>")
401
+ sample_rate = (packed >> 44) & 0xFFFFF
402
+ channels = ((packed >> 41) & 0x7) + 1
403
+ bit_depth = ((packed >> 36) & 0x1F) + 1
404
+ total_samples = packed & 0xFFFFFFFFF
405
+ md5 = data[18, 16].unpack1("H*")
406
+
407
+ format = Core::Format.new(
408
+ channels: channels,
409
+ sample_rate: sample_rate,
410
+ bit_depth: bit_depth,
411
+ sample_format: :pcm
412
+ )
413
+
414
+ {
415
+ format: format,
416
+ sample_frame_count: total_samples,
417
+ duration: Core::Duration.from_samples(total_samples, sample_rate),
418
+ min_block_size: min_block_size,
419
+ max_block_size: max_block_size,
420
+ min_frame_size: min_frame_size,
421
+ max_frame_size: max_frame_size,
422
+ md5: md5
423
+ }
424
+ end
425
+
426
+ def encode_verbatim_stream(buffer, format)
427
+ encoded_frames = encode_verbatim_frames(
428
+ buffer.samples,
429
+ format,
430
+ start_frame_number: 0,
431
+ block_size: DEFAULT_ENCODE_BLOCK_SIZE
432
+ )
433
+ md5_hex = pcm_md5_hex(buffer.samples, format)
434
+
435
+ streaminfo = build_streaminfo_bytes(
436
+ format: format,
437
+ sample_frame_count: buffer.sample_frame_count,
438
+ stats: encoded_frames,
439
+ md5_hex: md5_hex
440
+ )
441
+
442
+ bytes = +"fLaC"
443
+ bytes << [0x80, 0x00, 0x00, STREAMINFO_LENGTH].pack("C4")
444
+ bytes << streaminfo
445
+ bytes << encoded_frames.fetch(:bytes)
446
+ bytes
447
+ end
448
+
449
+ def write_stream_header(io)
450
+ io.write("fLaC")
451
+ io.write([0x80, 0x00, 0x00, STREAMINFO_LENGTH].pack("C4"))
452
+ streaminfo_offset = io.pos
453
+ io.write("\x00" * STREAMINFO_LENGTH)
454
+ { streaminfo_offset: streaminfo_offset }
455
+ end
456
+
457
+ def finalize_stream_header(io, header, format, total_sample_frames, encode_stats)
458
+ file_end = io.pos
459
+ io.seek(header.fetch(:streaminfo_offset), IO::SEEK_SET)
460
+ io.write(
461
+ build_streaminfo_bytes(
462
+ format: format,
463
+ sample_frame_count: total_sample_frames,
464
+ stats: encode_stats,
465
+ md5_hex: header.fetch(:md5).hexdigest
466
+ )
467
+ )
468
+ io.seek(file_end, IO::SEEK_SET)
469
+ end
470
+
471
+ def empty_encode_stats
472
+ {
473
+ min_block_size: 0,
474
+ max_block_size: 0,
475
+ min_frame_size: 0,
476
+ max_frame_size: 0
477
+ }
478
+ end
479
+
480
+ def normalize_stream_write_options(block_size, block_size_strategy)
481
+ strategy = block_size_strategy.to_sym
482
+ supported = %i[per_chunk source_chunk fixed]
483
+ unless supported.include?(strategy)
484
+ raise InvalidParameterError, "unsupported FLAC stream_write block_size_strategy: #{block_size_strategy.inspect}"
485
+ end
486
+
487
+ {
488
+ strategy: strategy,
489
+ block_size: normalize_encode_block_size(block_size)
490
+ }
491
+ rescue NoMethodError
492
+ raise InvalidParameterError, "block_size_strategy must be Symbol/String: #{block_size_strategy.inspect}"
493
+ end
494
+
495
+ def merge_encode_stats!(aggregate, encoded)
496
+ current_min_block = encoded.fetch(:min_block_size)
497
+ current_max_block = encoded.fetch(:max_block_size)
498
+ current_min_frame = encoded.fetch(:min_frame_size)
499
+ current_max_frame = encoded.fetch(:max_frame_size)
500
+ return if current_max_block.zero?
501
+
502
+ aggregate[:min_block_size] =
503
+ aggregate[:min_block_size].zero? ? current_min_block : [aggregate[:min_block_size], current_min_block].min
504
+ aggregate[:max_block_size] = [aggregate[:max_block_size], current_max_block].max
505
+ aggregate[:min_frame_size] =
506
+ aggregate[:min_frame_size].zero? ? current_min_frame : [aggregate[:min_frame_size], current_min_frame].min
507
+ aggregate[:max_frame_size] = [aggregate[:max_frame_size], current_max_frame].max
508
+ end
509
+
510
+ def encode_verbatim_frames(interleaved_samples, format, start_frame_number:, block_size:)
511
+ channels = format.channels
512
+ samples_per_frame = channels * normalize_encode_block_size(block_size)
513
+ bytes = +""
514
+ frame_number = start_frame_number
515
+ block_sizes = []
516
+ frame_sizes = []
517
+
518
+ interleaved_samples.each_slice(samples_per_frame) do |frame_samples|
519
+ encoded_frame = encode_pcm_frame(frame_samples, format, frame_number: frame_number)
520
+ bytes << encoded_frame
521
+ block_sizes << (frame_samples.length / channels)
522
+ frame_sizes << encoded_frame.bytesize
523
+ frame_number += 1
524
+ end
525
+
526
+ {
527
+ bytes: bytes,
528
+ next_frame_number: frame_number,
529
+ min_block_size: block_sizes.min || 0,
530
+ max_block_size: block_sizes.max || 0,
531
+ min_frame_size: frame_sizes.min || 0,
532
+ max_frame_size: frame_sizes.max || 0
533
+ }
534
+ end
535
+
536
+ def encode_pcm_frame(interleaved_samples, format, frame_number:)
537
+ channels = format.channels
538
+ block_size = interleaved_samples.length / channels
539
+ raise InvalidParameterError, "FLAC frame block size must be positive" if block_size <= 0
540
+
541
+ block_size_code, block_size_extra_bits = encode_block_size_descriptor(block_size)
542
+ channel_samples = deinterleave_samples(interleaved_samples, channels)
543
+ header_without_crc8 = build_frame_header_bytes(
544
+ block_size: block_size,
545
+ block_size_code: block_size_code,
546
+ block_size_extra_bits: block_size_extra_bits,
547
+ channels: channels,
548
+ frame_number: frame_number
549
+ )
550
+ header_crc8 = flac_crc8(header_without_crc8)
551
+
552
+ payload_writer = BitWriter.new
553
+ channel_samples.each do |channel|
554
+ write_best_subframe(payload_writer, channel, format.bit_depth)
555
+ end
556
+ payload_writer.align_to_byte
557
+ payload_bytes = payload_writer.to_s
558
+
559
+ crc16_input = header_without_crc8 + [header_crc8].pack("C") + payload_bytes
560
+ crc16 = flac_crc16(crc16_input)
561
+
562
+ crc16_input + [crc16].pack("n")
563
+ end
564
+
565
+ def write_best_subframe(writer, channel_samples, sample_size)
566
+ selection = select_subframe_encoding(channel_samples, sample_size)
567
+
568
+ if selection[:kind] == :fixed
569
+ write_fixed_subframe(
570
+ writer,
571
+ channel_samples,
572
+ sample_size,
573
+ selection: selection
574
+ )
575
+ return
576
+ end
577
+
578
+ write_verbatim_subframe(writer, channel_samples, sample_size)
579
+ end
580
+
581
+ def select_subframe_encoding(channel_samples, sample_size)
582
+ best = {
583
+ kind: :verbatim,
584
+ bit_length: verbatim_subframe_bit_length(channel_samples.length, sample_size)
585
+ }
586
+
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)
592
+
593
+ best = candidate
594
+ end
595
+
596
+ best
597
+ end
598
+
599
+ def verbatim_subframe_bit_length(sample_count, sample_size)
600
+ 8 + (sample_count * sample_size)
601
+ end
602
+
603
+ def build_fixed_subframe_encoding(channel_samples, sample_size, predictor_order)
604
+ residuals = fixed_subframe_residuals(channel_samples, predictor_order)
605
+ residual_encoding = choose_residual_encoding(residuals)
606
+ return nil unless residual_encoding
607
+
608
+ {
609
+ kind: :fixed,
610
+ predictor_order: predictor_order,
611
+ residuals: residuals,
612
+ residual_encoding: residual_encoding,
613
+ bit_length: 8 + (predictor_order * sample_size) + residual_encoding.fetch(:bit_length)
614
+ }
615
+ end
616
+
617
+ def fixed_subframe_residuals(samples, predictor_order)
618
+ history = samples.first(predictor_order).dup
619
+
620
+ samples.drop(predictor_order).each_with_object([]) do |sample, residuals|
621
+ predicted = fixed_predictor_value(history, predictor_order)
622
+ residuals << (sample - predicted)
623
+ history << sample
624
+ history.shift if predictor_order.positive? && history.length > predictor_order
625
+ end
626
+ end
627
+
628
+ def choose_residual_encoding(residuals)
629
+ rice_candidates = (0..14).map do |parameter|
630
+ {
631
+ kind: :rice,
632
+ parameter: parameter,
633
+ bit_length: rice_partition0_bit_length(residuals, parameter)
634
+ }
635
+ end
636
+
637
+ escape_candidate = escape_residual_encoding(residuals)
638
+ candidates = rice_candidates
639
+ candidates << escape_candidate if escape_candidate
640
+ candidates.min_by { |candidate| candidate.fetch(:bit_length) }
641
+ end
642
+
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
648
+ end
649
+ end
650
+
651
+ def escape_residual_encoding(residuals)
652
+ raw_bits = residuals.map { |residual| signed_bit_width(residual) }.max.to_i
653
+ return nil if raw_bits > 31
654
+
655
+ {
656
+ kind: :escape,
657
+ raw_bits: raw_bits,
658
+ bit_length: 6 + 4 + 5 + (residuals.length * raw_bits)
659
+ }
660
+ end
661
+
662
+ def signed_bit_width(value)
663
+ return 0 if value.zero?
664
+
665
+ bits = 1
666
+ bits += 1 until value.between?(-(1 << (bits - 1)), (1 << (bits - 1)) - 1)
667
+ bits
668
+ end
669
+
670
+ def write_fixed_subframe(writer, channel_samples, sample_size, selection:)
671
+ predictor_order = selection.fetch(:predictor_order)
672
+ residual_encoding = selection.fetch(:residual_encoding)
673
+ residuals = selection.fetch(:residuals)
674
+
675
+ writer.write_bits(0, 1) # padding bit
676
+ writer.write_bits(8 + predictor_order, 6)
677
+ writer.write_bits(0, 1) # no wasted bits
678
+
679
+ channel_samples.first(predictor_order).each { |sample| writer.write_signed_bits(sample, sample_size) }
680
+ write_partition0_residuals(writer, residual_encoding, residuals)
681
+ end
682
+
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
+
692
+ 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?
698
+ end
699
+
700
+ def build_frame_header_bytes(block_size:, block_size_code:, block_size_extra_bits:, channels:, frame_number:)
701
+ writer = BitWriter.new
702
+ writer.write_bits(FLAC_SYNC_CODE, 14)
703
+ writer.write_bits(0, 1) # reserved
704
+ writer.write_bits(0, 1) # fixed-blocksize stream
705
+ writer.write_bits(block_size_code, 4)
706
+ writer.write_bits(0, 4) # sample rate from STREAMINFO
707
+ writer.write_bits(channels - 1, 4) # independent channels
708
+ writer.write_bits(0, 3) # sample size from STREAMINFO
709
+ writer.write_bits(0, 1) # reserved
710
+ write_utf8_uint(writer, frame_number)
711
+ writer.write_bits(block_size - 1, block_size_extra_bits) if block_size_extra_bits.positive?
712
+ writer.align_to_byte
713
+ writer.to_s
714
+ end
715
+
716
+ def write_verbatim_subframe(writer, channel_samples, sample_size)
717
+ writer.write_bits(0, 1) # padding bit
718
+ writer.write_bits(1, 6) # verbatim subframe type
719
+ writer.write_bits(0, 1) # no wasted bits
720
+ channel_samples.each { |sample| writer.write_signed_bits(sample, sample_size) }
721
+ end
722
+
723
+ def deinterleave_samples(interleaved_samples, channels)
724
+ channel_samples = Array.new(channels) { [] }
725
+
726
+ interleaved_samples.each_slice(channels) do |frame|
727
+ channels.times do |channel_index|
728
+ channel_samples[channel_index] << frame.fetch(channel_index)
729
+ end
730
+ end
731
+
732
+ channel_samples
733
+ end
734
+
735
+ def encode_block_size_descriptor(block_size)
736
+ raise UnsupportedFormatError, "FLAC block size exceeds 65536 samples" if block_size > 65_536
737
+
738
+ if block_size <= 256
739
+ [6, 8]
740
+ else
741
+ [7, 16]
742
+ end
743
+ end
744
+
745
+ def write_utf8_uint(writer, value)
746
+ raise InvalidParameterError, "FLAC frame number must be non-negative Integer" unless value.is_a?(Integer) && value >= 0
747
+
748
+ if value <= 0x7F
749
+ writer.write_bits(value, 8)
750
+ return
751
+ end
752
+
753
+ payload_bits = Math.log2(value + 1).floor + 1
754
+ length = 2
755
+ length += 1 while payload_capacity_for_utf8_uint(length) < payload_bits
756
+ raise UnsupportedFormatError, "FLAC frame number is too large to encode" if length > 7
757
+
758
+ bytes = Array.new(length, 0)
759
+ remaining = value
760
+ (length - 1).downto(1) do |index|
761
+ bytes[index] = 0x80 | (remaining & 0x3F)
762
+ remaining >>= 6
763
+ end
764
+
765
+ prefix = ((1 << length) - 1) << (8 - length)
766
+ bytes[0] = prefix | remaining
767
+
768
+ bytes.each { |byte| writer.write_bits(byte, 8) }
769
+ end
770
+
771
+ def payload_capacity_for_utf8_uint(length)
772
+ (7 - length) + (6 * (length - 1))
773
+ end
774
+
775
+ def normalize_encode_block_size(block_size)
776
+ size = block_size.to_i
777
+ raise InvalidParameterError, "FLAC block_size must be a positive Integer" unless size.positive?
778
+
779
+ [size, 65_536].min
780
+ end
781
+
782
+ def build_streaminfo_bytes(format:, sample_frame_count:, stats:, md5_hex:)
783
+ raise UnsupportedFormatError, "FLAC total sample count exceeds 36-bit STREAMINFO limit" if sample_frame_count > 0xFFFFFFFFF
784
+
785
+ min_block_size = stats.fetch(:min_block_size)
786
+ max_block_size = stats.fetch(:max_block_size)
787
+ min_frame_size = stats.fetch(:min_frame_size)
788
+ max_frame_size = stats.fetch(:max_frame_size)
789
+ md5_bytes = [md5_hex].pack("H*")
790
+ raise InvalidParameterError, "md5_hex must be 32 hex characters" unless md5_bytes.bytesize == 16
791
+
792
+ packed = ((format.sample_rate & 0xFFFFF) << 44) |
793
+ (((format.channels - 1) & 0x7) << 41) |
794
+ (((format.bit_depth - 1) & 0x1F) << 36) |
795
+ (sample_frame_count & 0xFFFFFFFFF)
796
+
797
+ [min_block_size, max_block_size].pack("n2") +
798
+ pack_uint24(min_frame_size) +
799
+ pack_uint24(max_frame_size) +
800
+ [packed].pack("Q>") +
801
+ md5_bytes
802
+ end
803
+
804
+ def pcm_md5_hex(samples, format)
805
+ Digest::MD5.hexdigest(pcm_bytes_for_md5(samples, format))
806
+ end
807
+
808
+ 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}"
820
+ end
821
+ end
822
+
823
+ def encode_pcm24_le(samples)
824
+ bytes = samples.flat_map do |sample|
825
+ value = sample
826
+ value += 0x1000000 if value.negative?
827
+ [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF]
828
+ end
829
+ bytes.pack("C*")
830
+ end
831
+
832
+ def flac_crc8(data)
833
+ crc = 0
834
+ data.each_byte do |byte|
835
+ crc ^= byte
836
+ 8.times do
837
+ crc = crc.anybits?(0x80) ? ((crc << 1) ^ 0x07) : (crc << 1)
838
+ crc &= 0xFF
839
+ end
840
+ end
841
+ crc
842
+ end
843
+
844
+ def flac_crc16(data)
845
+ crc = 0
846
+ data.each_byte do |byte|
847
+ crc ^= (byte << 8)
848
+ 8.times do
849
+ crc = crc.anybits?(0x8000) ? ((crc << 1) ^ 0x8005) : (crc << 1)
850
+ crc &= 0xFFFF
851
+ end
852
+ end
853
+ crc
854
+ end
855
+
856
+ def pack_uint24(value)
857
+ raise InvalidParameterError, "value must fit in uint24" unless value.is_a?(Integer) && value.between?(0, 0xFFFFFF)
858
+
859
+ [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF].pack("C3")
860
+ end
861
+
862
+ def validate_encode_format!(format)
863
+ raise InvalidParameterError, "format must be Core::Format" unless format.is_a?(Core::Format)
864
+ raise UnsupportedFormatError, "FLAC encoding only supports PCM sample format" unless format.sample_format == :pcm
865
+ raise UnsupportedFormatError, "FLAC encoding supports 1..8 channels" unless format.channels.between?(1, 8)
866
+ raise UnsupportedFormatError, "FLAC encoding supports up to 32-bit PCM" if format.bit_depth > 32
867
+
868
+ format
869
+ end
870
+
871
+ def decode_frames(io, metadata)
872
+ decoded_samples = []
873
+ each_decoded_frame_samples(io, metadata) { |frame_samples| decoded_samples.concat(frame_samples) }
874
+ decoded_samples
875
+ end
876
+
877
+ def each_decoded_frame_samples(io, metadata)
878
+ return enum_for(__method__, io, metadata) unless block_given?
879
+
880
+ format = metadata.fetch(:format)
881
+ remaining_frames = metadata[:sample_frame_count]
882
+ bounded_total = remaining_frames.is_a?(Integer) && remaining_frames.positive?
883
+
884
+ until io.eof?
885
+ break if bounded_total && remaining_frames <= 0
886
+
887
+ next_byte = io.read(1)
888
+ break if next_byte.nil?
889
+
890
+ io.seek(-1, IO::SEEK_CUR)
891
+ frame_samples = decode_frame(io, metadata)
892
+
893
+ if bounded_total
894
+ max_samples = remaining_frames * format.channels
895
+ frame_samples = frame_samples.first(max_samples) if frame_samples.length > max_samples
896
+
897
+ decoded_frame_count = frame_samples.length / format.channels
898
+ remaining_frames -= decoded_frame_count
899
+ end
900
+
901
+ yield frame_samples unless frame_samples.empty?
902
+ end
903
+
904
+ return unless bounded_total && remaining_frames.positive?
905
+
906
+ raise InvalidFormatError, "decoded FLAC samples are shorter than STREAMINFO total sample count"
907
+ end
908
+
909
+ def decode_frame(io, metadata)
910
+ bit_reader = BitReader.new(io)
911
+ frame_header = parse_frame_header(bit_reader, metadata)
912
+ channel_samples = decode_subframes(bit_reader, frame_header)
913
+ channel_samples = restore_channel_assignment(channel_samples, frame_header)
914
+ bit_reader.align_to_byte
915
+ _crc16 = bit_reader.read_bits(16)
916
+
917
+ interleave_channels(channel_samples, frame_header.fetch(:block_size), frame_header.fetch(:channels))
918
+ end
919
+
920
+ def parse_frame_header(bit_reader, metadata)
921
+ sync = bit_reader.read_bits(14)
922
+ raise InvalidFormatError, "invalid FLAC frame sync code" unless sync == FLAC_SYNC_CODE
923
+
924
+ reserved = bit_reader.read_bits(1)
925
+ raise InvalidFormatError, "reserved FLAC frame header bit must be 0" unless reserved.zero?
926
+
927
+ blocking_strategy = bit_reader.read_bits(1)
928
+ raise UnsupportedFormatError, "FLAC variable-blocksize frames are not supported yet" unless blocking_strategy.zero?
929
+
930
+ block_size_code = bit_reader.read_bits(4)
931
+ sample_rate_code = bit_reader.read_bits(4)
932
+ channel_assignment = bit_reader.read_bits(4)
933
+ sample_size_code = bit_reader.read_bits(3)
934
+ reserved2 = bit_reader.read_bits(1)
935
+ raise InvalidFormatError, "reserved FLAC frame header bit must be 0" unless reserved2.zero?
936
+
937
+ _frame_number = read_utf8_uint(bit_reader)
938
+ block_size = decode_block_size(block_size_code, bit_reader)
939
+ 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)
942
+
943
+ channels = decode_channel_count(channel_assignment, metadata.fetch(:format).channels)
944
+
945
+ {
946
+ block_size: block_size,
947
+ sample_rate: sample_rate,
948
+ sample_size: sample_size,
949
+ channels: channels,
950
+ channel_assignment: channel_assignment
951
+ }
952
+ end
953
+
954
+ def read_utf8_uint(bit_reader)
955
+ first = bit_reader.read_bits(8)
956
+ return first if first.nobits?(0x80)
957
+
958
+ mask = 0x80
959
+ length = 0
960
+ while first.anybits?(mask)
961
+ length += 1
962
+ mask >>= 1
963
+ end
964
+ raise InvalidFormatError, "invalid UTF-8 integer in FLAC frame header" if length < 2 || length > 7
965
+
966
+ value_mask = (1 << (7 - length)) - 1
967
+ value = first & value_mask
968
+ (length - 1).times do
969
+ byte = bit_reader.read_bits(8)
970
+ raise InvalidFormatError, "invalid UTF-8 continuation byte in FLAC frame header" unless (byte & 0xC0) == 0x80
971
+
972
+ value = (value << 6) | (byte & 0x3F)
973
+ end
974
+ value
975
+ end
976
+
977
+ def decode_block_size(code, bit_reader)
978
+ case code
979
+ when 0
980
+ raise InvalidFormatError, "reserved FLAC block size code"
981
+ when 6
982
+ bit_reader.read_bits(8) + 1
983
+ when 7
984
+ bit_reader.read_bits(16) + 1
985
+ else
986
+ BLOCK_SIZE_CODES.fetch(code)
987
+ end
988
+ rescue KeyError
989
+ raise UnsupportedFormatError, "unsupported FLAC block size code: #{code}"
990
+ end
991
+
992
+ def decode_sample_rate(code, bit_reader, stream_sample_rate)
993
+ case code
994
+ when 0
995
+ stream_sample_rate
996
+ when 12
997
+ bit_reader.read_bits(8) * 1000
998
+ when 13
999
+ bit_reader.read_bits(16)
1000
+ when 14
1001
+ bit_reader.read_bits(16) * 10
1002
+ else
1003
+ SAMPLE_RATE_CODES.fetch(code)
1004
+ end
1005
+ rescue KeyError
1006
+ raise UnsupportedFormatError, "unsupported FLAC sample rate code: #{code}"
1007
+ end
1008
+
1009
+ def decode_sample_size(code, stream_bit_depth)
1010
+ return stream_bit_depth if code.zero?
1011
+
1012
+ SAMPLE_SIZE_CODES.fetch(code)
1013
+ rescue KeyError
1014
+ raise UnsupportedFormatError, "unsupported FLAC sample size code: #{code}"
1015
+ end
1016
+
1017
+ def decode_channel_count(channel_assignment, stream_channels)
1018
+ case channel_assignment
1019
+ when 0..7
1020
+ channels = channel_assignment + 1
1021
+ raise InvalidFormatError, "FLAC frame channel count does not match STREAMINFO" if channels != stream_channels
1022
+
1023
+ channels
1024
+ when 8..10
1025
+ raise InvalidFormatError, "FLAC side/mid channel assignments require stereo STREAMINFO" unless stream_channels == 2
1026
+
1027
+ 2
1028
+ else
1029
+ raise InvalidFormatError, "reserved FLAC channel assignment: #{channel_assignment}"
1030
+ end
1031
+ end
1032
+
1033
+ def decode_subframes(bit_reader, frame_header)
1034
+ channels = frame_header.fetch(:channels)
1035
+ block_size = frame_header.fetch(:block_size)
1036
+ sample_sizes = subframe_sample_sizes(frame_header)
1037
+
1038
+ Array.new(channels) do |channel_index|
1039
+ decode_subframe(bit_reader, block_size: block_size, sample_size: sample_sizes.fetch(channel_index))
1040
+ end
1041
+ end
1042
+
1043
+ def subframe_sample_sizes(frame_header)
1044
+ sample_size = frame_header.fetch(:sample_size)
1045
+
1046
+ case frame_header.fetch(:channel_assignment)
1047
+ when 8, 10
1048
+ [sample_size, sample_size + 1]
1049
+ when 9
1050
+ [sample_size + 1, sample_size]
1051
+ else
1052
+ Array.new(frame_header.fetch(:channels), sample_size)
1053
+ end
1054
+ end
1055
+
1056
+ def decode_subframe(bit_reader, block_size:, sample_size:)
1057
+ padding = bit_reader.read_bits(1)
1058
+ raise InvalidFormatError, "FLAC subframe padding bit must be 0" unless padding.zero?
1059
+
1060
+ subframe_type = bit_reader.read_bits(6)
1061
+ wasted_bits_flag = bit_reader.read_bits(1)
1062
+ wasted_bits = wasted_bits_flag.zero? ? 0 : (read_unary_zero_run(bit_reader) + 1)
1063
+ effective_sample_size = sample_size - wasted_bits
1064
+ raise InvalidFormatError, "invalid FLAC wasted bits count" unless effective_sample_size.positive?
1065
+
1066
+ decoded = case subframe_type
1067
+ when 0
1068
+ value = bit_reader.read_signed_bits(effective_sample_size)
1069
+ Array.new(block_size, value)
1070
+ when 1
1071
+ Array.new(block_size) { bit_reader.read_signed_bits(effective_sample_size) }
1072
+ when 8..12
1073
+ predictor_order = subframe_type - 8
1074
+ decode_fixed_subframe(
1075
+ bit_reader,
1076
+ block_size: block_size,
1077
+ sample_size: effective_sample_size,
1078
+ predictor_order: predictor_order
1079
+ )
1080
+ when 32..63
1081
+ predictor_order = (subframe_type & 0x1F) + 1
1082
+ decode_lpc_subframe(
1083
+ bit_reader,
1084
+ block_size: block_size,
1085
+ sample_size: effective_sample_size,
1086
+ predictor_order: predictor_order
1087
+ )
1088
+ else
1089
+ raise UnsupportedFormatError, "unsupported FLAC subframe type: #{subframe_type}"
1090
+ end
1091
+
1092
+ return decoded if wasted_bits.zero?
1093
+
1094
+ decoded.map { |sample| sample << wasted_bits }
1095
+ end
1096
+
1097
+ def decode_fixed_subframe(bit_reader, block_size:, sample_size:, predictor_order:)
1098
+ raise InvalidFormatError, "FLAC fixed predictor order exceeds block size" if predictor_order > block_size
1099
+
1100
+ warmup = Array.new(predictor_order) { bit_reader.read_signed_bits(sample_size) }
1101
+ residuals = decode_residuals(
1102
+ bit_reader,
1103
+ block_size: block_size,
1104
+ predictor_order: predictor_order
1105
+ )
1106
+
1107
+ reconstruct_fixed_subframe(warmup, residuals, predictor_order)
1108
+ end
1109
+
1110
+ def decode_lpc_subframe(bit_reader, block_size:, sample_size:, predictor_order:)
1111
+ raise InvalidFormatError, "FLAC LPC predictor order exceeds block size" if predictor_order > block_size
1112
+
1113
+ warmup = Array.new(predictor_order) { bit_reader.read_signed_bits(sample_size) }
1114
+
1115
+ precision_minus_one = bit_reader.read_bits(4)
1116
+ raise InvalidFormatError, "invalid FLAC LPC coefficient precision" if precision_minus_one == 0xF
1117
+
1118
+ coefficient_precision = precision_minus_one + 1
1119
+ qlp_shift = bit_reader.read_signed_bits(5)
1120
+ coefficients = Array.new(predictor_order) { bit_reader.read_signed_bits(coefficient_precision) }
1121
+ residuals = decode_residuals(
1122
+ bit_reader,
1123
+ block_size: block_size,
1124
+ predictor_order: predictor_order
1125
+ )
1126
+
1127
+ reconstruct_lpc_subframe(warmup, residuals, coefficients, qlp_shift)
1128
+ end
1129
+
1130
+ def decode_residuals(bit_reader, block_size:, predictor_order:)
1131
+ coding_method = bit_reader.read_bits(2)
1132
+ partition_order = bit_reader.read_bits(4)
1133
+ partition_count = 1 << partition_order
1134
+ raise InvalidFormatError, "invalid FLAC residual partitioning" if partition_count.zero?
1135
+ raise InvalidFormatError, "FLAC block size must be divisible by residual partitions" unless (block_size % partition_count).zero?
1136
+
1137
+ partition_block_size = block_size / partition_count
1138
+ case coding_method
1139
+ when 0
1140
+ decode_rice_partitions(
1141
+ bit_reader,
1142
+ partition_count: partition_count,
1143
+ partition_block_size: partition_block_size,
1144
+ predictor_order: predictor_order,
1145
+ coding: { parameter_bits: 4, escape_parameter: 0xF }
1146
+ )
1147
+ when 1
1148
+ decode_rice_partitions(
1149
+ bit_reader,
1150
+ partition_count: partition_count,
1151
+ partition_block_size: partition_block_size,
1152
+ predictor_order: predictor_order,
1153
+ coding: { parameter_bits: 5, escape_parameter: 0x1F }
1154
+ )
1155
+ else
1156
+ raise UnsupportedFormatError, "unsupported FLAC residual coding method: #{coding_method}"
1157
+ end
1158
+ end
1159
+
1160
+ def decode_rice_partitions(bit_reader, partition_count:, partition_block_size:, predictor_order:, coding:)
1161
+ residuals = []
1162
+ parameter_bits = coding.fetch(:parameter_bits)
1163
+ escape_parameter = coding.fetch(:escape_parameter)
1164
+
1165
+ partition_count.times do |partition_index|
1166
+ sample_count = partition_block_size
1167
+ sample_count -= predictor_order if partition_index.zero?
1168
+ raise InvalidFormatError, "invalid FLAC residual partition sample count" if sample_count.negative?
1169
+
1170
+ parameter = bit_reader.read_bits(parameter_bits)
1171
+ if parameter == escape_parameter
1172
+ raw_bits = bit_reader.read_bits(5)
1173
+ sample_count.times { residuals << (raw_bits.zero? ? 0 : bit_reader.read_signed_bits(raw_bits)) }
1174
+ else
1175
+ sample_count.times { residuals << read_rice_signed(bit_reader, parameter) }
1176
+ end
1177
+ end
1178
+
1179
+ residuals
1180
+ end
1181
+
1182
+ def read_rice_signed(bit_reader, parameter)
1183
+ quotient = read_unary_zero_run(bit_reader)
1184
+
1185
+ remainder = parameter.zero? ? 0 : bit_reader.read_bits(parameter)
1186
+ unsigned = (quotient << parameter) | remainder
1187
+ unsigned.even? ? (unsigned >> 1) : -((unsigned + 1) >> 1)
1188
+ end
1189
+
1190
+ def read_unary_zero_run(bit_reader)
1191
+ count = 0
1192
+ count += 1 while bit_reader.read_bits(1).zero?
1193
+ count
1194
+ end
1195
+
1196
+ def reconstruct_fixed_subframe(warmup, residuals, predictor_order)
1197
+ samples = warmup.dup
1198
+ residuals.each do |residual|
1199
+ predicted = fixed_predictor_value(samples, predictor_order)
1200
+ samples << (predicted + residual)
1201
+ end
1202
+ samples
1203
+ end
1204
+
1205
+ def reconstruct_lpc_subframe(warmup, residuals, coefficients, qlp_shift)
1206
+ samples = warmup.dup
1207
+
1208
+ residuals.each do |residual|
1209
+ sum = 0
1210
+ coefficients.each_with_index do |coefficient, index|
1211
+ sum += coefficient * samples[-1 - index]
1212
+ end
1213
+
1214
+ predicted = qlp_shift.negative? ? (sum << -qlp_shift) : (sum >> qlp_shift)
1215
+ samples << (predicted + residual)
1216
+ end
1217
+
1218
+ samples
1219
+ end
1220
+
1221
+ def fixed_predictor_value(samples, predictor_order)
1222
+ case predictor_order
1223
+ when 0
1224
+ 0
1225
+ when 1
1226
+ samples[-1]
1227
+ when 2
1228
+ (2 * samples[-1]) - samples[-2]
1229
+ when 3
1230
+ (3 * samples[-1]) - (3 * samples[-2]) + samples[-3]
1231
+ when 4
1232
+ (4 * samples[-1]) - (6 * samples[-2]) + (4 * samples[-3]) - samples[-4]
1233
+ else
1234
+ raise UnsupportedFormatError, "unsupported FLAC fixed predictor order: #{predictor_order}"
1235
+ end
1236
+ end
1237
+
1238
+ def interleave_channels(channel_samples, block_size, channels)
1239
+ samples = Array.new(block_size * channels)
1240
+
1241
+ block_size.times do |frame_index|
1242
+ channels.times do |channel_index|
1243
+ samples[(frame_index * channels) + channel_index] = channel_samples[channel_index][frame_index]
1244
+ end
1245
+ end
1246
+
1247
+ samples
1248
+ end
1249
+
1250
+ def restore_channel_assignment(channel_samples, frame_header)
1251
+ assignment = frame_header.fetch(:channel_assignment)
1252
+ return channel_samples if assignment <= 7
1253
+
1254
+ left_or_side = channel_samples.fetch(0)
1255
+ right_or_side = channel_samples.fetch(1)
1256
+
1257
+ case assignment
1258
+ when 8 # left + side
1259
+ left = left_or_side
1260
+ side = right_or_side
1261
+ right = left.zip(side).map { |l, s| l - s }
1262
+ [left, right]
1263
+ when 9 # side + right
1264
+ side = left_or_side
1265
+ right = right_or_side
1266
+ left = side.zip(right).map { |s, r| s + r }
1267
+ [left, right]
1268
+ when 10 # mid + side
1269
+ mid = left_or_side
1270
+ side = right_or_side
1271
+ left = []
1272
+ right = []
1273
+
1274
+ mid.each_with_index do |mid_sample, index|
1275
+ side_sample = side.fetch(index)
1276
+ adjusted_mid = (mid_sample << 1) | (side_sample & 0x1)
1277
+ left << ((adjusted_mid + side_sample) >> 1)
1278
+ right << ((adjusted_mid - side_sample) >> 1)
1279
+ end
1280
+
1281
+ [left, right]
1282
+ else
1283
+ raise InvalidFormatError, "unsupported FLAC channel assignment: #{assignment}"
1284
+ end
1285
+ end
1286
+
1287
+ def unpack_uint24(bytes)
1288
+ bytes.unpack("C3").then { |b0, b1, b2| (b0 << 16) | (b1 << 8) | b2 }
1289
+ end
1290
+
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
+ end
1320
+ end
1321
+ end
1322
+ end