Skip to content

Commit e2ae694

Browse files
authored
Enable decoding audio with >8 channels (#1166)
1 parent db0a147 commit e2ae694

7 files changed

Lines changed: 111 additions & 17 deletions

File tree

src/torchcodec/_core/Encoder.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,12 @@ UniqueAVFrame AudioEncoder::maybeConvertAVFrame(const UniqueAVFrame& avFrame) {
361361
avFrame,
362362
outNumChannels_));
363363
}
364+
// convertAudioAVFrameSamples uses avFrame's extended_data field, so we ensure
365+
// it's the same as data. This should always be the case since we validated
366+
// earlier that we have less than AV_NUM_DATA_POINTERS channels.
367+
TORCH_CHECK(
368+
avFrame->data == avFrame->extended_data,
369+
"Codec context data and extended_data pointers differ, this is unexpected.")
364370
UniqueAVFrame convertedAVFrame = convertAudioAVFrameSamples(
365371
swrContext_,
366372
avFrame,

src/torchcodec/_core/FFMPEGCommon.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,12 +512,16 @@ UniqueAVFrame convertAudioAVFrameSamples(
512512
"Could not allocate frame buffers for sample format conversion: ",
513513
getFFMPEGErrorStringFromErrorCode(status));
514514

515+
// Below we use AVFrame->extended_data instead of AVFrame->data to support
516+
// decoding audio with >8 audio channels. extended_data contains pointers
517+
// for all channels, while data only contains AV_NUM_DATA_POINTERS (8).
518+
// https://ffmpeg.org/doxygen/trunk/structAVFrame.html#afca04d808393822625e09b5ba91c6756
515519
auto numConvertedSamples = swr_convert(
516520
swrContext.get(),
517-
convertedAVFrame->data,
521+
convertedAVFrame->extended_data,
518522
convertedAVFrame->nb_samples,
519523
static_cast<const uint8_t**>(
520-
const_cast<const uint8_t**>(srcAVFrame->data)),
524+
const_cast<const uint8_t**>(srcAVFrame->extended_data)),
521525
srcAVFrame->nb_samples);
522526
// numConvertedSamples can be 0 if we're downsampling by a great factor and
523527
// the first frame doesn't contain a lot of samples. It should be handled

src/torchcodec/_core/SingleStreamDecoder.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -571,9 +571,8 @@ void SingleStreamDecoder::addAudioStream(
571571
"seek_mode must be 'approximate' for audio streams.");
572572
if (audioStreamOptions.numChannels.has_value()) {
573573
TORCH_CHECK(
574-
*audioStreamOptions.numChannels > 0 &&
575-
*audioStreamOptions.numChannels <= AV_NUM_DATA_POINTERS,
576-
"num_channels must be > 0 and <= AV_NUM_DATA_POINTERS (usually 8). Got: ",
574+
*audioStreamOptions.numChannels > 0,
575+
"num_channels must be > 0. Got: ",
577576
*audioStreamOptions.numChannels);
578577
}
579578

test/resources/sine_16ch_s16.wav

500 KB
Binary file not shown.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
[
2+
{
3+
"pts_time": "0.000000",
4+
"duration_time": "0.064000"
5+
},
6+
{
7+
"pts_time": "0.064000",
8+
"duration_time": "0.064000"
9+
},
10+
{
11+
"pts_time": "0.128000",
12+
"duration_time": "0.064000"
13+
},
14+
{
15+
"pts_time": "0.192000",
16+
"duration_time": "0.064000"
17+
},
18+
{
19+
"pts_time": "0.256000",
20+
"duration_time": "0.064000"
21+
},
22+
{
23+
"pts_time": "0.320000",
24+
"duration_time": "0.064000"
25+
},
26+
{
27+
"pts_time": "0.384000",
28+
"duration_time": "0.064000"
29+
},
30+
{
31+
"pts_time": "0.448000",
32+
"duration_time": "0.064000"
33+
},
34+
{
35+
"pts_time": "0.512000",
36+
"duration_time": "0.064000"
37+
},
38+
{
39+
"pts_time": "0.576000",
40+
"duration_time": "0.064000"
41+
},
42+
{
43+
"pts_time": "0.640000",
44+
"duration_time": "0.064000"
45+
},
46+
{
47+
"pts_time": "0.704000",
48+
"duration_time": "0.064000"
49+
},
50+
{
51+
"pts_time": "0.768000",
52+
"duration_time": "0.064000"
53+
},
54+
{
55+
"pts_time": "0.832000",
56+
"duration_time": "0.064000"
57+
},
58+
{
59+
"pts_time": "0.896000",
60+
"duration_time": "0.064000"
61+
},
62+
{
63+
"pts_time": "0.960000",
64+
"duration_time": "0.040000"
65+
}
66+
]

