Class: Wavify::Core::Stream
- Inherits:
-
Object
- Object
- Wavify::Core::Stream
- Includes:
- Enumerable, Enumerable[SampleBuffer]
- Defined in:
- lib/wavify/core/stream.rb,
sig/stream.rbs
Overview
Lazy streaming pipeline for chunk-based audio processing.
Instances are typically created via Audio.stream.
Defined Under Namespace
Classes: MeterProcessor, ProgressProcessor, UserCodeError
Instance Attribute Summary collapse
-
#chunk_size ⇒ Integer
readonly
Returns the value of attribute chunk_size.
Instance Method Summary collapse
-
#drop_duration(duration) ⇒ Stream
Drops an initial duration from the source stream.
-
#dry_run(format: nil) ⇒ Hash
Reads and processes the stream without writing output.
-
#each_chunk {|chunk| ... } ⇒ Enumerator
(also: #each)
Iterates processed chunks.
-
#format ⇒ Format
Returns a known source format.
-
#initialize(source, codec:, format:, chunk_size: 4096, codec_read_options: {}) ⇒ Stream
constructor
A new instance of Stream.
-
#latency ⇒ Float
Summed processor latency in seconds.
-
#lookahead ⇒ Float
Summed processor lookahead in seconds.
-
#map_chunks(name: nil) {|arg0| ... } ⇒ Stream
Adds a block processor for chunk-to-chunk transforms.
-
#meter {|stats| ... } ⇒ Stream
Installs a passive per-chunk level meter.
-
#pipe(processor = nil, name: nil, &block) ⇒ Stream
Adds a processor to the stream pipeline.
-
#pipeline ⇒ Array<Object>
Registered processors in execution order.
-
#pipeline_steps ⇒ Array<Hash>
Processor names and objects in execution order.
-
#progress(total_frames: nil) {|stats| ... } ⇒ Stream
Installs a passive progress callback based on processed output chunks.
-
#take_duration(duration) ⇒ Stream
Limits the source stream to the first duration after any dropped prefix.
-
#tee(path_or_io, format: nil, codec: nil, filename: nil, codec_options: nil) ⇒ Stream
Writes processed chunks to an additional output while the stream is consumed.
-
#to_audio ⇒ Audio
Materializes the processed stream into an Audio object.
-
#write_to(path_or_io, format: nil, codec: nil, filename: nil, codec_options: nil, overwrite: true) ⇒ String, IO
Writes the processed stream to a path or writable IO.
Constructor Details
#initialize(source, codec:, format:, chunk_size: 4096, codec_read_options: {}) ⇒ Stream
Returns a new instance of Stream.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/wavify/core/stream.rb', line 20 def initialize(source, codec:, format:, chunk_size: 4096, codec_read_options: {}) @source = source @codec = codec @format = format @chunk_size = validate_chunk_size!(chunk_size) @codec_read_options = () @pipeline = [] @pipeline_names = [] @tee_targets = [] @take_duration_seconds = nil @drop_duration_seconds = nil @enumerated = false @source_start_position = source_position(source) @stage_input_formats = [] end |
Instance Attribute Details
#chunk_size ⇒ Integer (readonly)
Returns the value of attribute chunk_size.
13 14 15 |
# File 'lib/wavify/core/stream.rb', line 13 def chunk_size @chunk_size end |
Instance Method Details
#drop_duration(duration) ⇒ Stream
Drops an initial duration from the source stream.
96 97 98 99 |
# File 'lib/wavify/core/stream.rb', line 96 def drop_duration(duration) @drop_duration_seconds = coerce_duration_seconds!(duration, "duration") self end |
#dry_run(format: nil) ⇒ Hash
Reads and processes the stream without writing output. User processors and callbacks are still executed and may have side effects.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/wavify/core/stream.rb', line 197 def dry_run(format: nil) raise InvalidFormatError, "format must be Core::Format" if format && !format.is_a?(Format) tee_targets = @tee_targets @tee_targets = [] stats = { chunks: 0, sample_frame_count: 0, format: format, pipeline: pipeline_steps, latency: latency, lookahead: lookahead, tail_duration: pipeline_tail_duration } each_chunk do |chunk| output_chunk = format ? chunk.convert(format) : chunk stats[:chunks] += 1 stats[:sample_frame_count] += output_chunk.sample_frame_count stats[:format] ||= output_chunk.format end stats[:duration] = stats[:format] ? Duration.from_samples(stats[:sample_frame_count], stats[:format].sample_rate) : nil stats ensure @tee_targets = tee_targets if defined?(tee_targets) end |
#each_chunk ⇒ Stream #each_chunk ⇒ Enumerator[SampleBuffer, Stream] Also known as: each
Iterates processed chunks.
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/wavify/core/stream.rb', line 229 def each_chunk return enum_for(:each_chunk) unless block_given? prepare_source_for_enumeration! reset_pipeline! @last_output_format = nil drop_frames = nil take_frames = nil input_taken_frames = 0 output_drop_frames = nil output_take_frames = nil output_taken_frames = 0 @stage_input_formats = [] with_tee_writers do |tee_writers| with_stream_context("stream read", codec: @codec, target: @source) do take_complete = Object.new catch(take_complete) do @codec.stream_read(@source, chunk_size: @chunk_size, **@codec_read_options) do |chunk| with_stream_context("stream processing", codec: @codec, target: @source) do @format ||= chunk.format drop_frames ||= duration_frames(@drop_duration_seconds, chunk.format) || 0 take_frames = duration_frames(@take_duration_seconds, chunk.format) if take_frames.nil? && @take_duration_seconds input_chunk, drop_frames, input_taken_frames = apply_duration_window( chunk, drop_frames: drop_frames, take_frames: take_frames, taken_frames: input_taken_frames ) throw(take_complete) if take_frames && input_taken_frames >= take_frames && input_chunk.nil? next unless input_chunk output_chunk = apply_pipeline(input_chunk) output_drop_frames, output_take_frames, output_taken_frames = emit_output_chunk( output_chunk, tee_writers: tee_writers, drop_frames: output_drop_frames, take_frames: output_take_frames, taken_frames: output_taken_frames ) { |windowed| yield windowed } throw(take_complete) if take_frames && input_taken_frames >= take_frames end end end end with_stream_context("stream flush", codec: @codec, target: @source) do flush_pipeline do |chunk| output_drop_frames, output_take_frames, output_taken_frames = emit_output_chunk( chunk, tee_writers: tee_writers, drop_frames: output_drop_frames, take_frames: output_take_frames, taken_frames: output_taken_frames ) { |windowed| yield windowed } end end end rescue UserCodeError => e raise e.original end |
#format ⇒ Format
Returns a known source format. Access before enumeration may perform a metadata probe; normal decoding learns the format from the first chunk.
38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/wavify/core/stream.rb', line 38 def format return @format if @format position = source_position(@source) = @codec.(@source, **) @format = .fetch(:format) ensure if position && @source.respond_to?(:seek) @source.seek(position, IO::SEEK_SET) end end |
#latency ⇒ Float
Returns summed processor latency in seconds.
183 184 185 |
# File 'lib/wavify/core/stream.rb', line 183 def latency @pipeline.sum { |processor| processor_duration(processor, :latency) } end |
#lookahead ⇒ Float
Returns summed processor lookahead in seconds.
188 189 190 |
# File 'lib/wavify/core/stream.rb', line 188 def lookahead @pipeline.sum { |processor| processor_duration(processor, :lookahead) } end |
#map_chunks(name: nil) {|arg0| ... } ⇒ Stream
Adds a block processor for chunk-to-chunk transforms.
77 78 79 80 81 |
# File 'lib/wavify/core/stream.rb', line 77 def map_chunks(name: nil, &block) raise InvalidParameterError, "map_chunks requires a block" unless block pipe(block, name: name || :map_chunks) end |
#meter {|stats| ... } ⇒ Stream
Installs a passive per-chunk level meter.
106 107 108 109 110 |
# File 'lib/wavify/core/stream.rb', line 106 def meter(&block) raise InvalidParameterError, "meter requires a block" unless block pipe(MeterProcessor.new(block), name: :meter) end |
#pipe(processor = nil, name: nil, &block) ⇒ Stream
Adds a processor to the stream pipeline.
Processors may respond to #process, #call, or #apply.
Stateful processors may also expose #reset and #flush.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/wavify/core/stream.rb', line 58 def pipe(processor = nil, name: nil, &block) if processor && block raise InvalidParameterError, "pipe accepts either a processor or a block, not both" end candidate = processor || block unless candidate.respond_to?(:call) || candidate.respond_to?(:process) || candidate.respond_to?(:apply) raise InvalidParameterError, "processor must respond to :call, :process, or :apply" end @pipeline << candidate @pipeline_names << validate_pipeline_name!(name) self end |
#pipeline ⇒ Array<Object>
Returns registered processors in execution order.
165 166 167 |
# File 'lib/wavify/core/stream.rb', line 165 def pipeline @pipeline.dup end |
#pipeline_steps ⇒ Array<Hash>
Returns processor names and objects in execution order.
170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/wavify/core/stream.rb', line 170 def pipeline_steps @pipeline.map.with_index do |processor, index| { name: @pipeline_names.fetch(index), processor: processor, latency: processor_duration(processor, :latency), lookahead: processor_duration(processor, :lookahead), tail_duration: processor_duration(processor, :tail_duration) } end end |
#progress(total_frames: nil) {|stats| ... } ⇒ Stream
Installs a passive progress callback based on processed output chunks.
118 119 120 121 122 123 124 125 |
# File 'lib/wavify/core/stream.rb', line 118 def progress(total_frames: nil, &block) raise InvalidParameterError, "progress requires a block" unless block if total_frames && (!total_frames.is_a?(Integer) || total_frames.negative?) raise InvalidParameterError, "total_frames must be a non-negative Integer" end pipe(ProgressProcessor.new(block, total_frames: total_frames), name: :progress) end |
#take_duration(duration) ⇒ Stream
Limits the source stream to the first duration after any dropped prefix.
87 88 89 90 |
# File 'lib/wavify/core/stream.rb', line 87 def take_duration(duration) @take_duration_seconds = coerce_duration_seconds!(duration, "duration") self end |
#tee(path_or_io, format: nil, codec: nil, filename: nil, codec_options: nil) ⇒ Stream
Writes processed chunks to an additional output while the stream is consumed.
133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/wavify/core/stream.rb', line 133 def tee(path_or_io, format: nil, codec: nil, filename: nil, codec_options: nil) output_codec = detect_output_codec(path_or_io, codec: codec, filename: filename) target_format = resolve_target_format(format, output_codec) raise InvalidFormatError, "format is required when teeing stream output" unless target_format.is_a?(Format) @tee_targets << { target: path_or_io, codec: output_codec, format: target_format, codec_options: (, "codec_options") } self end |
#to_audio ⇒ Audio
Materializes the processed stream into an Audio object.
150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/wavify/core/stream.rb', line 150 def to_audio output_format = nil samples = [] each_chunk do |chunk| output_format ||= chunk.format converted = chunk.format == output_format ? chunk : chunk.convert(output_format) samples.concat(converted.samples) end output_format ||= @last_output_format || @format raise InvalidFormatError, "stream format is unknown" unless output_format.is_a?(Format) Audio.new(SampleBuffer.new(samples, output_format)) end |
#write_to(path_or_io, format: nil, codec: nil, filename: nil, codec_options: nil, overwrite: true) ⇒ String, IO
Writes the processed stream to a path or writable IO.
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/wavify/core/stream.rb', line 302 def write_to(path_or_io, format: nil, codec: nil, filename: nil, codec_options: nil, overwrite: true) validate_overwrite!(path_or_io, overwrite) output_codec = detect_output_codec(path_or_io, codec: codec, filename: filename) target_format = resolve_target_format(format, output_codec) = (, "codec_options") with_output_target(path_or_io, overwrite: overwrite) do |target| with_stream_context("stream write", codec: output_codec, target: path_or_io) do output_codec.stream_write(target, format: target_format, **) do |writer| each_chunk do |chunk| output_chunk = target_format ? chunk.convert(target_format) : chunk with_stream_context("stream write", codec: output_codec, target: path_or_io) do writer.call(output_chunk) end end end end end path_or_io end |