Skip to content

Commit 4925a6a

Browse files
committed
Decode audio in the Blocks API, as RawAudioSamples
PacketDecoder now follows the demuxer it was built from: RawFrames for a VideoDemuxer, RawAudioSamples for an AudioDemuxer. Decoding is the same avcodec_send_packet / avcodec_receive_frame pair either way, so this stays one block rather than two. RawAudioSamples carries the samples in the codec's own sample type (uint8 for u8, int16 for s16, float32 for flt...), always as a contiguous [num_channels, num_samples] tensor. That is a copy, not a view: planar formats put each channel in its own allocation and packed ones interleave them, so neither is that shape as it stands, and an audio frame is only a few kB. Normalizing to [-1, 1] is the converter's job, not the decoder's. Audio is CPU-only, so a non-CPU device raises rather than being silently ignored, and an unspecified one doesn't inherit a non-CPU default device.
1 parent 7a07432 commit 4925a6a

13 files changed

Lines changed: 472 additions & 26 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// All rights reserved.
3+
//
4+
// This source code is licensed under the BSD-style license found in the
5+
// LICENSE file in the root directory of this source tree.
6+
7+
#include "AudioCommon.h"
8+
9+
namespace facebook::torchcodec {
10+
11+
torch::headeronly::ScalarType sample_format_dtype(
12+
AVSampleFormat sample_format) {
13+
switch (av_get_packed_sample_fmt(sample_format)) {
14+
case AV_SAMPLE_FMT_U8:
15+
return kStableUInt8;
16+
case AV_SAMPLE_FMT_S16:
17+
return kStableInt16;
18+
case AV_SAMPLE_FMT_S32:
19+
return kStableInt32;
20+
case AV_SAMPLE_FMT_S64:
21+
return kStableInt64;
22+
case AV_SAMPLE_FMT_FLT:
23+
return kStableFloat32;
24+
case AV_SAMPLE_FMT_DBL:
25+
return kStableFloat64;
26+
default:
27+
break;
28+
}
29+
const char* name = av_get_sample_fmt_name(sample_format);
30+
STD_TORCH_CHECK(
31+
false,
32+
"Unsupported sample format '",
33+
name == nullptr ? "unknown" : name,
34+
"'.");
35+
return kStableUInt8;
36+
}
37+
38+
} // namespace facebook::torchcodec

src/torchcodec/_core/AudioCommon.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// All rights reserved.
3+
//
4+
// This source code is licensed under the BSD-style license found in the
5+
// LICENSE file in the root directory of this source tree.
6+
7+
#pragma once
8+
9+
#include "FFMPEGCommon.h"
10+
#include "StableABICompat.h"
11+
12+
// Where FFmpeg's audio samples meet torch tensors. FFMPEGCommon deliberately
13+
// knows nothing about tensors, and these helpers are shared by the decode, the
14+
// conversion and the SingleStreamDecoder paths, so they live on their own.
15+
16+
namespace facebook::torchcodec {
17+
18+
// The dtype that holds `sample_format`'s samples exactly. Planar and packed
19+
// variants of a format share a sample type, which is why this doesn't care
20+
// which one it is given.
21+
torch::headeronly::ScalarType sample_format_dtype(AVSampleFormat sample_format);
22+
23+
} // namespace facebook::torchcodec

src/torchcodec/_core/PacketDecoder.cpp

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
#include "PacketDecoder.h"
88

9+
#include "AudioCommon.h"
10+
911
#include <algorithm>
12+
#include <cstring>
1013

