libav-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ class AudioEncoder
5
+ CODECS = {
6
+ aac: %w[aac],
7
+ opus: %w[libopus opus],
8
+ pcm_f32le: %w[pcm_f32le]
9
+ }.freeze
10
+ CODEC_IDS = {
11
+ pcm_f32le: 65_557,
12
+ aac: 86_018,
13
+ opus: 86_076
14
+ }.freeze
15
+ SAMPLE_FORMATS = {
16
+ aac: "fltp",
17
+ opus: "flt",
18
+ pcm_f32le: "flt"
19
+ }.freeze
20
+ DEFAULT_FRAME_SIZE = 1_024
21
+
22
+ attr_reader :stream, :time_base, :sample_rate, :channels
23
+
24
+ def initialize(output, codec: :aac, sample_rate: 48_000, channels: 2,
25
+ bitrate: 192_000, **)
26
+ @output = output
27
+ @codec_name = codec.to_sym
28
+ @sample_rate = positive_integer(sample_rate, :sample_rate)
29
+ @channels = positive_integer(channels, :channels)
30
+ raise ArgumentError, "channels must be between 1 and 64" if @channels > 64
31
+
32
+ @time_base = Rational(1, @sample_rate)
33
+ @codec = find_encoder
34
+ @stream = output.new_stream(@codec)
35
+ @context = Native.codec.avcodec_alloc_context3(@codec)
36
+ raise EncodingError, "avcodec_alloc_context3 returned NULL" if @context.null?
37
+
38
+ configure_context(bitrate)
39
+ open_context
40
+ allocate_conversion
41
+ @sample_index = 0
42
+ rescue StandardError
43
+ close
44
+ raise
45
+ end
46
+
47
+ def push(pcm)
48
+ raise ClosedError, "audio encoder is closed" if @closed
49
+
50
+ bytes = normalize_pcm(pcm)
51
+ convert_to_fifo(bytes, bytes.bytesize / input_sample_bytes)
52
+ encode_complete_frames
53
+ self
54
+ end
55
+
56
+ def flush
57
+ return if @flushed || @closed
58
+
59
+ flush_resampler
60
+ encode_complete_frames
61
+ encode_fifo_tail
62
+ send_frame(nil)
63
+ @flushed = true
64
+ end
65
+
66
+ def close
67
+ return if @closed
68
+
69
+ Native.util.av_audio_fifo_free(@fifo) if @fifo && !@fifo.null?
70
+ free_swr
71
+ Native.util.av_channel_layout_uninit(@input_layout) if @input_layout
72
+ free_pointer(@frame, :av_frame_free, Native.util)
73
+ free_pointer(@packet, :av_packet_free, Native.codec)
74
+ free_pointer(@context, :avcodec_free_context, Native.codec)
75
+ @fifo = @frame = @packet = @context = @swr = nil
76
+ @closed = true
77
+ end
78
+
79
+ private
80
+
81
+ def configure_context(bitrate)
82
+ sample_format = Native.util.av_get_sample_fmt(SAMPLE_FORMATS.fetch(@codec_name))
83
+ Native::Layout.write_int(@context, :codec_context, :sample_rate, @sample_rate)
84
+ Native::Layout.write_int(@context, :codec_context, :sample_fmt, sample_format)
85
+ Native::Layout.write_int64(@context, :codec_context, :bit_rate, Integer(bitrate)) if bitrate
86
+ Native::Layout.write_rational(@context, :codec_context, :time_base, time_base)
87
+ Native.util.av_channel_layout_default(
88
+ Native::Layout.field_pointer(@context, :codec_context, :ch_layout), @channels
89
+ )
90
+ Native::Layout.write_rational(stream, :stream, :time_base, time_base)
91
+
92
+ return unless @output.global_header?
93
+
94
+ flags = Native::Layout.read_int(@context, :codec_context, :flags)
95
+ Native::Layout.write_int(
96
+ @context, :codec_context, :flags, flags | Native::AV_CODEC_FLAG_GLOBAL_HEADER
97
+ )
98
+ end
99
+
100
+ def open_context
101
+ Native.check(Native.codec.avcodec_open2(@context, @codec, nil), "avcodec_open2(audio)")
102
+ parameters = Native::Layout.read_pointer(stream, :stream, :codecpar)
103
+ Native.check(
104
+ Native.codec.avcodec_parameters_from_context(parameters, @context),
105
+ "avcodec_parameters_from_context(audio)"
106
+ )
107
+ reported_size = Native::Layout.read_int(@context, :codec_context, :frame_size)
108
+ @variable_frames = reported_size.zero?
109
+ @frame_capacity = @variable_frames ? DEFAULT_FRAME_SIZE : reported_size
110
+ end
111
+
112
+ def allocate_conversion
113
+ @frame = Native.util.av_frame_alloc
114
+ @packet = Native.codec.av_packet_alloc
115
+ raise EncodingError, "failed to allocate an audio AVFrame or AVPacket" if
116
+ @frame.null? || @packet.null?
117
+
118
+ @sample_format = Native::Layout.read_int(@context, :codec_context, :sample_fmt)
119
+ Native::Layout.write_int(@frame, :frame, :format, @sample_format)
120
+ Native::Layout.write_int(@frame, :frame, :sample_rate, @sample_rate)
121
+ Native::Layout.write_int(@frame, :frame, :nb_samples, @frame_capacity)
122
+ Native.util.av_channel_layout_default(
123
+ Native::Layout.field_pointer(@frame, :frame, :ch_layout), @channels
124
+ )
125
+ Native.check(Native.util.av_frame_get_buffer(@frame, 0), "av_frame_get_buffer(audio)")
126
+
127
+ @input_layout = FFI::MemoryPointer.new(:char, Native::AV_CHANNEL_LAYOUT_SIZE, true)
128
+ Native.util.av_channel_layout_default(@input_layout, @channels)
129
+ swr_holder = FFI::MemoryPointer.new(:pointer)
130
+ result = Native.resample.swr_alloc_set_opts2(
131
+ swr_holder,
132
+ Native::Layout.field_pointer(@context, :codec_context, :ch_layout),
133
+ @sample_format, @sample_rate,
134
+ @input_layout, Native::AV_SAMPLE_FMT_FLT, @sample_rate,
135
+ 0, nil
136
+ )
137
+ Native.check(result, "swr_alloc_set_opts2")
138
+ @swr = swr_holder.read_pointer
139
+ raise EncodingError, "swr_alloc_set_opts2 returned NULL" if @swr.null?
140
+
141
+ Native.check(Native.resample.swr_init(@swr), "swr_init")
142
+ @fifo = Native.util.av_audio_fifo_alloc(
143
+ @sample_format, @channels, @frame_capacity
144
+ )
145
+ raise EncodingError, "av_audio_fifo_alloc returned NULL" if @fifo.null?
146
+ end
147
+
148
+ def encode_complete_frames
149
+ while Native.util.av_audio_fifo_size(@fifo) >= @frame_capacity
150
+ encode_fifo_samples(@frame_capacity)
151
+ end
152
+ end
153
+
154
+ def encode_fifo_tail
155
+ samples = Native.util.av_audio_fifo_size(@fifo)
156
+ return if samples.zero?
157
+
158
+ encode_fifo_samples(samples, pad_to: @variable_frames ? samples : @frame_capacity)
159
+ end
160
+
161
+ def encode_fifo_samples(samples, pad_to: samples)
162
+ Native.check(Native.util.av_frame_make_writable(@frame), "av_frame_make_writable(audio)")
163
+ frame_data = Native::Layout.read_pointer(@frame, :frame, :extended_data)
164
+ read = Native.util.av_audio_fifo_read(@fifo, frame_data, samples)
165
+ Native.check(read, "av_audio_fifo_read")
166
+ raise EncodingError, "AVAudioFifo returned #{read} samples, expected #{samples}" unless
167
+ read == samples
168
+
169
+ if pad_to > samples
170
+ Native.check(
171
+ Native.util.av_samples_set_silence(
172
+ frame_data, samples, pad_to - samples, @channels, @sample_format
173
+ ),
174
+ "av_samples_set_silence"
175
+ )
176
+ end
177
+
178
+ Native::Layout.write_int(@frame, :frame, :nb_samples, pad_to)
179
+ Native::Layout.write_int64(@frame, :frame, :pts, @sample_index)
180
+ Native::Layout.write_int64(@frame, :frame, :duration, pad_to)
181
+ @sample_index += pad_to
182
+ send_frame(@frame)
183
+ end
184
+
185
+ def convert_to_fifo(bytes, samples)
186
+ source = FFI::MemoryPointer.from_string(bytes)
187
+ input_data = FFI::MemoryPointer.new(:pointer)
188
+ input_data.write_pointer(source)
189
+ capacity = Native.resample.swr_get_out_samples(@swr, samples)
190
+ Native.check(capacity, "swr_get_out_samples")
191
+ return if capacity.zero?
192
+
193
+ with_sample_buffer(capacity) do |output_data|
194
+ converted = Native.resample.swr_convert(
195
+ @swr, output_data, capacity, input_data, samples
196
+ )
197
+ Native.check(converted, "swr_convert")
198
+ write_fifo(output_data, converted)
199
+ end
200
+ end
201
+
202
+ def flush_resampler
203
+ loop do
204
+ capacity = Native.resample.swr_get_out_samples(@swr, 0)
205
+ Native.check(capacity, "swr_get_out_samples(flush)")
206
+ break if capacity.zero?
207
+
208
+ converted = with_sample_buffer(capacity) do |output_data|
209
+ result = Native.resample.swr_convert(@swr, output_data, capacity, nil, 0)
210
+ Native.check(result, "swr_convert(flush)")
211
+ write_fifo(output_data, result)
212
+ result
213
+ end
214
+ break if converted.zero?
215
+ end
216
+ end
217
+
218
+ def write_fifo(output_data, samples)
219
+ return if samples.zero?
220
+
221
+ required = Native.util.av_audio_fifo_size(@fifo) + samples
222
+ Native.check(Native.util.av_audio_fifo_realloc(@fifo, required), "av_audio_fifo_realloc")
223
+ written = Native.util.av_audio_fifo_write(@fifo, output_data, samples)
224
+ Native.check(written, "av_audio_fifo_write")
225
+ raise EncodingError, "AVAudioFifo wrote #{written} samples, expected #{samples}" unless
226
+ written == samples
227
+ end
228
+
229
+ def with_sample_buffer(samples)
230
+ holder = FFI::MemoryPointer.new(:pointer)
231
+ linesize = FFI::MemoryPointer.new(:int)
232
+ Native.check(
233
+ Native.util.av_samples_alloc_array_and_samples(
234
+ holder, linesize, @channels, samples, @sample_format, 0
235
+ ),
236
+ "av_samples_alloc_array_and_samples"
237
+ )
238
+ data = holder.read_pointer
239
+ yield data
240
+ ensure
241
+ Native.util.av_freep(data) if data && !data.null?
242
+ Native.util.av_freep(holder) if holder && !holder.read_pointer.null?
243
+ end
244
+
245
+ def send_frame(frame)
246
+ result = Native.codec.avcodec_send_frame(@context, frame)
247
+ drain_packets if result == Native::AVERROR_EAGAIN
248
+ result = Native.codec.avcodec_send_frame(@context, frame) if result == Native::AVERROR_EAGAIN
249
+ Native.check(result, "avcodec_send_frame(audio)")
250
+ drain_packets
251
+ end
252
+
253
+ def drain_packets
254
+ loop do
255
+ result = Native.codec.avcodec_receive_packet(@context, @packet)
256
+ break if Native.control_flow?(result)
257
+
258
+ Native.check(result, "avcodec_receive_packet(audio)")
259
+ begin
260
+ duration = Native::Layout.read_int64(@packet, :packet, :duration)
261
+ Native::Layout.write_int64(@packet, :packet, :duration, @frame_capacity) unless
262
+ duration.positive?
263
+ @output.write_packet(@packet, time_base, stream)
264
+ ensure
265
+ Native.codec.av_packet_unref(@packet)
266
+ end
267
+ end
268
+ end
269
+
270
+ def normalize_pcm(pcm)
271
+ bytes = if pcm.is_a?(Array)
272
+ pcm.pack("e*")
273
+ else
274
+ pcm.to_str.b
275
+ end
276
+ alignment = @channels * FFI.type_size(:float)
277
+ raise ArgumentError, "PCM data must contain complete #{@channels}-channel f32 samples" unless
278
+ (bytes.bytesize % alignment).zero?
279
+
280
+ bytes.dup
281
+ rescue NoMethodError, TypeError
282
+ raise ArgumentError, "PCM must be packed f32 data or an Array of samples"
283
+ end
284
+
285
+ def input_sample_bytes
286
+ @channels * FFI.type_size(:float)
287
+ end
288
+
289
+ def find_encoder
290
+ names = CODECS.fetch(@codec_name) do
291
+ raise ArgumentError, "unsupported audio codec: #{@codec_name.inspect}"
292
+ end
293
+ names.each do |name|
294
+ encoder = Native.codec.avcodec_find_encoder_by_name(name)
295
+ return encoder unless encoder.null?
296
+ end
297
+ encoder = Native.codec.avcodec_find_encoder(CODEC_IDS.fetch(@codec_name))
298
+ return encoder unless encoder.null?
299
+
300
+ raise EncodingError, "FFmpeg does not provide a #{names.join(" or ")} encoder"
301
+ end
302
+
303
+ def positive_integer(value, name)
304
+ value = Integer(value)
305
+ return value if value.positive?
306
+
307
+ raise ArgumentError, "#{name} must be positive"
308
+ end
309
+
310
+ def free_swr
311
+ return unless @swr && !@swr.null?
312
+
313
+ holder = FFI::MemoryPointer.new(:pointer)
314
+ holder.write_pointer(@swr)
315
+ Native.resample.swr_free(holder)
316
+ end
317
+
318
+ def free_pointer(pointer, function, library)
319
+ return unless pointer && !pointer.null?
320
+
321
+ holder = FFI::MemoryPointer.new(:pointer)
322
+ holder.write_pointer(pointer)
323
+ library.public_send(function, holder)
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ class AudioFrame
5
+ attr_reader :sample_rate, :channels, :samples, :data, :pts
6
+
7
+ def initialize(sample_rate:, channels:, samples:, data:, pts: nil)
8
+ @sample_rate = Integer(sample_rate)
9
+ @channels = Integer(channels)
10
+ @samples = Integer(samples)
11
+ @data = data.to_str
12
+ @data = @data.dup unless @data.encoding == Encoding::BINARY
13
+ @data.force_encoding(Encoding::BINARY)
14
+ @pts = pts
15
+ end
16
+
17
+ def to_str
18
+ data
19
+ end
20
+
21
+ def duration
22
+ Rational(samples, sample_rate)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ class AudioReader
5
+ attr_reader :sample_rate, :channels, :duration
6
+
7
+ def self.open(path)
8
+ reader = new(path)
9
+ return reader unless block_given?
10
+
11
+ begin
12
+ yield reader
13
+ ensure
14
+ reader.close
15
+ end
16
+ end
17
+
18
+ def initialize(path)
19
+ @path = File.path(path)
20
+ @format_holder = FFI::MemoryPointer.new(:pointer)
21
+ Native.check(
22
+ Native.format.avformat_open_input(@format_holder, @path, nil, nil),
23
+ "avformat_open_input"
24
+ )
25
+ @format_context = @format_holder.read_pointer
26
+ Native.check(
27
+ Native.format.avformat_find_stream_info(@format_context, nil),
28
+ "avformat_find_stream_info"
29
+ )
30
+ open_audio_stream
31
+ allocate_decode_state
32
+ load_metadata
33
+ reset_decode_state
34
+ rescue StandardError
35
+ close
36
+ raise
37
+ end
38
+
39
+ def read_frame
40
+ raise ClosedError, "audio reader is closed" if closed?
41
+
42
+ loop do
43
+ return unless receive_next_frame
44
+
45
+ frame = convert_frame
46
+ next if before_seek_target?(frame)
47
+
48
+ frame = trim_to_seek_target(frame)
49
+ next if frame.samples.zero?
50
+
51
+ @seek_target = nil
52
+ return frame
53
+ end
54
+ end
55
+
56
+ def read_samples
57
+ read_frame&.data
58
+ end
59
+
60
+ def each_frame
61
+ return enum_for(__method__) unless block_given?
62
+
63
+ while (frame = read_frame)
64
+ yield frame, frame.pts
65
+ end
66
+ self
67
+ end
68
+
69
+ def each_chunk
70
+ return enum_for(__method__) unless block_given?
71
+
72
+ each_frame { |frame, pts| yield frame.data, pts }
73
+ self
74
+ end
75
+
76
+ def seek(seconds)
77
+ raise ClosedError, "audio reader is closed" if closed?
78
+
79
+ seconds = Float(seconds)
80
+ raise ArgumentError, "seek time must be nonnegative" if seconds.negative? || !seconds.finite?
81
+
82
+ timestamp = (seconds / @time_base).round
83
+ Native.check(
84
+ Native.format.av_seek_frame(
85
+ @format_context, @stream_index, timestamp, Native::AVSEEK_FLAG_BACKWARD
86
+ ),
87
+ "av_seek_frame"
88
+ )
89
+ Native.codec.avcodec_flush_buffers(@codec_context)
90
+ Native.util.av_frame_unref(@frame)
91
+ Native.codec.av_packet_unref(@packet)
92
+ free_swr
93
+ @conversion_signature = nil
94
+ reset_decode_state
95
+ @seek_target = seconds
96
+ self
97
+ end
98
+
99
+ def close
100
+ return if closed?
101
+
102
+ free_swr
103
+ Native.util.av_channel_layout_uninit(@output_layout) if @output_layout
104
+ free_pointer(@frame, :av_frame_free, Native.util)
105
+ free_pointer(@packet, :av_packet_free, Native.codec)
106
+ free_pointer(@codec_context, :avcodec_free_context, Native.codec)
107
+ if @format_context && !@format_context.null?
108
+ @format_holder.write_pointer(@format_context)
109
+ Native.format.avformat_close_input(@format_holder)
110
+ end
111
+ @format_context = @codec_context = @frame = @packet = @swr = nil
112
+ @closed = true
113
+ nil
114
+ end
115
+
116
+ def closed?
117
+ @closed == true
118
+ end
119
+
120
+ private
121
+
122
+ def open_audio_stream
123
+ decoder_holder = FFI::MemoryPointer.new(:pointer)
124
+ @stream_index = Native.format.av_find_best_stream(
125
+ @format_context, Native::AVMEDIA_TYPE_AUDIO, -1, -1, decoder_holder, 0
126
+ )
127
+ Native.check(@stream_index, "av_find_best_stream(audio)")
128
+ decoder = decoder_holder.read_pointer
129
+ raise Error, "FFmpeg did not return an audio decoder" if decoder.null?
130
+
131
+ @stream = stream_at(@stream_index)
132
+ @codec_context = Native.codec.avcodec_alloc_context3(decoder)
133
+ raise Error, "avcodec_alloc_context3 returned NULL" if @codec_context.null?
134
+
135
+ parameters = Native::Layout.read_pointer(@stream, :stream, :codecpar)
136
+ Native.check(
137
+ Native.codec.avcodec_parameters_to_context(@codec_context, parameters),
138
+ "avcodec_parameters_to_context(audio)"
139
+ )
140
+ Native.check(
141
+ Native.codec.avcodec_open2(@codec_context, decoder, nil),
142
+ "avcodec_open2(audio)"
143
+ )
144
+ end
145
+
146
+ def allocate_decode_state
147
+ @frame = Native.util.av_frame_alloc
148
+ @packet = Native.codec.av_packet_alloc
149
+ raise Error, "failed to allocate an audio AVFrame or AVPacket" if
150
+ @frame.null? || @packet.null?
151
+
152
+ @destination_data = FFI::MemoryPointer.new(:pointer)
153
+ end
154
+
155
+ def load_metadata
156
+ @sample_rate = Native::Layout.read_int(@codec_context, :codec_context, :sample_rate)
157
+ context_layout = Native::Layout.field_pointer(@codec_context, :codec_context, :ch_layout)
158
+ @channels = context_layout.get_int32(4)
159
+ raise Error, "invalid audio metadata: #{@sample_rate} Hz, #{@channels} channels" unless
160
+ @sample_rate.positive? && @channels.positive?
161
+
162
+ @time_base = Native::Layout.read_rational(@stream, :stream, :time_base)
163
+ stream_duration = Native::Layout.read_int64(@stream, :stream, :duration)
164
+ @duration = if valid_timestamp?(stream_duration)
165
+ stream_duration * @time_base
166
+ else
167
+ format_duration
168
+ end
169
+ @output_layout = FFI::MemoryPointer.new(:char, Native::AV_CHANNEL_LAYOUT_SIZE, true)
170
+ Native.util.av_channel_layout_default(@output_layout, @channels)
171
+ end
172
+
173
+ def receive_next_frame
174
+ loop do
175
+ result = Native.codec.avcodec_receive_frame(@codec_context, @frame)
176
+ return true if result.zero?
177
+ return false if result == Native::AVERROR_EOF
178
+
179
+ Native.check(result, "avcodec_receive_frame(audio)") unless
180
+ result == Native::AVERROR_EAGAIN
181
+ return false unless feed_decoder
182
+ end
183
+ end
184
+
185
+ def feed_decoder
186
+ if @input_eof
187
+ return true if send_flush_packet
188
+
189
+ return false
190
+ end
191
+
192
+ loop do
193
+ result = Native.format.av_read_frame(@format_context, @packet)
194
+ if result == Native::AVERROR_EOF
195
+ @input_eof = true
196
+ return send_flush_packet
197
+ end
198
+ Native.check(result, "av_read_frame")
199
+
200
+ packet_stream = Native::Layout.read_int(@packet, :packet, :stream_index)
201
+ if packet_stream == @stream_index
202
+ begin
203
+ Native.check(
204
+ Native.codec.avcodec_send_packet(@codec_context, @packet),
205
+ "avcodec_send_packet(audio)"
206
+ )
207
+ ensure
208
+ Native.codec.av_packet_unref(@packet)
209
+ end
210
+ return true
211
+ end
212
+ Native.codec.av_packet_unref(@packet)
213
+ end
214
+ end
215
+
216
+ def send_flush_packet
217
+ return false if @flush_sent
218
+
219
+ result = Native.codec.avcodec_send_packet(@codec_context, nil)
220
+ Native.check(result, "avcodec_send_packet(audio flush)") unless result == Native::AVERROR_EOF
221
+ @flush_sent = true
222
+ true
223
+ end
224
+
225
+ def convert_frame
226
+ samples = Native::Layout.read_int(@frame, :frame, :nb_samples)
227
+ frame_format = Native::Layout.read_int(@frame, :frame, :format)
228
+ input_rate = Native::Layout.read_int(@frame, :frame, :sample_rate)
229
+ input_rate = @sample_rate unless input_rate.positive?
230
+ input_layout = Native::Layout.field_pointer(@frame, :frame, :ch_layout)
231
+ input_layout = Native::Layout.field_pointer(
232
+ @codec_context, :codec_context, :ch_layout
233
+ ) unless input_layout.get_int32(4).positive?
234
+ prepare_conversion(frame_format, input_rate, input_layout)
235
+
236
+ capacity = samples + Native.resample.swr_get_delay(@swr, input_rate)
237
+ capacity = samples if capacity < samples
238
+ destination = FFI::MemoryPointer.new(:float, capacity * @channels)
239
+ @destination_data.put_pointer(0, destination)
240
+ converted = Native.resample.swr_convert(
241
+ @swr, @destination_data, capacity,
242
+ Native::Layout.read_pointer(@frame, :frame, :extended_data), samples
243
+ )
244
+ Native.check(converted, "swr_convert(audio decode)")
245
+
246
+ timestamp = Native::Layout.read_int64(@frame, :frame, :best_effort_timestamp)
247
+ timestamp = Native::Layout.read_int64(@frame, :frame, :pts) unless valid_timestamp?(timestamp)
248
+ pts = valid_timestamp?(timestamp) ? timestamp * @time_base : nil
249
+ byte_size = converted * @channels * FFI.type_size(:float)
250
+ AudioFrame.new(
251
+ sample_rate: @sample_rate, channels: @channels, samples: converted,
252
+ data: destination.read_string_length(byte_size), pts:
253
+ )
254
+ end
255
+
256
+ def prepare_conversion(frame_format, input_rate, input_layout)
257
+ signature = [frame_format, input_rate, input_layout.get_int32(4)]
258
+ return if signature == @conversion_signature
259
+
260
+ free_swr
261
+ holder = FFI::MemoryPointer.new(:pointer)
262
+ Native.check(
263
+ Native.resample.swr_alloc_set_opts2(
264
+ holder,
265
+ @output_layout, Native::AV_SAMPLE_FMT_FLT, @sample_rate,
266
+ input_layout, frame_format, input_rate,
267
+ 0, nil
268
+ ),
269
+ "swr_alloc_set_opts2(audio decode)"
270
+ )
271
+ @swr = holder.read_pointer
272
+ raise Error, "swr_alloc_set_opts2 returned NULL" if @swr.null?
273
+
274
+ Native.check(Native.resample.swr_init(@swr), "swr_init(audio decode)")
275
+ @conversion_signature = signature
276
+ end
277
+
278
+ def before_seek_target?(frame)
279
+ return false unless @seek_target && frame.pts
280
+
281
+ frame.pts + frame.duration <= @seek_target
282
+ end
283
+
284
+ def trim_to_seek_target(frame)
285
+ return frame unless @seek_target && frame.pts && frame.pts < @seek_target
286
+
287
+ skipped_samples = ((@seek_target - frame.pts) * sample_rate).ceil
288
+ skipped_samples = [skipped_samples, frame.samples].min
289
+ remaining_samples = frame.samples - skipped_samples
290
+ byte_offset = skipped_samples * channels * FFI.type_size(:float)
291
+ AudioFrame.new(
292
+ sample_rate:, channels:, samples: remaining_samples,
293
+ data: frame.data.byteslice(byte_offset..),
294
+ pts: frame.pts + Rational(skipped_samples, sample_rate)
295
+ )
296
+ end
297
+
298
+ def reset_decode_state
299
+ @input_eof = false
300
+ @flush_sent = false
301
+ end
302
+
303
+ def stream_at(index)
304
+ streams = Native::Layout.read_pointer(@format_context, :format_context, :streams)
305
+ streams.get_pointer(index * FFI.type_size(:pointer))
306
+ end
307
+
308
+ def format_duration
309
+ value = Native::Layout.read_int64(@format_context, :format_context, :duration)
310
+ return unless valid_timestamp?(value)
311
+
312
+ Rational(value, Native::AV_TIME_BASE)
313
+ end
314
+
315
+ def valid_timestamp?(value)
316
+ value && value != Native::AV_NOPTS_VALUE && value >= 0
317
+ end
318
+
319
+ def free_swr
320
+ return unless @swr && !@swr.null?
321
+
322
+ holder = FFI::MemoryPointer.new(:pointer)
323
+ holder.write_pointer(@swr)
324
+ Native.resample.swr_free(holder)
325
+ @swr = nil
326
+ end
327
+
328
+ def free_pointer(pointer, function, library)
329
+ return unless pointer && !pointer.null?
330
+
331
+ holder = FFI::MemoryPointer.new(:pointer)
332
+ holder.write_pointer(pointer)
333
+ library.public_send(function, holder)
334
+ end
335
+ end
336
+ end