test/test_decoders.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
needs_cuda,
4343
needs_ffmpeg_cli,
4444
psnr,
45+
SINE_16_CHANNEL_S16,
4546
SINE_MONO_S16,
4647
SINE_MONO_S32,
4748
SINE_MONO_S32_44100,
@@ -1947,7 +1948,9 @@ def test_cpu_fallback_no_fallback_on_supported_video(self, device):
19471948

19481949

19491950
class TestAudioDecoder:
1950-
@pytest.mark.parametrize("asset", (NASA_AUDIO, NASA_AUDIO_MP3, SINE_MONO_S32))
1951+
@pytest.mark.parametrize(
1952+
"asset", (NASA_AUDIO, NASA_AUDIO_MP3, SINE_MONO_S32, SINE_16_CHANNEL_S16)
1953+
)
19511954
def test_metadata(self, asset):
19521955
decoder = AudioDecoder(asset.path)
19531956
assert isinstance(decoder.metadata, AudioStreamMetadata)
@@ -2005,7 +2008,7 @@ def test_get_all_samples_with_range(self, asset, stop_seconds):
20052008
assert samples.sample_rate == asset.sample_rate
20062009
assert samples.pts_seconds == asset.get_frame_info(idx=0).pts_seconds
20072010

2008-
@pytest.mark.parametrize("asset", (NASA_AUDIO, NASA_AUDIO_MP3))
2011+
@pytest.mark.parametrize("asset", (NASA_AUDIO, NASA_AUDIO_MP3, SINE_16_CHANNEL_S16))
20092012
def test_get_all_samples(self, asset):
20102013
decoder = AudioDecoder(asset.path)
20112014
torch.testing.assert_close(
@@ -2274,8 +2277,7 @@ def test_samples_duration(self, asset, sample_rate):
22742277
# that the extra tensor allocation that happens within
22752278
# maybeFlushSwrBuffers() is correct.
22762279
@pytest.mark.parametrize("sample_rate", (None, 16_000))
2277-
# FFmpeg can handle up to AV_NUM_DATA_POINTERS=8 channels
2278-
@pytest.mark.parametrize("num_channels", (1, 2, 8, None))
2280+
@pytest.mark.parametrize("num_channels", (1, 2, 8, 16, 24, None))
22792281
def test_num_channels(self, asset, sample_rate, num_channels):
22802282
decoder = AudioDecoder(
22812283
asset.path, sample_rate=sample_rate, num_channels=num_channels
@@ -2289,12 +2291,12 @@ def test_num_channels(self, asset, sample_rate, num_channels):
22892291

22902292
@pytest.mark.parametrize("asset", (SINE_MONO_S32, NASA_AUDIO_MP3))
22912293
def test_num_channels_errors(self, asset):
2292-
with pytest.raises(
2293-
RuntimeError, match="num_channels must be > 0 and <= AV_NUM_DATA_POINTERS"
2294-
):
2294+
with pytest.raises(RuntimeError, match="num_channels must be > 0"):
22952295
AudioDecoder(asset.path, num_channels=0)
2296-
with pytest.raises(
2297-
RuntimeError, match="num_channels must be > 0 and <= AV_NUM_DATA_POINTERS"
2298-
):
2299-
# FFmpeg can handle up to AV_NUM_DATA_POINTERS=8 channels
2300-
AudioDecoder(asset.path, num_channels=9)
2296+
for num_channels in (15, 23):
2297+
with pytest.raises(RuntimeError, match="Couldn't initialize SwrContext:"):
2298+
decoder = AudioDecoder(asset.path, num_channels=num_channels)
2299+
# Call get_all_samples to trigger num_channels conversion.
2300+
# FFmpeg fails to find a default layout for certain channel counts,
2301+
# which causes SwrContext to fail to initialize.
2302+
decoder.get_all_samples()

test/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,3 +834,20 @@ def sample_format(self) -> str:
834834
)
835835
},
836836
)
837+
838+
# 16-channel audio for testing support for >8 channels. Generated with:
839+
# ffmpeg -i test/resources/sine_mono_s32.wav -t 1 -filter_complex "[0]asplit=16[s0][s1][s2][s3][s4][s5][s6][s7][s8][s9][s10][s11][s12][s13][s14][s15];[s0][s1][s2][s3][s4][s5][s6][s7][s8][s9][s10][s11][s12][s13][s14][s15]amerge=inputs=16" -c:a pcm_s16le test/resources/sine_16ch_s16.wav
840+
SINE_16_CHANNEL_S16 = TestAudio(
841+
filename="sine_16ch_s16.wav",
842+
default_stream_index=0,
843+
frames={}, # Automatically loaded from json file
844+
stream_infos={
845+
0: TestAudioStreamInfo(
846+
sample_rate=16_000,
847+
num_channels=16,
848+
duration_seconds=1,
849+
num_frames=16,
850+
sample_format="s16",
851+
)
852+
},
853+
)

0 commit comments

Comments
 (0)