1114
namespace facebook::torchcodec {
1215

@@ -57,26 +60,49 @@ const AVCodec* find_decoder(
5760
PacketDecoder::PacketDecoder(
5861
const Demuxer& demuxer,
5962
const StableDevice& device,
60-
std::optional<int> ffmpeg_thread_count) {
63+
std::optional<int> ffmpeg_thread_count)
64+
: media_type_(demuxer.media_type()) {
65+
bool is_audio = media_type_ == AVMEDIA_TYPE_AUDIO;
66+
STD_TORCH_CHECK(
67+
!is_audio || device.type() == kStableCPU,
68+
"Audio can only be decoded on the CPU.");
69+
6170
device_interface_ = create_device_interface(device);
6271
STD_TORCH_CHECK(
6372
device_interface_ != nullptr,
6473
"Failed to create device interface. This should never happen, please report.");
6574

6675
AVStream* stream = demuxer.active_stream();
6776
time_base_ = stream->time_base;
68-
is_mpeg_ps_ =
69-
std::string_view(demuxer.format_context()->iformat->name) == "mpeg";
70-
if (const int32_t* matrix = get_display_matrix_from_stream(stream)) {
71-
display_matrix_.emplace();
72-
std::copy(
73-
matrix, matrix + display_matrix_->size(), display_matrix_->begin());
77+
78+
if (is_audio) {
79+
// Audio codecs are hardcoded to a single FFmpeg thread, see
80+
// https://github.com/pytorch/torchcodec/issues/1253.
81+
ffmpeg_thread_count = 1;
82+
} else {
83+
is_mpeg_ps_ =
84+
std::string_view(demuxer.format_context()->iformat->name) == "mpeg";
85+
if (const int32_t* matrix = get_display_matrix_from_stream(stream)) {
86+
display_matrix_.emplace();
87+
std::copy(
88+
matrix, matrix + display_matrix_->size(), display_matrix_->begin());
89+
}
7490
}
91+
7592
const AVCodec* av_codec = find_decoder(stream, device_interface_.get());
7693
codec_context_ = create_and_open_codec_context(
7794
stream, av_codec, device_interface_.get(), ffmpeg_thread_count);
7895
device_interface_->initialize(codec_context_);
7996

97+
if (is_audio) {
98+
// Nothing else to set up: unlike video, we hand out the samples in the
99+
// codec's own format, so no conversion state is needed here. Note we
100+
// deliberately do NOT set request_sample_fmt: what SingleStreamDecoder
101+
// asks for (FLTP) is an optimization for its own conversion, and here it
102+
// would hide what the codec natively produces.
103+
return;
104+
}
105+
80106
const AVPixFmtDescriptor* stream_desc =
81107
av_pix_fmt_desc_get(codec_context_->pix_fmt);
82108
int stream_bit_depth = stream_desc ? stream_desc->comp[0].depth : 8;
@@ -133,10 +159,12 @@ int PacketDecoder::receive_frame(UniqueAVFrame& av_frame) {
133159
int status = device_interface_->receive_frame(av_frame);
134160
if (status == AVSUCCESS) {
135161
device_interface_->make_frame_standalone(av_frame);
136-
// Attach a copy of the display matrix to the frame, so the ColorConverter
137-
// can use it.
138-
set_display_matrix_on_frame(
139-
*av_frame, display_matrix_ ? display_matrix_->data() : nullptr);
162+
if (media_type_ == AVMEDIA_TYPE_VIDEO) {
163+
// Attach a copy of the display matrix to the frame, so the ColorConverter
164+
// can use it.
165+
set_display_matrix_on_frame(
166+
*av_frame, display_matrix_ ? display_matrix_->data() : nullptr);
167+
}
140168
}
141169
return status;
142170
}
@@ -240,4 +268,74 @@ std::vector<torch::stable::Tensor> frame_planes(
240268
return planes;
241269
}
242270

271+
namespace {
272+
// Scatters `num_channels`-interleaved samples into one contiguous row per
273+
// channel. Templated on an integer of the right width rather than the actual
274+
// sample type: we're only moving bytes around, so all that matters is size.
275+
template <typename T>
276+
void deinterleave(
277+
const uint8_t* src,
278+
uint8_t* dst,
279+
int num_channels,
280+
int num_samples) {
281+
const T* in = reinterpret_cast<const T*>(src);
282+
T* out = reinterpret_cast<T*>(dst);
283+
for (int channel = 0; channel < num_channels; ++channel) {
284+
T* row = out + static_cast<int64_t>(channel) * num_samples;
285+
for (int sample = 0; sample < num_samples; ++sample) {
286+
row[sample] = in[static_cast<int64_t>(sample) * num_channels + channel];
287+
}
288+
}
289+
}
290+
} // namespace
291+
292+
torch::stable::Tensor audio_samples(const AVFrame& av_frame) {
293+
auto sample_format = static_cast<AVSampleFormat>(av_frame.format);
294+
int num_channels = get_num_channels(av_frame);
295+
int64_t num_samples = av_frame.nb_samples;
296+
297+
torch::stable::Tensor samples = torch::stable::empty(
298+
{num_channels, num_samples}, sample_format_dtype(sample_format));
299+
if (num_samples == 0) {
300+
return samples;
301+
}
302+
303+
int bytes_per_sample = av_get_bytes_per_sample(sample_format);
304+
auto* dst = static_cast<uint8_t*>(samples.mutable_data_ptr());
305+
int64_t bytes_per_channel = num_samples * bytes_per_sample;
306+
307+
if (av_sample_fmt_is_planar(sample_format)) {
308+
for (int channel = 0; channel < num_channels; ++channel) {
309+
// extended_data rather than data: the latter only holds
310+
// AV_NUM_DATA_POINTERS (8) pointers, and we support more channels.
311+
std::memcpy(
312+
dst + channel * bytes_per_channel,
313+
av_frame.extended_data[channel],
314+
bytes_per_channel);
315+
}
316+
return samples;
317+
}
318+
319+
const uint8_t* src = av_frame.extended_data[0];
320+
int num_samples_int = static_cast<int>(num_samples);
321+
switch (bytes_per_sample) {
322+
case 1:
323+
deinterleave<uint8_t>(src, dst, num_channels, num_samples_int);
324+
break;
325+
case 2:
326+
deinterleave<uint16_t>(src, dst, num_channels, num_samples_int);
327+
break;
328+
case 4:
329+
deinterleave<uint32_t>(src, dst, num_channels, num_samples_int);
330+
break;
331+
case 8:
332+
deinterleave<uint64_t>(src, dst, num_channels, num_samples_int);
333+
break;
334+
default:
335+
STD_TORCH_CHECK(
336+
false, "Unexpected sample width: ", bytes_per_sample, " bytes.");
337+
}
338+
return samples;
339+
}
340+
243341
} // namespace facebook::torchcodec

src/torchcodec/_core/PacketDecoder.h

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ SharedAVCodecContext create_and_open_codec_context(
2929
DeviceInterface* device_interface,
3030
std::optional<int> thread_count);
3131

32-
// Decode building block: turns compressed packets into decoded (YUV) frames.
33-
// Configured from a Demuxer's active stream; stateful. Not thread-safe.
32+
// Decode building block: turns compressed packets into decoded frames - (YUV)
33+
// pictures for a video stream, samples in the codec's own format for an audio
34+
// one. Configured from a Demuxer's active stream; stateful. Not thread-safe.
3435
class FORCE_PUBLIC_VISIBILITY PacketDecoder {
3536
public:
3637
explicit PacketDecoder(
@@ -67,10 +68,15 @@ class FORCE_PUBLIC_VISIBILITY PacketDecoder {
6768
return time_base_;
6869
}
6970

71+
AVMediaType media_type() const {
72+
return media_type_;
73+
}
74+
7075
private:
7176
std::unique_ptr<DeviceInterface> device_interface_;
7277
SharedAVCodecContext codec_context_;
7378
AVRational time_base_ = {};
79+
AVMediaType media_type_ = AVMEDIA_TYPE_VIDEO;
7480
// Stamped onto every frame we hand out, so downstream blocks can read the
7581
// rotation off the frame itself instead of knowing about the stream. Held by
7682
// value: we're only handed the Demuxer at construction and it may well be
@@ -111,4 +117,14 @@ FORCE_PUBLIC_VISIBILITY std::vector<torch::stable::Tensor> frame_planes(
111117
const StableDevice& device,
112118
const torch::stable::Tensor& tensor_handle);
113119

120+
// A decoded audio frame's samples as a contiguous [num_channels, num_samples]
121+
// tensor whose dtype is the frame's own sample type: uint8 for u8, int16 for
122+
// s16, float32 for flt, and so on, planar or not. This is a copy rather than a
123+
// view: planar formats put each channel in its own allocation and packed ones
124+
// interleave them, so neither is a [C, N] tensor as it stands. An audio frame
125+
// is a few kB, so normalizing here buys a uniform layout for the price of a
126+
// memcpy - and it means a converter can treat the result as planar-of-dtype.
127+
FORCE_PUBLIC_VISIBILITY torch::stable::Tensor audio_samples(
128+
const AVFrame& av_frame);
129+
114130
} // namespace facebook::torchcodec

src/torchcodec/_core/StableABICompat.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ constexpr auto kStableXPU = torch::headeronly::DeviceType::XPU;
6363
// Scalar type constants
6464
constexpr auto kStableUInt8 = torch::headeronly::ScalarType::Byte;
6565
constexpr auto kStableUInt16 = torch::headeronly::ScalarType::UInt16;
66+
constexpr auto kStableInt16 = torch::headeronly::ScalarType::Short;
6667
constexpr auto kStableInt32 = torch::headeronly::ScalarType::Int;
6768
constexpr auto kStableInt64 = torch::headeronly::ScalarType::Long;
6869
constexpr auto kStableFloat32 = torch::headeronly::ScalarType::Float;

src/torchcodec/_core/_ffmpeg_op_names.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"_blocks_packet_decoder_send_eof",
4242
"_blocks_packet_decoder_reset",
4343
"_blocks_packet_decoder_receive_frame",
44+
"_blocks_audio_packet_decoder_receive_frame",
4445
"_blocks_create_color_converter",
4546
"_blocks_convert_frame",
4647
"_blocks_frame_metadata",

src/torchcodec/_core/_ffmpeg_ops.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ def add_video_stream(
121121
_blocks_packet_decoder_receive_frame = (
122122
torch.ops.torchcodec_ns._blocks_packet_decoder_receive_frame.default
123123
)
124+
_blocks_audio_packet_decoder_receive_frame = (
125+
torch.ops.torchcodec_ns._blocks_audio_packet_decoder_receive_frame.default
126+
)
124127
_blocks_create_color_converter = (
125128
torch.ops.torchcodec_ns._blocks_create_color_converter.default
126129
)

src/torchcodec/_core/custom_ops.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ STABLE_TORCH_LIBRARY_FRAGMENT(torchcodec_ns, m) {
9191
m.def("_blocks_packet_decoder_reset(Tensor(a!) decoder) -> ()");
9292
m.def(
9393
"_blocks_packet_decoder_receive_frame(Tensor(a!) decoder) -> (Tensor, int, float, float, Device, Tensor)");
94+
m.def(
95+
"_blocks_audio_packet_decoder_receive_frame(Tensor(a!) decoder) -> (Tensor, int, float, float, int, str)");
9496
m.def(
9597
"_blocks_create_color_converter(str device=\"cpu\", str output_dtype=\"uint8\") -> Tensor");
9698
m.def(
@@ -978,6 +980,46 @@ OpsReceiveFrameOutput _blocks_packet_decoder_receive_frame(
978980
storage);
979981
}
980982

983+
// (samples, status, pts_seconds, duration_seconds, sample_rate,
984+
// sample_format). `samples` is [num_channels, num_samples] in the frame's own
985+
// sample type; there is no frame handle because, unlike a video frame, nothing
986+
// downstream needs the AVFrame itself.
987+
using OpsReceiveAudioFrameOutput = std::
988+
tuple<torch::stable::Tensor, int64_t, double, double, int64_t, std::string>;
989+
990+
OpsReceiveAudioFrameOutput _blocks_audio_packet_decoder_receive_frame(
991+
torch::stable::Tensor& decoder) {
992+
PacketDecoder* decoder_ptr = unwrap_tensor_to_pointer<PacketDecoder>(decoder);
993+
STD_TORCH_CHECK(
994+
decoder_ptr->media_type() == AVMEDIA_TYPE_AUDIO,
995+
"This PacketDecoder decodes video, not audio.");
996+
997+
UniqueAVFrame av_frame(av_frame_alloc());
998+
STD_TORCH_CHECK(av_frame != nullptr, "Failed to allocate AVFrame");
999+
int status = decoder_ptr->receive_frame(av_frame);
1000+
if (status != AVSUCCESS) {
1001+
return std::make_tuple(
1002+
torch::stable::empty({int64_t(0)}, kStableUInt8),
1003+
static_cast<int64_t>(status),
1004+
0.0,
1005+
0.0,
1006+
static_cast<int64_t>(0),
1007+
std::string(""));
1008+
}
1009+
1010+
AVRational time_base = decoder_ptr->time_base();
1011+
const char* sample_format_name =
1012+
av_get_sample_fmt_name(static_cast<AVSampleFormat>(av_frame->format));
1013+
return std::make_tuple(
1014+
audio_samples(*av_frame),
1015+
static_cast<int64_t>(0),
1016+
pts_to_seconds(get_pts_or_dts(*av_frame), time_base),
1017+
pts_to_seconds(get_duration(*av_frame), time_base),
1018+
static_cast<int64_t>(av_frame->sample_rate),
1019+
std::string(
1020+
sample_format_name == nullptr ? "unknown" : sample_format_name));
1021+
}
1022+
9811023
torch::stable::Tensor _blocks_create_color_converter(
9821024
std::string device,
9831025
std::string output_dtype) {
@@ -1613,6 +1655,9 @@ STABLE_TORCH_LIBRARY_IMPL(torchcodec_ns, CPU, m) {
16131655
m.impl(
16141656
"_blocks_packet_decoder_receive_frame",
16151657
TORCH_BOX(&_blocks_packet_decoder_receive_frame));
1658+
m.impl(
1659+
"_blocks_audio_packet_decoder_receive_frame",
1660+
TORCH_BOX(&_blocks_audio_packet_decoder_receive_frame));
16161661
m.impl("_blocks_convert_frame", TORCH_BOX(&_blocks_convert_frame));
16171662
m.impl("_blocks_frame_metadata", TORCH_BOX(&_blocks_frame_metadata));
16181663
m.impl("_blocks_frame_planes", TORCH_BOX(&_blocks_frame_planes));

src/torchcodec/_core/sources.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ decoder_core_sources = [
3333
"CpuDeviceInterface.cpp",
3434
"Demuxer.cpp",
3535
"PacketDecoder.cpp",
36+
"AudioCommon.cpp",
3637
"ColorConverter.cpp",
3738
"SingleStreamDecoder.cpp",
3839
"Encoder.cpp",

src/torchcodec/decoders/_blocks/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from ._color_converter import ColorConverter
2323
from ._demuxer import AudioDemuxer, StreamIndex, VideoDemuxer
24-
from ._frame import Packet, RawFrame
24+
from ._frame import Packet, RawAudioSamples, RawFrame
2525
from ._packet_decoder import PacketDecoder
2626

2727
__all__ = [
@@ -31,6 +31,7 @@
3131
"ColorConverter",
3232
"Packet",
3333
"RawFrame",
34+
"RawAudioSamples",
3435
"StreamIndex",
3536
]
3637

0 commit comments

Comments
 (0)