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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -1
- data/README.md +204 -22
- data/exe/wavify +6 -0
- data/lib/wavify/adapters.rb +81 -0
- data/lib/wavify/audio.rb +1066 -120
- data/lib/wavify/cli.rb +237 -0
- data/lib/wavify/codecs/aiff.rb +388 -87
- data/lib/wavify/codecs/base.rb +98 -5
- data/lib/wavify/codecs/flac.rb +660 -170
- data/lib/wavify/codecs/ogg_vorbis.rb +515 -155
- data/lib/wavify/codecs/raw.rb +235 -45
- data/lib/wavify/codecs/registry.rb +272 -10
- data/lib/wavify/codecs/wav.rb +453 -74
- data/lib/wavify/core/duration.rb +54 -2
- data/lib/wavify/core/format.rb +96 -11
- data/lib/wavify/core/sample_buffer.rb +648 -60
- data/lib/wavify/core/stream.rb +590 -34
- data/lib/wavify/dsl.rb +712 -100
- data/lib/wavify/dsp/automation.rb +109 -0
- data/lib/wavify/dsp/effects/auto_pan.rb +71 -0
- data/lib/wavify/dsp/effects/bitcrusher.rb +74 -0
- data/lib/wavify/dsp/effects/chorus.rb +15 -4
- data/lib/wavify/dsp/effects/compressor.rb +133 -23
- data/lib/wavify/dsp/effects/delay.rb +46 -1
- data/lib/wavify/dsp/effects/distortion.rb +3 -2
- data/lib/wavify/dsp/effects/effect_base.rb +69 -2
- data/lib/wavify/dsp/effects/effect_chain.rb +94 -0
- data/lib/wavify/dsp/effects/envelope_controlled_effect.rb +93 -0
- data/lib/wavify/dsp/effects/eq.rb +102 -0
- data/lib/wavify/dsp/effects/expander.rb +90 -0
- data/lib/wavify/dsp/effects/flanger.rb +112 -0
- data/lib/wavify/dsp/effects/limiter.rb +259 -0
- data/lib/wavify/dsp/effects/mastering_chain.rb +31 -0
- data/lib/wavify/dsp/effects/noise_gate.rb +72 -0
- data/lib/wavify/dsp/effects/phaser.rb +112 -0
- data/lib/wavify/dsp/effects/podcast_chain.rb +32 -0
- data/lib/wavify/dsp/effects/reverb.rb +100 -14
- data/lib/wavify/dsp/effects/soft_limiter.rb +55 -0
- data/lib/wavify/dsp/effects/stereo_widener.rb +73 -0
- data/lib/wavify/dsp/effects/tremolo.rb +66 -0
- data/lib/wavify/dsp/effects/vibrato.rb +102 -0
- data/lib/wavify/dsp/effects.rb +106 -0
- data/lib/wavify/dsp/envelope.rb +90 -19
- data/lib/wavify/dsp/filter.rb +149 -8
- data/lib/wavify/dsp/headroom.rb +57 -0
- data/lib/wavify/dsp/lfo.rb +65 -0
- data/lib/wavify/dsp/loudness_meter.rb +281 -0
- data/lib/wavify/dsp/oscillator.rb +261 -22
- data/lib/wavify/dsp/processor.rb +90 -0
- data/lib/wavify/errors.rb +2 -2
- data/lib/wavify/sequencer/engine.rb +405 -89
- data/lib/wavify/sequencer/note_sequence.rb +45 -7
- data/lib/wavify/sequencer/pattern.rb +121 -12
- data/lib/wavify/sequencer/track.rb +149 -10
- data/lib/wavify/version.rb +1 -1
- data/lib/wavify.rb +18 -0
- data/sig/audio.rbs +86 -0
- data/sig/codecs.rbs +77 -0
- data/sig/core.rbs +108 -0
- data/sig/dsl.rbs +32 -0
- data/sig/dsp.rbs +87 -0
- data/sig/effects.rbs +129 -0
- data/sig/public_api.yml +111 -0
- data/sig/sequencer.rbs +126 -0
- data/sig/stream.rbs +30 -0
- data/sig/wavify.rbs +24 -0
- metadata +44 -58
- data/.serena/.gitignore +0 -1
- data/.serena/memories/project_overview.md +0 -5
- data/.serena/memories/style_and_completion.md +0 -5
- data/.serena/memories/suggested_commands.md +0 -11
- data/.serena/project.yml +0 -126
- data/.simplecov +0 -18
- data/.yardopts +0 -4
- data/Rakefile +0 -190
- data/benchmarks/README.md +0 -46
- data/benchmarks/benchmark_helper.rb +0 -112
- data/benchmarks/dsp_effects_benchmark.rb +0 -46
- data/benchmarks/flac_benchmark.rb +0 -74
- data/benchmarks/streaming_memory_benchmark.rb +0 -94
- data/benchmarks/wav_io_benchmark.rb +0 -110
- data/examples/audio_processing.rb +0 -73
- data/examples/cinematic_transition.rb +0 -118
- data/examples/drum_machine.rb +0 -74
- data/examples/format_convert.rb +0 -81
- data/examples/hybrid_arrangement.rb +0 -165
- data/examples/streaming_master_chain.rb +0 -129
- data/examples/synth_pad.rb +0 -42
- data/tools/fixture_writer.rb +0 -85
data/lib/wavify/codecs/flac.rb
CHANGED
|
@@ -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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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 <<
|
|
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
|
|
451
|
-
io
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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.
|
|
554
|
-
write_best_subframe(payload_writer, channel,
|
|
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
|
|
566
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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: :
|
|
632
|
-
|
|
633
|
-
|
|
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
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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:
|
|
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
|
-
|
|
1139
|
+
write_residuals(writer, residual_encoding)
|
|
681
1140
|
end
|
|
682
1141
|
|
|
683
|
-
def
|
|
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(
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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:,
|
|
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(
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(:
|
|
941
|
-
|
|
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
|
-
|
|
1193
|
-
|
|
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
|