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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
|
-
require "
|
|
4
|
+
require "tempfile"
|
|
5
5
|
|
|
6
6
|
module Wavify
|
|
7
7
|
module Codecs
|
|
@@ -15,6 +15,22 @@ module Wavify
|
|
|
15
15
|
# resampled to the first logical stream sample rate when rates differ).
|
|
16
16
|
# Interleaved multi-stream OGG logical streams are mixed with clipping.
|
|
17
17
|
class OggVorbis < Base
|
|
18
|
+
DEPENDENCY_LOAD_ERRORS = begin
|
|
19
|
+
errors = {}
|
|
20
|
+
begin
|
|
21
|
+
require "ogg"
|
|
22
|
+
rescue LoadError => e
|
|
23
|
+
errors["ogg-ruby"] = e
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
require "vorbis"
|
|
28
|
+
rescue LoadError => e
|
|
29
|
+
errors["vorbis"] = e
|
|
30
|
+
end
|
|
31
|
+
errors.freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
18
34
|
# Recognized filename extensions.
|
|
19
35
|
EXTENSIONS = %w[.ogg .oga].freeze
|
|
20
36
|
|
|
@@ -24,17 +40,44 @@ module Wavify
|
|
|
24
40
|
SETUP_HEADER_TYPE = 0x05 # :nodoc:
|
|
25
41
|
GRANULE_POSITION_UNKNOWN = 0xFFFF_FFFF_FFFF_FFFF # :nodoc:
|
|
26
42
|
VORBIS_ENCODE_DEFAULT_QUALITY = 0.4 # :nodoc:
|
|
43
|
+
VORBIS_ENCODE_QUALITY_RANGE = (-0.1..1.0) # :nodoc:
|
|
27
44
|
|
|
28
45
|
class << self
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def available?
|
|
48
|
+
DEPENDENCY_LOAD_ERRORS.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def runtime_diagnostics
|
|
52
|
+
return { available: false, compatible: false, missing: DEPENDENCY_LOAD_ERRORS.keys.freeze } unless available?
|
|
53
|
+
|
|
54
|
+
required_native_methods = %i[
|
|
55
|
+
vorbis_info_init vorbis_info_clear vorbis_comment_init vorbis_comment_clear
|
|
56
|
+
vorbis_synthesis_headerin vorbis_synthesis_init vorbis_block_init
|
|
57
|
+
vorbis_synthesis vorbis_synthesis_blockin vorbis_synthesis_pcmout vorbis_synthesis_read
|
|
58
|
+
]
|
|
59
|
+
missing_methods = required_native_methods.reject { |method| Vorbis::Native.respond_to?(method) }
|
|
60
|
+
{
|
|
61
|
+
available: true,
|
|
62
|
+
compatible: missing_methods.empty?,
|
|
63
|
+
missing_native_methods: missing_methods.freeze,
|
|
64
|
+
versions: {
|
|
65
|
+
"ogg-ruby" => Gem.loaded_specs["ogg-ruby"]&.version&.to_s,
|
|
66
|
+
"vorbis" => Gem.loaded_specs["vorbis"]&.version&.to_s
|
|
67
|
+
}.freeze
|
|
68
|
+
}.freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
29
71
|
# @param io_or_path [String, IO]
|
|
30
72
|
# @return [Boolean]
|
|
31
73
|
def can_read?(io_or_path)
|
|
32
74
|
return true if io_or_path.is_a?(String) && EXTENSIONS.include?(File.extname(io_or_path).downcase)
|
|
33
75
|
return false unless io_or_path.respond_to?(:read)
|
|
34
76
|
|
|
35
|
-
magic = io_or_path
|
|
36
|
-
io_or_path.rewind if io_or_path.respond_to?(:rewind)
|
|
77
|
+
magic = probe_bytes(io_or_path, 4)
|
|
37
78
|
magic == "OggS"
|
|
79
|
+
rescue StreamError
|
|
80
|
+
false
|
|
38
81
|
end
|
|
39
82
|
|
|
40
83
|
# Reads OGG Vorbis audio.
|
|
@@ -43,13 +86,15 @@ module Wavify
|
|
|
43
86
|
# OGG logical streams are concatenated and normalized to the first
|
|
44
87
|
# logical stream format (including resampling). Interleaved
|
|
45
88
|
# multi-stream OGG logical streams are mixed.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
def read(io_or_path, format: nil, mix_strategy: :clip)
|
|
90
|
+
ensure_available!
|
|
91
|
+
mix_strategy = validate_vorbis_mix_strategy!(mix_strategy)
|
|
92
|
+
|
|
93
|
+
if (chained_decoded = decode_chained_vorbis_read_if_needed(
|
|
94
|
+
io_or_path,
|
|
95
|
+
target_format: format,
|
|
96
|
+
mix_strategy: mix_strategy
|
|
97
|
+
))
|
|
53
98
|
return chained_decoded
|
|
54
99
|
end
|
|
55
100
|
|
|
@@ -61,10 +106,12 @@ module Wavify
|
|
|
61
106
|
end
|
|
62
107
|
|
|
63
108
|
# Writes OGG Vorbis audio.
|
|
64
|
-
def write(io_or_path, sample_buffer, format:)
|
|
109
|
+
def write(io_or_path, sample_buffer, format:, quality: VORBIS_ENCODE_DEFAULT_QUALITY, **codec_options)
|
|
110
|
+
ensure_available!
|
|
111
|
+
validate_no_codec_options!(codec_options, operation: "OGG Vorbis write")
|
|
65
112
|
raise InvalidParameterError, "sample_buffer must be Core::SampleBuffer" unless sample_buffer.is_a?(Core::SampleBuffer)
|
|
66
113
|
|
|
67
|
-
stream_write(io_or_path, format: format) do |writer|
|
|
114
|
+
stream_write(io_or_path, format: format, quality: quality) do |writer|
|
|
68
115
|
writer.call(sample_buffer)
|
|
69
116
|
end
|
|
70
117
|
end
|
|
@@ -75,14 +122,22 @@ module Wavify
|
|
|
75
122
|
# OGG logical streams are concatenated and normalized to the first
|
|
76
123
|
# logical stream format during streaming (including resampling).
|
|
77
124
|
# Interleaved multi-stream OGG logical streams are mixed.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
125
|
+
def stream_read(io_or_path, chunk_size: 4096, mix_strategy: :clip, &block)
|
|
126
|
+
unless block_given?
|
|
127
|
+
return enum_for(__method__, io_or_path, chunk_size: chunk_size, mix_strategy: mix_strategy)
|
|
128
|
+
end
|
|
129
|
+
ensure_available!
|
|
81
130
|
raise InvalidParameterError, "chunk_size must be a positive Integer" unless chunk_size.is_a?(Integer) && chunk_size.positive?
|
|
82
|
-
|
|
83
|
-
placeholder].include?(decode_mode)
|
|
131
|
+
mix_strategy = validate_vorbis_mix_strategy!(mix_strategy)
|
|
84
132
|
|
|
85
|
-
|
|
133
|
+
if multistream_ogg_prefix?(io_or_path)
|
|
134
|
+
return nil if stream_chained_vorbis_if_needed(
|
|
135
|
+
io_or_path,
|
|
136
|
+
chunk_size: chunk_size,
|
|
137
|
+
mix_strategy: mix_strategy,
|
|
138
|
+
&block
|
|
139
|
+
)
|
|
140
|
+
end
|
|
86
141
|
|
|
87
142
|
decode_context = build_vorbis_decode_context(io_or_path)
|
|
88
143
|
run_vorbis_decode_pipeline(decode_context, streaming: true, chunk_size: chunk_size, &block)
|
|
@@ -92,8 +147,13 @@ module Wavify
|
|
|
92
147
|
#
|
|
93
148
|
# @note Encodes using libvorbis at the default VBR quality level.
|
|
94
149
|
# Accepts any channel count and sample rate supported by libvorbis.
|
|
95
|
-
def stream_write(io_or_path, format:)
|
|
96
|
-
|
|
150
|
+
def stream_write(io_or_path, format:, quality: VORBIS_ENCODE_DEFAULT_QUALITY, **codec_options)
|
|
151
|
+
ensure_available!
|
|
152
|
+
validate_no_codec_options!(codec_options, operation: "OGG Vorbis stream_write")
|
|
153
|
+
encode_quality = normalize_vorbis_quality(quality)
|
|
154
|
+
unless block_given?
|
|
155
|
+
return enum_for(__method__, io_or_path, format: format, quality: quality, **codec_options)
|
|
156
|
+
end
|
|
97
157
|
raise InvalidParameterError, "format must be Core::Format" unless format.is_a?(Core::Format)
|
|
98
158
|
raise InvalidParameterError, "Vorbis encode requires positive channel count" unless format.channels.to_i.positive?
|
|
99
159
|
raise InvalidParameterError, "Vorbis encode requires positive sample_rate" unless format.sample_rate.to_i.positive?
|
|
@@ -106,16 +166,16 @@ module Wavify
|
|
|
106
166
|
)
|
|
107
167
|
|
|
108
168
|
io, close_io = open_output(io_or_path)
|
|
109
|
-
io
|
|
110
|
-
io.truncate(0) if io.respond_to?(:truncate)
|
|
169
|
+
prepare_output!(io, owned: close_io)
|
|
111
170
|
|
|
112
171
|
encoder = Vorbis::Encoder.new(
|
|
113
172
|
channels: target_format.channels,
|
|
114
173
|
rate: target_format.sample_rate,
|
|
115
|
-
quality:
|
|
174
|
+
quality: encode_quality
|
|
116
175
|
)
|
|
117
176
|
|
|
118
|
-
encoder.write_headers { |page_bytes| io
|
|
177
|
+
encoder.write_headers { |page_bytes| write_all(io, page_bytes) }
|
|
178
|
+
channels_data = Array.new(target_format.channels) { [] }
|
|
119
179
|
|
|
120
180
|
writer = lambda do |chunk|
|
|
121
181
|
raise InvalidParameterError, "stream chunk must be Core::SampleBuffer" unless chunk.is_a?(Core::SampleBuffer)
|
|
@@ -123,19 +183,18 @@ module Wavify
|
|
|
123
183
|
buffer = chunk.format == target_format ? chunk : chunk.convert(target_format)
|
|
124
184
|
next nil if buffer.sample_frame_count.zero?
|
|
125
185
|
|
|
126
|
-
channels_data
|
|
127
|
-
buffer.samples.
|
|
128
|
-
|
|
186
|
+
channels_data.each(&:clear)
|
|
187
|
+
buffer.samples.each_with_index do |sample, index|
|
|
188
|
+
channels_data[index % target_format.channels] << sample.to_f
|
|
129
189
|
end
|
|
130
|
-
encoder.encode(channels_data) { |page_bytes| io
|
|
190
|
+
encoder.encode(channels_data) { |page_bytes| write_all(io, page_bytes) }
|
|
131
191
|
end
|
|
132
192
|
|
|
133
193
|
yield writer
|
|
134
194
|
|
|
135
|
-
encoder.finish { |page_bytes| io
|
|
195
|
+
encoder.finish { |page_bytes| write_all(io, page_bytes) }
|
|
136
196
|
encoder.close
|
|
137
|
-
io
|
|
138
|
-
io.rewind if io.respond_to?(:rewind)
|
|
197
|
+
finalize_output!(io, owned: close_io)
|
|
139
198
|
io_or_path
|
|
140
199
|
rescue StandardError
|
|
141
200
|
begin
|
|
@@ -153,13 +212,13 @@ module Wavify
|
|
|
153
212
|
# @param io_or_path [String, IO]
|
|
154
213
|
# @return [Hash]
|
|
155
214
|
def metadata(io_or_path)
|
|
215
|
+
ensure_available!
|
|
156
216
|
io, close_io = open_input(io_or_path)
|
|
157
|
-
|
|
158
|
-
chained_streams, physical_ogg_info = read_ogg_logical_stream_chains(io)
|
|
217
|
+
chained_streams, physical_ogg_info = read_ogg_logical_stream_chains(io, storage: :tempfile)
|
|
159
218
|
|
|
160
219
|
if chained_streams.length > 1
|
|
161
220
|
chain_metadatas = chained_streams.map do |stream|
|
|
162
|
-
parse_single_logical_stream_metadata(
|
|
221
|
+
parse_single_logical_stream_metadata(logical_stream_io(stream))
|
|
163
222
|
end
|
|
164
223
|
if physical_ogg_info[:interleaved_multistream]
|
|
165
224
|
return merge_interleaved_vorbis_metadata(chain_metadatas, chained_streams, physical_ogg_info)
|
|
@@ -168,29 +227,48 @@ module Wavify
|
|
|
168
227
|
return merge_chained_vorbis_metadata(chain_metadatas, chained_streams)
|
|
169
228
|
end
|
|
170
229
|
|
|
171
|
-
parse_single_logical_stream_metadata(
|
|
230
|
+
parse_single_logical_stream_metadata(logical_stream_io(chained_streams.fetch(0)))
|
|
172
231
|
ensure
|
|
232
|
+
close_logical_streams(chained_streams)
|
|
173
233
|
io.close if close_io && io
|
|
174
234
|
end
|
|
175
235
|
|
|
176
236
|
private
|
|
177
237
|
|
|
238
|
+
def ensure_available!
|
|
239
|
+
return if available?
|
|
240
|
+
|
|
241
|
+
missing = DEPENDENCY_LOAD_ERRORS.keys.join(", ")
|
|
242
|
+
raise UnsupportedFormatError,
|
|
243
|
+
"OGG Vorbis support requires optional gems: #{missing}. Add them to your Gemfile to read or write .ogg/.oga files."
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def normalize_vorbis_quality(quality)
|
|
247
|
+
unless quality.is_a?(Numeric) && VORBIS_ENCODE_QUALITY_RANGE.cover?(quality.to_f)
|
|
248
|
+
raise InvalidParameterError, "quality must be Numeric in -0.1..1.0"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
quality.to_f
|
|
252
|
+
end
|
|
253
|
+
|
|
178
254
|
# ---------------------------------------------------------------------------
|
|
179
255
|
# OGG container reading (using ogg-ruby)
|
|
180
256
|
# ---------------------------------------------------------------------------
|
|
181
257
|
|
|
182
|
-
def read_ogg_logical_stream_chains_from_input(io_or_path, with_info: false)
|
|
258
|
+
def read_ogg_logical_stream_chains_from_input(io_or_path, with_info: false, storage: :memory)
|
|
183
259
|
io, close_io = open_input(io_or_path)
|
|
184
|
-
ensure_seekable!(io)
|
|
185
260
|
|
|
186
|
-
chains, physical_info = read_ogg_logical_stream_chains(io)
|
|
261
|
+
chains, physical_info = read_ogg_logical_stream_chains(io, storage: storage)
|
|
187
262
|
with_info ? [chains, physical_info] : chains
|
|
188
263
|
ensure
|
|
189
264
|
io.close if close_io && io
|
|
190
265
|
end
|
|
191
266
|
|
|
192
|
-
def read_ogg_logical_stream_chains(io)
|
|
193
|
-
|
|
267
|
+
def read_ogg_logical_stream_chains(io, storage: :memory)
|
|
268
|
+
unless %i[memory tempfile].include?(storage)
|
|
269
|
+
raise InvalidParameterError, "OGG logical stream storage must be :memory or :tempfile"
|
|
270
|
+
end
|
|
271
|
+
|
|
194
272
|
sync = Ogg::SyncState.new
|
|
195
273
|
streams_by_serial = {}
|
|
196
274
|
total_page_count = 0
|
|
@@ -217,7 +295,7 @@ module Wavify
|
|
|
217
295
|
|
|
218
296
|
stream = {
|
|
219
297
|
serial_number: sn,
|
|
220
|
-
|
|
298
|
+
storage: build_logical_stream_storage(storage),
|
|
221
299
|
page_count: 0,
|
|
222
300
|
bos_page_count: 0,
|
|
223
301
|
eos_page_count: 0,
|
|
@@ -225,7 +303,7 @@ module Wavify
|
|
|
225
303
|
eos_seen: false,
|
|
226
304
|
first_physical_page_index: physical_page_index,
|
|
227
305
|
last_physical_page_index: physical_page_index,
|
|
228
|
-
physical_page_indices: []
|
|
306
|
+
physical_page_indices: storage == :memory ? [] : nil
|
|
229
307
|
}
|
|
230
308
|
streams_by_serial[sn] = stream
|
|
231
309
|
elsif stream[:eos_seen]
|
|
@@ -234,14 +312,14 @@ module Wavify
|
|
|
234
312
|
|
|
235
313
|
raise InvalidFormatError, "unexpected BOS page in OGG logical stream" if stream[:page_count].positive? && bos
|
|
236
314
|
|
|
237
|
-
stream
|
|
315
|
+
append_logical_stream_page(stream.fetch(:storage), page.to_s)
|
|
238
316
|
stream[:page_count] += 1
|
|
239
317
|
stream[:bos_page_count] += 1 if bos
|
|
240
318
|
stream[:eos_page_count] += 1 if eos
|
|
241
319
|
stream[:continued_page_count] += 1 if continued
|
|
242
320
|
stream[:eos_seen] = true if eos
|
|
243
321
|
stream[:last_physical_page_index] = physical_page_index
|
|
244
|
-
stream[:physical_page_indices]
|
|
322
|
+
stream[:physical_page_indices]&.push(physical_page_index)
|
|
245
323
|
|
|
246
324
|
total_page_count += 1
|
|
247
325
|
total_bos_page_count += 1 if bos
|
|
@@ -275,12 +353,20 @@ module Wavify
|
|
|
275
353
|
end
|
|
276
354
|
|
|
277
355
|
result_streams = streams.map do |stream|
|
|
278
|
-
stream.
|
|
279
|
-
|
|
280
|
-
interleaved_pages: per_stream_overlap.fetch(stream[:serial_number], false)
|
|
281
|
-
|
|
356
|
+
finalize_logical_stream_storage!(stream.fetch(:storage))
|
|
357
|
+
result = stream.reject { |key, _value| key == :storage }.merge(
|
|
358
|
+
interleaved_pages: per_stream_overlap.fetch(stream[:serial_number], false),
|
|
359
|
+
physical_page_indices: (stream[:physical_page_indices] || []).freeze
|
|
360
|
+
)
|
|
361
|
+
if storage == :memory
|
|
362
|
+
result[:bytes] = stream.fetch(:storage).dup.freeze
|
|
363
|
+
else
|
|
364
|
+
result[:io] = stream.fetch(:storage)
|
|
365
|
+
end
|
|
366
|
+
result.freeze
|
|
282
367
|
end
|
|
283
368
|
|
|
369
|
+
completed = true
|
|
284
370
|
[
|
|
285
371
|
result_streams,
|
|
286
372
|
{
|
|
@@ -299,6 +385,49 @@ module Wavify
|
|
|
299
385
|
raise InvalidFormatError, "OGG stream sequence error: #{e.message}"
|
|
300
386
|
ensure
|
|
301
387
|
sync&.clear
|
|
388
|
+
unless completed
|
|
389
|
+
close_logical_streams(streams_by_serial&.values&.map { |stream| { io: stream[:storage] } })
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def build_logical_stream_storage(storage)
|
|
394
|
+
return +"".b if storage == :memory
|
|
395
|
+
|
|
396
|
+
Tempfile.new(["wavify-ogg-logical-stream", ".ogg"]).tap(&:binmode)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def append_logical_stream_page(storage, bytes)
|
|
400
|
+
if storage.is_a?(String)
|
|
401
|
+
storage << bytes
|
|
402
|
+
else
|
|
403
|
+
write_all(storage, bytes)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def finalize_logical_stream_storage!(storage)
|
|
408
|
+
return if storage.is_a?(String)
|
|
409
|
+
|
|
410
|
+
storage.flush
|
|
411
|
+
storage.rewind
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def logical_stream_io(stream)
|
|
415
|
+
return StringIO.new(stream.fetch(:bytes)) if stream.key?(:bytes)
|
|
416
|
+
|
|
417
|
+
io = stream.fetch(:io)
|
|
418
|
+
io.rewind
|
|
419
|
+
io
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def close_logical_streams(streams)
|
|
423
|
+
Array(streams).each do |stream|
|
|
424
|
+
io = stream[:io]
|
|
425
|
+
next unless io.respond_to?(:close!)
|
|
426
|
+
|
|
427
|
+
io.close!
|
|
428
|
+
rescue IOError, SystemCallError
|
|
429
|
+
nil
|
|
430
|
+
end
|
|
302
431
|
end
|
|
303
432
|
|
|
304
433
|
# Reads OGG packets from a single logical stream IO using ogg-ruby.
|
|
@@ -307,7 +436,6 @@ module Wavify
|
|
|
307
436
|
# :data, :kind, :granule_position keys, and ogg_info is a Hash with
|
|
308
437
|
# page-level statistics.
|
|
309
438
|
def read_ogg_packets(io)
|
|
310
|
-
io.rewind
|
|
311
439
|
sync = Ogg::SyncState.new
|
|
312
440
|
stream_state = nil
|
|
313
441
|
serial_number = nil
|
|
@@ -378,15 +506,125 @@ module Wavify
|
|
|
378
506
|
sync&.clear
|
|
379
507
|
end
|
|
380
508
|
|
|
509
|
+
def build_ogg_packet_reader(io)
|
|
510
|
+
{
|
|
511
|
+
io: io,
|
|
512
|
+
sync: Ogg::SyncState.new,
|
|
513
|
+
stream_state: nil,
|
|
514
|
+
serial_number: nil,
|
|
515
|
+
page_count: 0,
|
|
516
|
+
bos_page_count: 0,
|
|
517
|
+
eos_page_count: 0,
|
|
518
|
+
continued_page_count: 0,
|
|
519
|
+
max_granule_position: nil,
|
|
520
|
+
input_eof: false,
|
|
521
|
+
closed: false
|
|
522
|
+
}
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def next_ogg_packet!(reader)
|
|
526
|
+
loop do
|
|
527
|
+
if (packet = reader[:stream_state]&.packetout)
|
|
528
|
+
granule_position = packet.granulepos == -1 ? nil : packet.granulepos
|
|
529
|
+
if granule_position
|
|
530
|
+
reader[:max_granule_position] = [reader[:max_granule_position] || 0, granule_position].max
|
|
531
|
+
end
|
|
532
|
+
return {
|
|
533
|
+
data: packet.data,
|
|
534
|
+
bos: packet.bos?,
|
|
535
|
+
eos: packet.eos?,
|
|
536
|
+
packetno: packet.packetno,
|
|
537
|
+
kind: classify_vorbis_packet(packet.data),
|
|
538
|
+
granule_position: granule_position
|
|
539
|
+
}
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
if (page = reader.fetch(:sync).pageout)
|
|
543
|
+
serial_number = page.serialno
|
|
544
|
+
if reader[:serial_number] && serial_number != reader[:serial_number]
|
|
545
|
+
raise UnsupportedFormatError, "multiple OGG logical streams require the multistream decoder"
|
|
546
|
+
end
|
|
547
|
+
if reader[:page_count].zero? && !page.bos?
|
|
548
|
+
raise InvalidFormatError, "first OGG page must have BOS flag"
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
reader[:serial_number] ||= serial_number
|
|
552
|
+
reader[:stream_state] ||= Ogg::StreamState.new(serial_number)
|
|
553
|
+
reader.fetch(:stream_state).pagein(page)
|
|
554
|
+
reader[:page_count] += 1
|
|
555
|
+
reader[:bos_page_count] += 1 if page.bos?
|
|
556
|
+
reader[:eos_page_count] += 1 if page.eos?
|
|
557
|
+
reader[:continued_page_count] += 1 if page.continued?
|
|
558
|
+
next
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
return nil if reader[:input_eof]
|
|
562
|
+
|
|
563
|
+
data = reader.fetch(:io).read(4096)
|
|
564
|
+
if data.nil? || data.empty?
|
|
565
|
+
reader[:input_eof] = true
|
|
566
|
+
else
|
|
567
|
+
reader.fetch(:sync).write(data)
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
rescue Ogg::CorruptDataError, Ogg::SyncCorruptDataError => e
|
|
571
|
+
raise InvalidFormatError, "OGG data corrupt or invalid checksum: #{e.message}"
|
|
572
|
+
rescue Ogg::StreamCorruptDataError => e
|
|
573
|
+
raise InvalidFormatError, "OGG stream sequence error: #{e.message}"
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def close_ogg_packet_reader(reader)
|
|
577
|
+
return unless reader && !reader[:closed]
|
|
578
|
+
|
|
579
|
+
reader[:stream_state]&.clear
|
|
580
|
+
reader[:sync]&.clear
|
|
581
|
+
reader[:closed] = true
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def multistream_ogg_prefix?(io_or_path, probe_size: 4096, tail_probe_size: 65_536)
|
|
585
|
+
io, close_io = open_input(io_or_path)
|
|
586
|
+
ensure_seekable!(io)
|
|
587
|
+
original_position = io.pos
|
|
588
|
+
bytes = +"".b
|
|
589
|
+
while bytes.bytesize < probe_size
|
|
590
|
+
chunk = io.read(probe_size - bytes.bytesize)
|
|
591
|
+
break if chunk.nil? || chunk.empty?
|
|
592
|
+
|
|
593
|
+
bytes << chunk
|
|
594
|
+
end
|
|
595
|
+
io.seek(0, IO::SEEK_END)
|
|
596
|
+
input_end = io.pos
|
|
597
|
+
tail_start = [original_position + bytes.bytesize, input_end - tail_probe_size].max
|
|
598
|
+
if tail_start < input_end
|
|
599
|
+
io.seek(tail_start, IO::SEEK_SET)
|
|
600
|
+
tail = io.read(tail_probe_size)
|
|
601
|
+
bytes << tail if tail
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
sync = Ogg::SyncState.new
|
|
605
|
+
sync.write(bytes)
|
|
606
|
+
serial_numbers = []
|
|
607
|
+
while (page = sync.pageout)
|
|
608
|
+
serial_numbers << page.serialno
|
|
609
|
+
return true if serial_numbers.uniq.length > 1
|
|
610
|
+
end
|
|
611
|
+
false
|
|
612
|
+
rescue Ogg::CorruptDataError, Ogg::SyncCorruptDataError
|
|
613
|
+
false
|
|
614
|
+
ensure
|
|
615
|
+
sync&.clear
|
|
616
|
+
io.seek(original_position, IO::SEEK_SET) if original_position && io
|
|
617
|
+
io.close if close_io && io
|
|
618
|
+
end
|
|
619
|
+
|
|
381
620
|
# ---------------------------------------------------------------------------
|
|
382
621
|
# Vorbis decode (using Vorbis::Native synthesis functions)
|
|
383
622
|
# ---------------------------------------------------------------------------
|
|
384
623
|
|
|
385
624
|
def build_vorbis_decode_context(io_or_path)
|
|
386
625
|
io, close_io = open_input(io_or_path)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
packet_entries, ogg_info = read_ogg_packets(io)
|
|
626
|
+
packet_reader = build_ogg_packet_reader(io)
|
|
627
|
+
packet_entries = Array.new(3) { next_ogg_packet!(packet_reader) }
|
|
390
628
|
raise InvalidFormatError, "missing Vorbis identification header" if packet_entries[0].nil?
|
|
391
629
|
raise InvalidFormatError, "missing Vorbis comment header" if packet_entries[1].nil?
|
|
392
630
|
raise InvalidFormatError, "missing Vorbis setup header" if packet_entries[2].nil?
|
|
@@ -394,7 +632,9 @@ module Wavify
|
|
|
394
632
|
info_ptr = FFI::MemoryPointer.new(Vorbis::Native::VorbisInfo.size)
|
|
395
633
|
comment_ptr = FFI::MemoryPointer.new(Vorbis::Native::VorbisComment.size)
|
|
396
634
|
Vorbis::Native.vorbis_info_init(info_ptr)
|
|
635
|
+
info_initialized = true
|
|
397
636
|
Vorbis::Native.vorbis_comment_init(comment_ptr)
|
|
637
|
+
comment_initialized = true
|
|
398
638
|
|
|
399
639
|
packet_entries.first(3).each_with_index do |entry, idx|
|
|
400
640
|
pkt = Ogg::Packet.new(
|
|
@@ -411,30 +651,34 @@ module Wavify
|
|
|
411
651
|
channels = vinfo[:channels]
|
|
412
652
|
sample_rate = vinfo[:rate]
|
|
413
653
|
|
|
414
|
-
audio_packets = packet_entries.drop(3).select { |e| e.fetch(:kind) == :audio }
|
|
415
|
-
raise InvalidFormatError, "OGG Vorbis stream does not contain audio packets" if audio_packets.empty?
|
|
416
|
-
|
|
417
654
|
format = Core::Format.new(channels: channels, sample_rate: sample_rate, bit_depth: 32, sample_format: :float)
|
|
418
655
|
|
|
419
|
-
{
|
|
656
|
+
context = {
|
|
420
657
|
format: format,
|
|
421
658
|
channels: channels,
|
|
422
659
|
sample_rate: sample_rate,
|
|
423
|
-
|
|
424
|
-
|
|
660
|
+
packet_reader: packet_reader,
|
|
661
|
+
io: io,
|
|
662
|
+
close_io: close_io,
|
|
425
663
|
info_ptr: info_ptr,
|
|
426
664
|
comment_ptr: comment_ptr
|
|
427
665
|
}
|
|
666
|
+
ownership_transferred = true
|
|
667
|
+
context
|
|
428
668
|
ensure
|
|
429
|
-
|
|
669
|
+
unless ownership_transferred
|
|
670
|
+
Vorbis::Native.vorbis_comment_clear(comment_ptr) if comment_initialized
|
|
671
|
+
Vorbis::Native.vorbis_info_clear(info_ptr) if info_initialized
|
|
672
|
+
close_ogg_packet_reader(packet_reader)
|
|
673
|
+
io.close if close_io && io
|
|
674
|
+
end
|
|
430
675
|
end
|
|
431
676
|
|
|
432
677
|
def run_vorbis_decode_pipeline(decode_context, streaming: false, chunk_size: nil, &block)
|
|
433
678
|
info_ptr = decode_context.fetch(:info_ptr)
|
|
434
679
|
comment_ptr = decode_context.fetch(:comment_ptr)
|
|
435
|
-
|
|
680
|
+
packet_reader = decode_context.fetch(:packet_reader)
|
|
436
681
|
channels = decode_context.fetch(:channels)
|
|
437
|
-
max_granule = decode_context.fetch(:sample_frame_count)
|
|
438
682
|
format = decode_context.fetch(:format)
|
|
439
683
|
|
|
440
684
|
dsp_ptr = FFI::MemoryPointer.new(Vorbis::Native::VorbisDspState.size)
|
|
@@ -453,10 +697,16 @@ module Wavify
|
|
|
453
697
|
|
|
454
698
|
block_initialized = true
|
|
455
699
|
|
|
456
|
-
|
|
700
|
+
samples = []
|
|
701
|
+
emitted_sample_count = 0
|
|
702
|
+
audio_packet_count = 0
|
|
703
|
+
chunk_sample_count = streaming ? chunk_size * channels : nil
|
|
457
704
|
ptr_size = FFI::Pointer.size
|
|
458
705
|
|
|
459
|
-
|
|
706
|
+
while (entry = next_ogg_packet!(packet_reader))
|
|
707
|
+
next unless entry.fetch(:kind) == :audio
|
|
708
|
+
|
|
709
|
+
audio_packet_count += 1
|
|
460
710
|
pkt = Ogg::Packet.new(
|
|
461
711
|
data: entry.fetch(:data),
|
|
462
712
|
bos: entry.fetch(:bos, false),
|
|
@@ -464,7 +714,11 @@ module Wavify
|
|
|
464
714
|
granulepos: entry[:granule_position].nil? ? -1 : entry[:granule_position],
|
|
465
715
|
packetno: entry.fetch(:packetno, 0)
|
|
466
716
|
)
|
|
467
|
-
|
|
717
|
+
synthesis_result = Vorbis::Native.vorbis_synthesis(block_ptr, pkt.native)
|
|
718
|
+
unless synthesis_result.zero?
|
|
719
|
+
raise InvalidFormatError,
|
|
720
|
+
"Vorbis packet decode failed (packet #{entry.fetch(:packetno)}, code #{synthesis_result})"
|
|
721
|
+
end
|
|
468
722
|
|
|
469
723
|
Vorbis::Native.vorbis_synthesis_blockin(dsp_ptr, block_ptr)
|
|
470
724
|
|
|
@@ -473,34 +727,42 @@ module Wavify
|
|
|
473
727
|
n.times do |i|
|
|
474
728
|
channels.times do |ch|
|
|
475
729
|
ch_ptr = ch_array_ptr.get_pointer(ch * ptr_size)
|
|
476
|
-
|
|
730
|
+
samples << ch_ptr.get_float(i * 4)
|
|
477
731
|
end
|
|
478
732
|
end
|
|
479
733
|
Vorbis::Native.vorbis_synthesis_read(dsp_ptr, n)
|
|
734
|
+
|
|
735
|
+
while streaming && samples.length > chunk_sample_count
|
|
736
|
+
chunk_samples = samples.slice!(0, chunk_sample_count)
|
|
737
|
+
yield Core::SampleBuffer.new(chunk_samples, format)
|
|
738
|
+
emitted_sample_count += chunk_samples.length
|
|
739
|
+
end
|
|
480
740
|
end
|
|
481
741
|
end
|
|
742
|
+
raise InvalidFormatError, "OGG Vorbis stream does not contain audio packets" if audio_packet_count.zero?
|
|
482
743
|
|
|
744
|
+
max_granule = packet_reader.fetch(:max_granule_position)
|
|
483
745
|
if max_granule&.positive?
|
|
484
|
-
|
|
485
|
-
|
|
746
|
+
remaining_sample_count = [(max_granule * channels) - emitted_sample_count, 0].max
|
|
747
|
+
samples = samples.first(remaining_sample_count) if samples.length > remaining_sample_count
|
|
486
748
|
end
|
|
487
749
|
|
|
488
|
-
result_buffer = Core::SampleBuffer.new(all_samples, format)
|
|
489
|
-
|
|
490
750
|
if streaming && block
|
|
491
|
-
each_sample_buffer_frame_slice(
|
|
751
|
+
each_sample_buffer_frame_slice(Core::SampleBuffer.new(samples, format), chunk_size, &block)
|
|
492
752
|
nil
|
|
493
753
|
elsif block
|
|
494
|
-
yield
|
|
754
|
+
yield Core::SampleBuffer.new(samples, format)
|
|
495
755
|
nil
|
|
496
756
|
else
|
|
497
|
-
|
|
757
|
+
Core::SampleBuffer.new(samples, format)
|
|
498
758
|
end
|
|
499
759
|
ensure
|
|
500
760
|
Vorbis::Native.vorbis_block_clear(block_ptr) if block_initialized
|
|
501
761
|
Vorbis::Native.vorbis_dsp_clear(dsp_ptr) if dsp_initialized
|
|
502
762
|
Vorbis::Native.vorbis_comment_clear(comment_ptr) if comment_ptr
|
|
503
763
|
Vorbis::Native.vorbis_info_clear(info_ptr) if info_ptr
|
|
764
|
+
close_ogg_packet_reader(packet_reader)
|
|
765
|
+
decode_context[:io].close if decode_context[:close_io] && decode_context[:io]
|
|
504
766
|
end
|
|
505
767
|
|
|
506
768
|
# ---------------------------------------------------------------------------
|
|
@@ -508,15 +770,53 @@ module Wavify
|
|
|
508
770
|
# ---------------------------------------------------------------------------
|
|
509
771
|
|
|
510
772
|
def parse_single_logical_stream_metadata(io)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
773
|
+
packet_reader = build_ogg_packet_reader(io)
|
|
774
|
+
header_entries = []
|
|
775
|
+
packet_count = 0
|
|
776
|
+
audio_packet_count = 0
|
|
777
|
+
non_audio_packet_count = 0
|
|
778
|
+
audio_packets_with_granule_count = 0
|
|
779
|
+
first_audio_granule_position = nil
|
|
780
|
+
last_audio_granule_position = nil
|
|
781
|
+
|
|
782
|
+
while (entry = next_ogg_packet!(packet_reader))
|
|
783
|
+
packet_count += 1
|
|
784
|
+
if header_entries.length < 3
|
|
785
|
+
header_entries << entry
|
|
786
|
+
next
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
if entry.fetch(:kind) == :audio
|
|
790
|
+
audio_packet_count += 1
|
|
791
|
+
granule_position = entry[:granule_position]
|
|
792
|
+
if granule_position
|
|
793
|
+
audio_packets_with_granule_count += 1
|
|
794
|
+
first_audio_granule_position ||= granule_position
|
|
795
|
+
last_audio_granule_position = granule_position
|
|
796
|
+
end
|
|
797
|
+
else
|
|
798
|
+
non_audio_packet_count += 1
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
raise InvalidFormatError, "missing Vorbis identification header" if header_entries[0].nil?
|
|
802
|
+
raise InvalidFormatError, "missing Vorbis comment header" if header_entries[1].nil?
|
|
803
|
+
raise InvalidFormatError, "missing Vorbis setup header" if header_entries[2].nil?
|
|
804
|
+
|
|
805
|
+
ogg_info = {
|
|
806
|
+
serial_number: packet_reader[:serial_number],
|
|
807
|
+
page_count: packet_reader[:page_count],
|
|
808
|
+
max_granule_position: packet_reader[:max_granule_position],
|
|
809
|
+
bos_page_count: packet_reader[:bos_page_count],
|
|
810
|
+
eos_page_count: packet_reader[:eos_page_count],
|
|
811
|
+
continued_page_count: packet_reader[:continued_page_count]
|
|
812
|
+
}
|
|
515
813
|
|
|
516
814
|
info_ptr = FFI::MemoryPointer.new(Vorbis::Native::VorbisInfo.size)
|
|
517
815
|
comment_ptr = FFI::MemoryPointer.new(Vorbis::Native::VorbisComment.size)
|
|
518
816
|
Vorbis::Native.vorbis_info_init(info_ptr)
|
|
817
|
+
info_initialized = true
|
|
519
818
|
Vorbis::Native.vorbis_comment_init(comment_ptr)
|
|
819
|
+
comment_initialized = true
|
|
520
820
|
|
|
521
821
|
setup_parsed = false
|
|
522
822
|
saved_channels = nil
|
|
@@ -524,7 +824,7 @@ module Wavify
|
|
|
524
824
|
saved_bitrate_nominal = nil
|
|
525
825
|
saved_bitrate_lower = nil
|
|
526
826
|
saved_bitrate_upper = nil
|
|
527
|
-
|
|
827
|
+
header_entries.each_with_index do |entry, index|
|
|
528
828
|
pkt = Ogg::Packet.new(
|
|
529
829
|
data: entry.fetch(:data),
|
|
530
830
|
bos: entry.fetch(:bos, index.zero?),
|
|
@@ -563,8 +863,11 @@ module Wavify
|
|
|
563
863
|
end
|
|
564
864
|
|
|
565
865
|
vc = Vorbis::Native::VorbisComment.new(comment_ptr)
|
|
566
|
-
vendor = vc[:vendor].null? ? nil : vc[:vendor].read_string
|
|
866
|
+
vendor = vc[:vendor].null? ? nil : vc[:vendor].read_string.force_encoding(Encoding::UTF_8).scrub
|
|
567
867
|
comments_hash = {}
|
|
868
|
+
comment_values = Hash.new { |hash, key| hash[key] = [] }
|
|
869
|
+
raw_comments = []
|
|
870
|
+
raw_comment_bytes = []
|
|
568
871
|
n_comments = vc[:comments]
|
|
569
872
|
if n_comments.positive? && !vc[:user_comments].null?
|
|
570
873
|
user_comments_ptr = vc[:user_comments]
|
|
@@ -577,15 +880,18 @@ module Wavify
|
|
|
577
880
|
next unless len.positive?
|
|
578
881
|
|
|
579
882
|
str = str_ptr.read_bytes(len)
|
|
580
|
-
|
|
581
|
-
|
|
883
|
+
raw_comment_bytes << str.b.dup.freeze
|
|
884
|
+
display = str.force_encoding(Encoding::UTF_8).scrub
|
|
885
|
+
raw_comments << display
|
|
886
|
+
key, value = display.split("=", 2)
|
|
887
|
+
if key && value
|
|
888
|
+
normalized_key = key.downcase
|
|
889
|
+
comments_hash[normalized_key] ||= value
|
|
890
|
+
comment_values[normalized_key] << value
|
|
891
|
+
end
|
|
582
892
|
end
|
|
583
893
|
end
|
|
584
894
|
|
|
585
|
-
audio_packets = packet_entries.drop(3).select { |e| e.fetch(:kind) == :audio }
|
|
586
|
-
non_audio_packets = packet_entries.drop(3).reject { |e| e.fetch(:kind) == :audio }
|
|
587
|
-
known_granule_positions = audio_packets.filter_map { |e| e[:granule_position] }
|
|
588
|
-
|
|
589
895
|
format = Core::Format.new(channels: channels, sample_rate: sample_rate, bit_depth: 32, sample_format: :float)
|
|
590
896
|
sample_frame_count = ogg_info[:max_granule_position]
|
|
591
897
|
duration = sample_frame_count ? Core::Duration.from_samples(sample_frame_count, format.sample_rate) : nil
|
|
@@ -593,9 +899,14 @@ module Wavify
|
|
|
593
899
|
{
|
|
594
900
|
format: format,
|
|
595
901
|
sample_frame_count: sample_frame_count,
|
|
902
|
+
granule_position_frame_count: sample_frame_count,
|
|
903
|
+
sample_frame_count_estimated: !sample_frame_count.nil?,
|
|
596
904
|
duration: duration,
|
|
597
905
|
vendor: vendor,
|
|
598
906
|
comments: comments_hash,
|
|
907
|
+
comment_values: comment_values.transform_values(&:freeze).freeze,
|
|
908
|
+
raw_comments: raw_comments.freeze,
|
|
909
|
+
raw_comment_bytes: raw_comment_bytes.freeze,
|
|
599
910
|
nominal_bitrate: nominal_bitrate_raw.positive? ? nominal_bitrate_raw : nil,
|
|
600
911
|
minimum_bitrate: minimum_bitrate_raw.positive? ? minimum_bitrate_raw : nil,
|
|
601
912
|
maximum_bitrate: maximum_bitrate_raw.positive? ? maximum_bitrate_raw : nil,
|
|
@@ -603,17 +914,15 @@ module Wavify
|
|
|
603
914
|
blocksize_large: blocksize_large,
|
|
604
915
|
ogg_serial_number: ogg_info[:serial_number],
|
|
605
916
|
ogg_page_count: ogg_info[:page_count],
|
|
606
|
-
ogg_packet_count:
|
|
917
|
+
ogg_packet_count: packet_count,
|
|
607
918
|
ogg_bos_page_count: ogg_info[:bos_page_count],
|
|
608
919
|
ogg_eos_page_count: ogg_info[:eos_page_count],
|
|
609
920
|
ogg_continued_page_count: ogg_info[:continued_page_count],
|
|
610
|
-
vorbis_audio_packet_count:
|
|
611
|
-
vorbis_non_audio_packet_count:
|
|
612
|
-
vorbis_audio_packets_with_granule_count:
|
|
613
|
-
first_audio_packet_granule_position:
|
|
614
|
-
last_audio_packet_granule_position:
|
|
615
|
-
!e[:granule_position].nil?
|
|
616
|
-
end&.fetch(:granule_position),
|
|
921
|
+
vorbis_audio_packet_count: audio_packet_count,
|
|
922
|
+
vorbis_non_audio_packet_count: non_audio_packet_count,
|
|
923
|
+
vorbis_audio_packets_with_granule_count: audio_packets_with_granule_count,
|
|
924
|
+
first_audio_packet_granule_position: first_audio_granule_position,
|
|
925
|
+
last_audio_packet_granule_position: last_audio_granule_position,
|
|
617
926
|
vorbis_setup_parsed: setup_parsed,
|
|
618
927
|
vorbis_codebook_count: nil,
|
|
619
928
|
vorbis_codebook_dimensions: nil,
|
|
@@ -656,11 +965,12 @@ module Wavify
|
|
|
656
965
|
vorbis_output_assembly_window_curve_preflight_count: nil,
|
|
657
966
|
vorbis_long_window_packet_count: nil,
|
|
658
967
|
vorbis_short_window_packet_count: nil,
|
|
659
|
-
setup_header_size:
|
|
968
|
+
setup_header_size: header_entries[2]&.fetch(:data)&.bytesize
|
|
660
969
|
}
|
|
661
970
|
ensure
|
|
662
|
-
Vorbis::Native.vorbis_comment_clear(comment_ptr) if
|
|
663
|
-
Vorbis::Native.vorbis_info_clear(info_ptr) if
|
|
971
|
+
Vorbis::Native.vorbis_comment_clear(comment_ptr) if comment_initialized
|
|
972
|
+
Vorbis::Native.vorbis_info_clear(info_ptr) if info_initialized
|
|
973
|
+
close_ogg_packet_reader(packet_reader)
|
|
664
974
|
end
|
|
665
975
|
|
|
666
976
|
# ---------------------------------------------------------------------------
|
|
@@ -789,68 +1099,120 @@ module Wavify
|
|
|
789
1099
|
# Chained / interleaved stream decoding (high-level helpers)
|
|
790
1100
|
# ---------------------------------------------------------------------------
|
|
791
1101
|
|
|
792
|
-
def decode_chained_vorbis_read_if_needed(io_or_path,
|
|
793
|
-
|
|
794
|
-
|
|
1102
|
+
def decode_chained_vorbis_read_if_needed(io_or_path, target_format: nil, mix_strategy: :clip)
|
|
1103
|
+
original_position = io_or_path.pos if !io_or_path.is_a?(String) && io_or_path.respond_to?(:pos)
|
|
1104
|
+
chained_streams, physical_ogg_info = read_ogg_logical_stream_chains_from_input(
|
|
1105
|
+
io_or_path,
|
|
1106
|
+
with_info: true,
|
|
1107
|
+
storage: :tempfile
|
|
1108
|
+
)
|
|
1109
|
+
unless chained_streams.length > 1
|
|
1110
|
+
io_or_path.seek(original_position, IO::SEEK_SET) if original_position && io_or_path.respond_to?(:seek)
|
|
1111
|
+
return nil
|
|
1112
|
+
end
|
|
795
1113
|
|
|
796
1114
|
if physical_ogg_info[:interleaved_multistream]
|
|
797
1115
|
decoded_buffers = chained_streams.map do |stream|
|
|
798
|
-
read(
|
|
1116
|
+
read(logical_stream_io(stream))
|
|
799
1117
|
end
|
|
800
1118
|
|
|
801
|
-
return mix_vorbis_sample_buffers(decoded_buffers, target_format: target_format)
|
|
1119
|
+
return mix_vorbis_sample_buffers(decoded_buffers, target_format: target_format, strategy: mix_strategy)
|
|
802
1120
|
end
|
|
803
1121
|
|
|
804
1122
|
decoded_buffers = chained_streams.map do |stream|
|
|
805
|
-
read(
|
|
1123
|
+
read(logical_stream_io(stream), format: target_format)
|
|
806
1124
|
end
|
|
807
1125
|
|
|
808
1126
|
concatenate_vorbis_sample_buffers(decoded_buffers, target_format: target_format)
|
|
1127
|
+
ensure
|
|
1128
|
+
close_logical_streams(chained_streams)
|
|
809
1129
|
end
|
|
810
1130
|
|
|
811
|
-
def stream_chained_vorbis_if_needed(io_or_path, chunk_size:,
|
|
812
|
-
chained_streams, physical_ogg_info = read_ogg_logical_stream_chains_from_input(
|
|
1131
|
+
def stream_chained_vorbis_if_needed(io_or_path, chunk_size:, mix_strategy: :clip, &block)
|
|
1132
|
+
chained_streams, physical_ogg_info = read_ogg_logical_stream_chains_from_input(
|
|
1133
|
+
io_or_path,
|
|
1134
|
+
with_info: true,
|
|
1135
|
+
storage: :tempfile
|
|
1136
|
+
)
|
|
813
1137
|
return false unless chained_streams.length > 1
|
|
814
1138
|
|
|
815
1139
|
if physical_ogg_info[:interleaved_multistream]
|
|
816
1140
|
stream_metadatas = chained_streams.map do |stream|
|
|
817
|
-
parse_single_logical_stream_metadata(
|
|
1141
|
+
parse_single_logical_stream_metadata(logical_stream_io(stream))
|
|
818
1142
|
end
|
|
819
1143
|
target_format = stream_metadatas.first.fetch(:format)
|
|
820
1144
|
return stream_interleaved_vorbis_logical_streams_mixed!(
|
|
821
1145
|
chained_streams,
|
|
822
1146
|
chunk_size: chunk_size,
|
|
823
|
-
decode_mode: decode_mode,
|
|
824
1147
|
target_format: target_format,
|
|
825
|
-
stream_metadatas: stream_metadatas,
|
|
1148
|
+
stream_metadatas: stream_metadatas,
|
|
1149
|
+
mix_strategy: mix_strategy,
|
|
1150
|
+
&block
|
|
826
1151
|
)
|
|
827
1152
|
end
|
|
828
1153
|
|
|
829
1154
|
stream_metadatas = chained_streams.map do |stream|
|
|
830
|
-
parse_single_logical_stream_metadata(
|
|
1155
|
+
parse_single_logical_stream_metadata(logical_stream_io(stream))
|
|
831
1156
|
end
|
|
832
1157
|
target_format = stream_metadatas.first.fetch(:format)
|
|
833
1158
|
same_sample_rate = stream_metadatas.all? { |metadata| metadata.fetch(:format).sample_rate == target_format.sample_rate }
|
|
834
1159
|
|
|
835
1160
|
unless same_sample_rate
|
|
836
|
-
chained_streams.each do |stream|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1161
|
+
chained_streams.zip(stream_metadatas).each do |stream, metadata|
|
|
1162
|
+
stream_vorbis_logical_stream_resampled!(
|
|
1163
|
+
stream,
|
|
1164
|
+
source_format: metadata.fetch(:format),
|
|
1165
|
+
target_format: target_format,
|
|
1166
|
+
chunk_size: chunk_size,
|
|
1167
|
+
&block
|
|
840
1168
|
)
|
|
841
|
-
each_sample_buffer_frame_slice(normalized, chunk_size, &block)
|
|
842
1169
|
end
|
|
843
1170
|
return true
|
|
844
1171
|
end
|
|
845
1172
|
|
|
846
1173
|
chained_format = nil
|
|
847
1174
|
chained_streams.each do |stream|
|
|
848
|
-
stream_read(
|
|
1175
|
+
stream_read(logical_stream_io(stream), chunk_size: chunk_size) do |chunk|
|
|
849
1176
|
chained_format ||= chunk.format
|
|
850
1177
|
yield(chunk.format == chained_format ? chunk : chunk.convert(chained_format))
|
|
851
1178
|
end
|
|
852
1179
|
end
|
|
853
1180
|
|
|
1181
|
+
true
|
|
1182
|
+
ensure
|
|
1183
|
+
close_logical_streams(chained_streams)
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
def stream_vorbis_logical_stream_resampled!(stream, source_format:, target_format:, chunk_size:)
|
|
1187
|
+
return enum_for(
|
|
1188
|
+
__method__,
|
|
1189
|
+
stream,
|
|
1190
|
+
source_format: source_format,
|
|
1191
|
+
target_format: target_format,
|
|
1192
|
+
chunk_size: chunk_size
|
|
1193
|
+
) unless block_given?
|
|
1194
|
+
|
|
1195
|
+
resampler = build_vorbis_streaming_linear_resampler_state(
|
|
1196
|
+
source_format: source_format,
|
|
1197
|
+
target_sample_rate: target_format.sample_rate
|
|
1198
|
+
)
|
|
1199
|
+
stream_read(logical_stream_io(stream), chunk_size: chunk_size) do |chunk|
|
|
1200
|
+
unless resampler
|
|
1201
|
+
yield(chunk.format == target_format ? chunk : chunk.convert(target_format))
|
|
1202
|
+
next
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
feed_vorbis_streaming_linear_resampler_chunk!(resampler, chunk)
|
|
1206
|
+
while (output = drain_vorbis_streaming_linear_resampler_chunk!(resampler, max_frames: chunk_size))
|
|
1207
|
+
yield(output.format == target_format ? output : output.convert(target_format))
|
|
1208
|
+
end
|
|
1209
|
+
end
|
|
1210
|
+
return true unless resampler
|
|
1211
|
+
|
|
1212
|
+
finish_vorbis_streaming_linear_resampler!(resampler)
|
|
1213
|
+
while (output = drain_vorbis_streaming_linear_resampler_chunk!(resampler, max_frames: chunk_size))
|
|
1214
|
+
yield(output.format == target_format ? output : output.convert(target_format))
|
|
1215
|
+
end
|
|
854
1216
|
true
|
|
855
1217
|
end
|
|
856
1218
|
|
|
@@ -873,7 +1235,7 @@ module Wavify
|
|
|
873
1235
|
end
|
|
874
1236
|
end
|
|
875
1237
|
|
|
876
|
-
def mix_vorbis_sample_buffers(buffers, target_format: nil)
|
|
1238
|
+
def mix_vorbis_sample_buffers(buffers, target_format: nil, strategy: :clip)
|
|
877
1239
|
buffers = Array(buffers)
|
|
878
1240
|
raise InvalidFormatError, "OGG Vorbis multi-stream decode did not produce any logical streams" if buffers.empty?
|
|
879
1241
|
|
|
@@ -895,7 +1257,7 @@ module Wavify
|
|
|
895
1257
|
mixed_samples[index] += sample.to_f
|
|
896
1258
|
end
|
|
897
1259
|
end
|
|
898
|
-
mixed_samples
|
|
1260
|
+
apply_vorbis_mix_strategy!(mixed_samples, strategy, source_count: converted.length)
|
|
899
1261
|
|
|
900
1262
|
mixed = Core::SampleBuffer.new(mixed_samples, work_format)
|
|
901
1263
|
return mixed if mixed.format == resolved_target_format
|
|
@@ -903,6 +1265,31 @@ module Wavify
|
|
|
903
1265
|
mixed.convert(resolved_target_format)
|
|
904
1266
|
end
|
|
905
1267
|
|
|
1268
|
+
def apply_vorbis_mix_strategy!(samples, strategy, source_count:)
|
|
1269
|
+
strategy = validate_vorbis_mix_strategy!(strategy)
|
|
1270
|
+
case strategy
|
|
1271
|
+
when :clip
|
|
1272
|
+
samples.map! { |sample| sample.clamp(-1.0, 1.0) }
|
|
1273
|
+
when :normalize
|
|
1274
|
+
peak = samples.map(&:abs).max.to_f
|
|
1275
|
+
samples.map! { |sample| sample / peak } if peak > 1.0
|
|
1276
|
+
when :headroom
|
|
1277
|
+
samples.map! { |sample| sample / source_count } if source_count > 1
|
|
1278
|
+
when :soft_limit
|
|
1279
|
+
samples.map! { |sample| Math.tanh(sample) }
|
|
1280
|
+
end
|
|
1281
|
+
samples
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
def validate_vorbis_mix_strategy!(strategy)
|
|
1285
|
+
value = strategy.to_sym
|
|
1286
|
+
return value if %i[clip normalize headroom soft_limit].include?(value)
|
|
1287
|
+
|
|
1288
|
+
raise InvalidParameterError, "mix_strategy must be :clip, :normalize, :headroom, or :soft_limit"
|
|
1289
|
+
rescue NoMethodError
|
|
1290
|
+
raise InvalidParameterError, "mix_strategy must be Symbol/String"
|
|
1291
|
+
end
|
|
1292
|
+
|
|
906
1293
|
def normalize_vorbis_logical_stream_buffer_for_target(buffer, target_format)
|
|
907
1294
|
raise InvalidParameterError, "buffer must be Core::SampleBuffer" unless buffer.is_a?(Core::SampleBuffer)
|
|
908
1295
|
raise InvalidParameterError, "target_format must be Core::Format" unless target_format.is_a?(Core::Format)
|
|
@@ -989,18 +1376,18 @@ module Wavify
|
|
|
989
1376
|
def stream_interleaved_vorbis_logical_streams_mixed!(
|
|
990
1377
|
chained_streams,
|
|
991
1378
|
chunk_size:,
|
|
992
|
-
decode_mode:,
|
|
993
1379
|
target_format: nil,
|
|
994
|
-
stream_metadatas: nil,
|
|
1380
|
+
stream_metadatas: nil,
|
|
1381
|
+
mix_strategy: :clip, &block
|
|
995
1382
|
)
|
|
996
1383
|
unless block_given?
|
|
997
1384
|
return enum_for(
|
|
998
1385
|
__method__,
|
|
999
1386
|
chained_streams,
|
|
1000
1387
|
chunk_size: chunk_size,
|
|
1001
|
-
decode_mode: decode_mode,
|
|
1002
1388
|
target_format: target_format,
|
|
1003
|
-
stream_metadatas: stream_metadatas
|
|
1389
|
+
stream_metadatas: stream_metadatas,
|
|
1390
|
+
mix_strategy: mix_strategy
|
|
1004
1391
|
)
|
|
1005
1392
|
end
|
|
1006
1393
|
|
|
@@ -1023,16 +1410,15 @@ module Wavify
|
|
|
1023
1410
|
stream_metadatas: metadatas,
|
|
1024
1411
|
target_format: resolved_target_format,
|
|
1025
1412
|
chunk_size: chunk_size,
|
|
1026
|
-
|
|
1413
|
+
mix_strategy: mix_strategy, &block
|
|
1027
1414
|
)
|
|
1028
1415
|
end
|
|
1029
1416
|
end
|
|
1030
1417
|
|
|
1031
1418
|
enumerators = streams.map do |stream|
|
|
1032
1419
|
stream_read(
|
|
1033
|
-
|
|
1034
|
-
chunk_size: chunk_size
|
|
1035
|
-
decode_mode: decode_mode
|
|
1420
|
+
logical_stream_io(stream),
|
|
1421
|
+
chunk_size: chunk_size
|
|
1036
1422
|
)
|
|
1037
1423
|
end
|
|
1038
1424
|
loop do
|
|
@@ -1044,7 +1430,7 @@ module Wavify
|
|
|
1044
1430
|
active_chunks = chunks.compact
|
|
1045
1431
|
break if active_chunks.empty?
|
|
1046
1432
|
|
|
1047
|
-
yield mix_vorbis_sample_buffers(active_chunks)
|
|
1433
|
+
yield mix_vorbis_sample_buffers(active_chunks, strategy: mix_strategy)
|
|
1048
1434
|
end
|
|
1049
1435
|
|
|
1050
1436
|
true
|
|
@@ -1055,7 +1441,7 @@ module Wavify
|
|
|
1055
1441
|
stream_metadatas:,
|
|
1056
1442
|
target_format:,
|
|
1057
1443
|
chunk_size:,
|
|
1058
|
-
|
|
1444
|
+
mix_strategy: :clip
|
|
1059
1445
|
)
|
|
1060
1446
|
unless block_given?
|
|
1061
1447
|
return enum_for(
|
|
@@ -1064,7 +1450,7 @@ module Wavify
|
|
|
1064
1450
|
stream_metadatas: stream_metadatas,
|
|
1065
1451
|
target_format: target_format,
|
|
1066
1452
|
chunk_size: chunk_size,
|
|
1067
|
-
|
|
1453
|
+
mix_strategy: mix_strategy
|
|
1068
1454
|
)
|
|
1069
1455
|
end
|
|
1070
1456
|
|
|
@@ -1077,9 +1463,8 @@ module Wavify
|
|
|
1077
1463
|
stream_states = streams.zip(metadatas).map do |stream, _metadata|
|
|
1078
1464
|
{
|
|
1079
1465
|
enumerator: stream_read(
|
|
1080
|
-
|
|
1081
|
-
chunk_size: chunk_size
|
|
1082
|
-
decode_mode: decode_mode
|
|
1466
|
+
logical_stream_io(stream),
|
|
1467
|
+
chunk_size: chunk_size
|
|
1083
1468
|
),
|
|
1084
1469
|
source_eof: false,
|
|
1085
1470
|
pending_samples: [],
|
|
@@ -1116,7 +1501,7 @@ module Wavify
|
|
|
1116
1501
|
mixed_inputs = stream_states.map do |stream_state|
|
|
1117
1502
|
take_vorbis_interleaved_stream_pending_chunk!(stream_state, frame_count: emit_frames)
|
|
1118
1503
|
end.compact
|
|
1119
|
-
yield mix_vorbis_sample_buffers(mixed_inputs, target_format: target_work_format)
|
|
1504
|
+
yield mix_vorbis_sample_buffers(mixed_inputs, target_format: target_work_format, strategy: mix_strategy)
|
|
1120
1505
|
end
|
|
1121
1506
|
|
|
1122
1507
|
true
|
|
@@ -1416,31 +1801,6 @@ module Wavify
|
|
|
1416
1801
|
:unknown
|
|
1417
1802
|
end
|
|
1418
1803
|
|
|
1419
|
-
# ---------------------------------------------------------------------------
|
|
1420
|
-
# IO helpers
|
|
1421
|
-
# ---------------------------------------------------------------------------
|
|
1422
|
-
|
|
1423
|
-
def open_input(io_or_path)
|
|
1424
|
-
return [io_or_path, false] if io_or_path.respond_to?(:read)
|
|
1425
|
-
raise InvalidParameterError, "input path must be String or IO: #{io_or_path.inspect}" unless io_or_path.is_a?(String)
|
|
1426
|
-
|
|
1427
|
-
[File.open(io_or_path, "rb"), true]
|
|
1428
|
-
rescue Errno::ENOENT
|
|
1429
|
-
raise InvalidFormatError, "input file not found: #{io_or_path}"
|
|
1430
|
-
end
|
|
1431
|
-
|
|
1432
|
-
def open_output(io_or_path)
|
|
1433
|
-
return [io_or_path, false] if io_or_path.respond_to?(:write)
|
|
1434
|
-
raise InvalidParameterError, "output path must be String or IO: #{io_or_path.inspect}" unless io_or_path.is_a?(String)
|
|
1435
|
-
|
|
1436
|
-
[File.open(io_or_path, "wb"), true]
|
|
1437
|
-
end
|
|
1438
|
-
|
|
1439
|
-
def ensure_seekable!(io)
|
|
1440
|
-
return if io.respond_to?(:seek) && io.respond_to?(:rewind)
|
|
1441
|
-
|
|
1442
|
-
raise StreamError, "OGG Vorbis codec requires seekable IO"
|
|
1443
|
-
end
|
|
1444
1804
|
end
|
|
1445
1805
|
end
|
|
1446
1806
|
end
|