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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +256 -0
- data/Rakefile +15 -0
- data/examples/frames_to_mp4.rb +23 -0
- data/examples/stagecraft_offscreen_to_mp4.rb +47 -0
- data/examples/vizcore_scene_to_mp4.rb +40 -0
- data/lib/libav/audio_encoder.rb +326 -0
- data/lib/libav/audio_frame.rb +25 -0
- data/lib/libav/audio_reader.rb +336 -0
- data/lib/libav/audio_writer.rb +87 -0
- data/lib/libav/errors.rb +20 -0
- data/lib/libav/native/layout.rb +60 -0
- data/lib/libav/native/v60/layout.rb +30 -0
- data/lib/libav/native/v61/layout.rb +30 -0
- data/lib/libav/native.rb +311 -0
- data/lib/libav/output_context.rb +95 -0
- data/lib/libav/texel_support.rb +24 -0
- data/lib/libav/version.rb +7 -0
- data/lib/libav/video_encoder.rb +257 -0
- data/lib/libav/video_frame.rb +24 -0
- data/lib/libav/video_reader.rb +333 -0
- data/lib/libav/video_writer.rb +213 -0
- data/lib/libav.rb +35 -0
- data/native/abi_probe.c +72 -0
- data/native/generate_layout.rb +103 -0
- metadata +82 -0
|
@@ -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
|