Repository: pytorch/audio

Issue:
🚀 Feature Request: Opus audio format support
## 🚀 Feature
<!-- -->
Add opus format 
## Motivation

<!-- Please outline the motivation for the proposal. Is your feature request related to a problem? e.g., I'm always frustrated when [...]. If this is related to another GitHub issue, please link here too -->
Opus is a good and popular lossy audio coding format. A lot of audio files are stored in opus format but it's impossible to use it with torchaudio.load(). 
## Pitch

<!-- A clear and concise description of what you want to happen. -->
Please, make possible to do 
audio = torchaudio.load("my_audio.opus")



Discussion:
Hi @zhukovaes 

Thanks for bringing up the request.
How are you installing `torchaudio` and which backend are you using?
I see two possible solutions;
1. If you have `sox` with `opus` support in your system, building `torchaudio`'s C++ extension from source and making it depend on external `libsox` should allow you to use `opus` file. 
2. Similarly, if you have `libsndfile` with `opus` support in your system, switching to `soundfile` backend will do.
Please refer to README for the instruction for building from source and backend. If something is not clear feel free to ask further questions.

If you are using binary distributions of `torchaudio`, the best way to bring `opus` support is your contribution to add support for it. Currently, we use [this script](https://github.com/pytorch/audio/blob/master/build_tools/setup_helpers/build_third_party.sh) to build `libsox` statically with support for `lame`, `libmad` and `flac`. If you can successfully modify this script to add `opus` support, and open a PR, we can review it.

Resolving patch:
diff --git a/build_tools/setup_helpers/extension.py b/build_tools/setup_helpers/extension.py
--- a/build_tools/setup_helpers/extension.py
+++ b/build_tools/setup_helpers/extension.py
@@ -83,6 +83,8 @@ def _get_extra_objects():
             'libmad.a',
             'libFLAC.a',
             'libmp3lame.a',
+            'libopusfile.a',
+            'libopus.a',
             'libvorbisenc.a',
             'libvorbisfile.a',
             'libvorbis.a',


Test patch:
diff --git a/test/assets/io/96k_0_1ch.opus b/test/assets/io/96k_0_1ch.opus
new file mode 100644
Binary files /dev/null and b/test/assets/io/96k_0_1ch.opus differ
diff --git a/test/assets/io/96k_0_2ch.opus b/test/assets/io/96k_0_2ch.opus
new file mode 100644
Binary files /dev/null and b/test/assets/io/96k_0_2ch.opus differ
diff --git a/test/assets/io/96k_10_1ch.opus b/test/assets/io/96k_10_1ch.opus
new file mode 100644
Binary files /dev/null and b/test/assets/io/96k_10_1ch.opus differ
diff --git a/test/assets/io/96k_10_2ch.opus b/test/assets/io/96k_10_2ch.opus
new file mode 100644
Binary files /dev/null and b/test/assets/io/96k_10_2ch.opus differ
diff --git a/test/assets/io/96k_5_1ch.opus b/test/assets/io/96k_5_1ch.opus
new file mode 100644
Binary files /dev/null and b/test/assets/io/96k_5_1ch.opus differ
diff --git a/test/assets/io/96k_5_2ch.opus b/test/assets/io/96k_5_2ch.opus
new file mode 100644
Binary files /dev/null and b/test/assets/io/96k_5_2ch.opus differ
diff --git a/test/assets/io/generate_opus.py b/test/assets/io/generate_opus.py
new file mode 100644
--- /dev/null
+++ b/test/assets/io/generate_opus.py
@@ -0,0 +1,50 @@
+"""Generate opus file for testing load functions"""
+
+import argparse
+import subprocess
+
+import scipy.io.wavfile
+import torch
+
+
+def _parse_args():
+    parser = argparse.ArgumentParser(
+        description='Generate opus files for test'
+    )
+    parser.add_argument('--num-channels', required=True, type=int)
+    parser.add_argument('--compression-level', required=True, type=int, choices=list(range(11)))
+    parser.add_argument('--bitrate', default='96k')
+    return parser.parse_args()
+
+
+def convert_to_opus(
+        src_path, dst_path,
+        *, bitrate, compression_level):
+    """Convert audio file with `ffmpeg` command."""
+    command = ['ffmpeg', '-y', '-i', src_path, '-c:a', 'libopus', '-b:a', bitrate]
+    if compression_level is not None:
+        command += ['-compression_level', str(compression_level)]
+    command += [dst_path]
+    print(' '.join(command))
+    subprocess.run(command, check=True)
+
+
+def _generate(num_channels, compression_level, bitrate):
+    org_path = 'original.wav'
+    ops_path = f'{bitrate}_{compression_level}_{num_channels}ch.opus'
+
+    # Note: ffmpeg forces sample rate 48k Hz for opus https://stackoverflow.com/a/39186779
+    # 1. generate original wav
+    data = torch.linspace(-32768, 32767, 32768, dtype=torch.int16).repeat([num_channels, 1]).t()
+    scipy.io.wavfile.write(org_path, 48000, data.numpy())
+    # 2. convert to opus
+    convert_to_opus(org_path, ops_path, bitrate=bitrate, compression_level=compression_level)
+
+
+def _main():
+    args = _parse_args()
+    _generate(args.num_channels, args.compression_level, args.bitrate)
+
+
+if __name__ == '__main__':
+    _main()
diff --git a/test/sox_io_backend/test_info.py b/test/sox_io_backend/test_info.py
--- a/test/sox_io_backend/test_info.py
+++ b/test/sox_io_backend/test_info.py
@@ -8,9 +8,10 @@
     PytorchTestCase,
     skipIfNoExec,
     skipIfNoExtension,
-    sox_utils,
+    get_asset_path,
     get_wav_data,
     save_wav,
+    sox_utils,
 )
 from .common import (
     name_func,
@@ -106,3 +107,19 @@ def test_vorbis(self, sample_rate, num_channels, quality_level):
         assert info.get_sample_rate() == sample_rate
         assert info.get_num_frames() == sample_rate * duration
         assert info.get_num_channels() == num_channels
+
+
+@skipIfNoExtension
+class TestInfoOpus(PytorchTestCase):
+    @parameterized.expand(list(itertools.product(
+        ['96k'],
+        [1, 2],
+        [0, 5, 10],
+    )), name_func=name_func)
+    def test_opus(self, bitrate, num_channels, compression_level):
+        """`sox_io_backend.info` can check opus file correcty"""
+        path = get_asset_path('io', f'{bitrate}_{compression_level}_{num_channels}ch.opus')
+        info = sox_io_backend.info(path)
+        assert info.get_sample_rate() == 48000
+        assert info.get_num_frames() == 32768
+        assert info.get_num_channels() == num_channels
diff --git a/test/sox_io_backend/test_load.py b/test/sox_io_backend/test_load.py
--- a/test/sox_io_backend/test_load.py
+++ b/test/sox_io_backend/test_load.py
@@ -8,6 +8,7 @@
     PytorchTestCase,
     skipIfNoExec,
     skipIfNoExtension,
+    get_asset_path,
     get_wav_data,
     load_wav,
     save_wav,
@@ -212,6 +213,23 @@ def test_vorbis_large(self, sample_rate, num_channels, quality_level):
         two_hours = 2 * 60 * 60
         self.assert_vorbis(sample_rate, num_channels, quality_level, two_hours)
 
+    @parameterized.expand(list(itertools.product(
+        ['96k'],
+        [1, 2],
+        [0, 5, 10],
+    )), name_func=name_func)
+    def test_opus(self, bitrate, num_channels, compression_level):
+        """`sox_io_backend.load` can load opus file correctly."""
+        ops_path = get_asset_path('io', f'{bitrate}_{compression_level}_{num_channels}ch.opus')
+        wav_path = self.get_temp_path(f'{bitrate}_{compression_level}_{num_channels}ch.opus.wav')
+        sox_utils.convert_audio_file(ops_path, wav_path)
+
+        expected, sample_rate = load_wav(wav_path)
+        found, sr = sox_io_backend.load(ops_path)
+
+        assert sample_rate == sr
+        self.assertEqual(expected, found)
+
 
 @skipIfNoExec('sox')
 @skipIfNoExtension