├── MANIFEST.in ├── .gitignore ├── multilinux.env.Dockerfile ├── release.sh ├── install_opus.sh ├── README.md ├── CMakeLists.txt ├── setup.py └── opuspy.cc /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CMakeLists.txt 2 | include opuspy.cc 3 | include CMakeLists.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build-release 2 | dist 3 | build 4 | wheelhouse 5 | opuspy.egg-info 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | .idea -------------------------------------------------------------------------------- /multilinux.env.Dockerfile: -------------------------------------------------------------------------------- 1 | # docker build --platform linux/amd64 -f multilinux.env.Dockerfile . -t multilinux-env 2 | FROM quay.io/pypa/manylinux2014_x86_64 3 | 4 | RUN yum install autoconf automake libtool curl make cmake gcc-c++ unzip wget -y 5 | RUN yum install python3 -y 6 | RUN pip3 install pybind11[global] 7 | RUN yum install openssl-devel -y 8 | RUN yum install libogg-devel -y 9 | ADD install_opus.sh . 10 | RUN sh install_opus.sh 11 | 12 | 13 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -f multilinux.env.Dockerfile . -t multilinux-env 4 | 5 | rm -R -f dist 6 | 7 | python3 setup.py sdist || ! echo "Failed on sdist export." || exit 1 8 | 9 | sh build_manylinux_wheels.sh || ! echo "Failed building manylinux wheels." || exit 1 10 | 11 | cp wheelhouse/* dist/ || ! echo "No multilinux wheels? Check build_manylinux_wheels.sh" || exit 1 12 | rm -R -f wheelhouse 13 | 14 | python3 -m twine upload dist/* -------------------------------------------------------------------------------- /install_opus.sh: -------------------------------------------------------------------------------- 1 | # OGG and OpenSSL are required to install. 2 | mkdir -p libs || exit 1 3 | cd libs || exit 1 4 | 5 | wget https://archive.mozilla.org/pub/opus/opus-1.3.1.tar.gz || exit 1 6 | tar -xf opus-1.3.1.tar.gz || exit 1 7 | cd opus-1.3.1 || exit 1 8 | ./configure || exit 1 9 | make -j 8 || exit 1 10 | make install || exit 1 11 | ldconfig || exit 1 12 | cd .. || exit 1 13 | 14 | wget http://downloads.xiph.org/releases/opus/opusfile-0.12.tar.gz || exit 1 15 | tar -xf opusfile-0.12.tar.gz || exit 1 16 | cd opusfile-0.12 || exit 1 17 | ./configure || exit 1 18 | make -j 8 || exit 1 19 | make install || exit 1 20 | ldconfig || exit 1 21 | cd .. || exit 1 22 | 23 | wget http://downloads.xiph.org/releases/opus//libopusenc-0.2.1.tar.gz || exit 1 24 | tar -xf libopusenc-0.2.1.tar.gz || exit 1 25 | cd libopusenc-0.2.1 || exit 1 26 | ./configure || exit 1 27 | make -j 8 || exit 1 28 | make install || exit 1 29 | ldconfig || exit 1 30 | cd .. || exit 1 31 | 32 | cd .. || exit 1 33 | 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Opuspy 2 | 3 | A simple wrapper over Opus allowing to write and read Opus files in Python. 4 | 5 | #### Installation: 6 | 7 | ```pip3 install opuspy``` 8 | 9 | Wheels available for linux. 10 | 11 | #### Docs: 12 | 13 | ```python 14 | def read(path: str) -> Tuple[numpy.ndarray[numpy.int16], int]: 15 | """Returns the waveform_tc as the int16 np.array of shape [samples, channels] 16 | and the original sample rate. NOTE: the waveform returned is ALWAYS at 48khz 17 | as this is how opus stores any waveform, the sample rate returned is just the 18 | original sample rate of encoded audio that you might witch to resample the returned 19 | waveform to.""" 20 | 21 | def write( 22 | path: str, 23 | waveform_tc: numpy.ndarray[numpy.int16], 24 | sample_rate: int, 25 | bitrate: int = -1000, 26 | signal_type: int = 0, 27 | encoder_complexity: int = 10) -> None: 28 | """Saves the waveform_tc as the opus-encoded file at the specified path. 29 | The waveform must be a numpy array of np.int16 type, and shape [samples (time axis), channels]. 30 | Recommended sample rate is 48000. You can specify the bitrate in bits/s, as well as 31 | encoder_complexity (in range [0, 10] inclusive, the higher the better quality at given bitrate, 32 | but more CPU usage, 10 is recommended). Finally, there is signal_type option, that can help to 33 | improve quality for specific audio, types (0 = AUTO (default), 1 = MUSIC, 2 = SPEECH).""" 34 | ``` -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(opuspy) 3 | 4 | set(CMAKE_CXX_STANDARD 14) 5 | 6 | message("CMAKE_SYSTEM_PREFIX_PATH ${CMAKE_SYSTEM_PREFIX_PATH}") 7 | if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") 8 | # MacOS support. 9 | list(APPEND CMAKE_PREFIX_PATH /opt/homebrew) 10 | set(CMAKE_OSX_ARCHITECTURES "arm64") 11 | 12 | endif() 13 | 14 | 15 | list(APPEND CMAKE_PREFIX_PATH /usr/lib/x86_64-linux-gnu) 16 | 17 | # Optimize. 18 | set(CMAKE_CXX_FLAGS " -Wall -O2") 19 | 20 | message("heljlojd") 21 | 22 | # Pybind11 23 | find_package(Python COMPONENTS Interpreter Development) 24 | find_package(pybind11 CONFIG REQUIRED) 25 | 26 | 27 | # opus 28 | find_library(opus_LIBRARIES opus REQUIRED) 29 | find_path(opus_INCLUDE_DIRS 30 | NAMES opus.h 31 | PATH_SUFFIXES opus 32 | REQUIRED 33 | ) 34 | include_directories(${opus_INCLUDE_DIRS}) 35 | message("opus_LIBRARIES ${opus_LIBRARIES} opus_INCLUDE_DIRS ${opus_INCLUDE_DIRS}") 36 | 37 | 38 | # opusFile 39 | find_library(opusfile_LIBRARIES opusfile REQUIRED) 40 | find_path( 41 | opusfile_INCLUDE_DIRS 42 | NAMES opusfile.h 43 | PATH_SUFFIXES opus 44 | REQUIRED 45 | ) 46 | include_directories(${opusfile_INCLUDE_DIRS}) 47 | message("opusfile_LIBRARIES ${opusfile_LIBRARIES} opusfile_INCLUDE_DIRS ${opusfile_INCLUDE_DIRS}") 48 | 49 | 50 | # opusEnc 51 | find_library(opusenc_LIBRARIES opusenc REQUIRED) 52 | find_path( 53 | opusenc_INCLUDE_DIRS 54 | NAMES opusenc.h 55 | PATH_SUFFIXES opus 56 | REQUIRED 57 | ) 58 | include_directories(${opusenc_INCLUDE_DIRS}) 59 | message("opusenc_LIBRARIES ${opusenc_LIBRARIES} opusenc_INCLUDE_DIRS ${opusenc_INCLUDE_DIRS}") 60 | 61 | 62 | pybind11_add_module(opuspy opuspy.cc) 63 | target_link_libraries(opuspy PRIVATE ${opusfile_LIBRARIES} ${opusenc_LIBRARIES}) 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import platform 5 | import subprocess 6 | 7 | from setuptools import setup, Extension, find_packages 8 | from setuptools.command.build_ext import build_ext 9 | from distutils.version import LooseVersion 10 | 11 | 12 | class CMakeExtension(Extension): 13 | def __init__(self, name, sourcedir=''): 14 | Extension.__init__(self, name, sources=[]) 15 | self.sourcedir = os.path.abspath(sourcedir) 16 | 17 | 18 | class CMakeBuild(build_ext): 19 | def run(self): 20 | try: 21 | out = subprocess.check_output(['cmake', '--version']) 22 | except OSError: 23 | raise RuntimeError("CMake must be installed to build the following extensions: " + 24 | ", ".join(e.name for e in self.extensions)) 25 | 26 | if platform.system() == "Windows": 27 | cmake_version = LooseVersion(re.search(r'version\s*([\d.]+)', out.decode()).group(1)) 28 | if cmake_version < '3.1.0': 29 | raise RuntimeError("CMake >= 3.1.0 is required on Windows") 30 | 31 | for ext in self.extensions: 32 | self.build_extension(ext) 33 | 34 | def build_extension(self, ext): 35 | extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) 36 | # required for auto-detection of auxiliary "native" libs 37 | if not extdir.endswith(os.path.sep): 38 | extdir += os.path.sep 39 | 40 | cmake_args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir, 41 | '-DPYTHON_EXECUTABLE=' + sys.executable] 42 | assert not self.debug, "No debug here." 43 | cfg = 'Debug' if self.debug else 'Release' 44 | build_args = ['--config', cfg] 45 | 46 | if platform.system() == "Windows": 47 | cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)] 48 | if sys.maxsize > 2**32: 49 | cmake_args += ['-A', 'x64'] 50 | build_args += ['--', '/m'] 51 | else: 52 | cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg] 53 | build_args += ['--', '-j8'] 54 | 55 | env = os.environ.copy() 56 | env['CXXFLAGS'] = '{} -DVERSION_INFO=\\"{}\\"'.format(env.get('CXXFLAGS', ''), 57 | self.distribution.get_version()) 58 | if not os.path.exists(self.build_temp): 59 | os.makedirs(self.build_temp) 60 | subprocess.check_call(['cmake', ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env) 61 | subprocess.check_call(['cmake', '--build', '.'] + build_args, cwd=self.build_temp) 62 | 63 | setup( 64 | name='opuspy', 65 | version='0.0.3', 66 | author='Eleven Labs', 67 | # url='https://github.com/PiotrDabkowski/pytdb', 68 | description='OPUS wrapper.', 69 | long_description='', 70 | ext_modules=[CMakeExtension('opuspy')], 71 | cmdclass=dict(build_ext=CMakeBuild), 72 | zip_safe=False, 73 | ) -------------------------------------------------------------------------------- /opuspy.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "opusenc.h" 3 | #include "opusfile.h" 4 | #include "opus.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace py = pybind11; 13 | 14 | template 15 | py::array_t MakeNpArray(std::vector shape, T* data) { 16 | 17 | std::vector strides(shape.size()); 18 | size_t v = sizeof(T); 19 | size_t i = shape.size(); 20 | while (i--) { 21 | strides[i] = v; 22 | v *= shape[i]; 23 | } 24 | py::capsule free_when_done(data, [](void* f) { 25 | auto* foo = reinterpret_cast(f); 26 | delete[] foo; 27 | }); 28 | return py::array_t(shape, strides, data, free_when_done); 29 | } 30 | 31 | 32 | 33 | void OpusWrite(const std::string& path, const py::array_t& waveform_tc, const int sample_rate, const int bitrate=OPUS_AUTO, const int signal_type = 0, const int encoder_complexity = 10) { 34 | if (waveform_tc.ndim() != 2) { 35 | throw py::value_error("waveform_tc must have exactly 2 dimension: [time, channels]."); 36 | } 37 | if (waveform_tc.shape(1) > 8 || waveform_tc.shape(1) < 1) { 38 | throw py::value_error("waveform_tc must have at least 1 channel, and no more than 8."); 39 | } 40 | if ((bitrate < 500 or bitrate > 512000) && bitrate != OPUS_BITRATE_MAX && bitrate != OPUS_AUTO) { 41 | throw py::value_error("Invalid bitrate, must be at least 512 and at most 512k bits/s."); 42 | } 43 | if (sample_rate < 8000 or sample_rate > 48000) { 44 | throw py::value_error("Invalid sample_rate, must be at least 8k and at most 48k."); 45 | } 46 | if (encoder_complexity > 10 || encoder_complexity < 0) { 47 | throw py::value_error("Invalid encoder_complexity, must be in range [0, 10] inclusive. The higher, the better quality at the given bitrate, but uses more CPU."); 48 | } 49 | opus_int32 opus_signal_type; 50 | switch (signal_type) { 51 | case 0: 52 | opus_signal_type = OPUS_AUTO; 53 | break; 54 | case 1: 55 | opus_signal_type = OPUS_SIGNAL_MUSIC; 56 | break; 57 | case 2: 58 | opus_signal_type = OPUS_SIGNAL_VOICE; 59 | break; 60 | default: 61 | throw py::value_error("Invalid signal type, must be 0 (auto), 1 (music) or 2 (voice)."); 62 | } 63 | 64 | OggOpusComments* comments = ope_comments_create(); 65 | // ope_comments_add(comments, "hello", "world"); 66 | int error; 67 | // We set family == 1, and channels based on waveform. 68 | OggOpusEnc* enc = ope_encoder_create_file( 69 | path.data(), comments, sample_rate, waveform_tc.shape(1), 0, &error); 70 | if (error != 0) { 71 | throw py::value_error("Unexpected error, is the provided path valid?"); 72 | } 73 | 74 | if (ope_encoder_ctl(enc, OPUS_SET_BITRATE_REQUEST, bitrate) != 0) { 75 | throw py::value_error("This should not happen. Could not set bitrate..."); 76 | } 77 | 78 | 79 | if (ope_encoder_ctl(enc, OPUS_SET_SIGNAL_REQUEST, opus_signal_type) != 0) { 80 | throw py::value_error("This should not happen. Could not set signal type..."); 81 | } 82 | if (ope_encoder_ctl(enc, OPUS_SET_COMPLEXITY_REQUEST, encoder_complexity) != 0) { 83 | throw py::value_error("This should not happen. Could not set encoder complexity..."); 84 | } 85 | 86 | // OK, now we are all configured. Let's write! 87 | if (ope_encoder_write(enc, waveform_tc.data(), waveform_tc.shape(0)) != 0) { 88 | throw py::value_error("Could not write audio data."); 89 | } 90 | if (ope_encoder_drain(enc) != 0) { 91 | throw py::value_error("Could not finalize write."); 92 | } 93 | 94 | // Cleanup. 95 | ope_encoder_destroy(enc); 96 | ope_comments_destroy(comments); 97 | } 98 | 99 | std::tuple, int> OpusRead(const std::string& path) { 100 | int error; 101 | OggOpusFile* file = op_open_file(path.data(), &error); 102 | if (error != 0) { 103 | throw py::value_error("Could not open opus file."); 104 | } 105 | const ssize_t num_chans = op_channel_count(file, -1); 106 | const ssize_t num_samples = op_pcm_total(file, -1) / num_chans; 107 | 108 | const OpusHead* meta = op_head(file, -1); // unowned 109 | const int sample_rate = meta->input_sample_rate; 110 | 111 | auto* data = static_cast(malloc(sizeof(opus_int16) * num_chans * num_samples)); 112 | auto waveform_tc = MakeNpArray({num_samples, num_chans}, data); 113 | size_t num_read = 0; 114 | 115 | while (true) { 116 | int chunk = op_read(file, data + num_read*num_chans, num_samples-num_read*num_chans, nullptr); 117 | if (chunk < 0) { 118 | throw py::value_error("Could not read opus file."); 119 | } 120 | if (chunk == 0) { 121 | break; 122 | } 123 | num_read += chunk; 124 | if (num_read > num_samples) { 125 | throw py::value_error("Read too much???"); 126 | } 127 | } 128 | 129 | if (num_read < num_samples-10) { 130 | std::cout << num_read << " " << num_samples << " " << num_chans; 131 | throw py::value_error("Could not complete read..."); 132 | } 133 | op_free(file); 134 | return std::make_tuple(std::move(waveform_tc), sample_rate); 135 | } 136 | 137 | //int main(int argc, char *argv[]) 138 | //{ 139 | // int err; 140 | // const int sample_rate = 48000; 141 | // const int wave_hz = 330; 142 | // const opus_int16 max_ampl = std::numeric_limits::max() / 2; 143 | // OggOpusComments* a = ope_comments_create(); 144 | // OggOpusEnc* file = ope_encoder_create_file( 145 | // "hello.opus", a, sample_rate, 1, 0, &err); 146 | // if (ope_encoder_ctl(file, OPUS_SET_BITRATE_REQUEST, 10000) != 0) { 147 | // throw std::invalid_argument("Invalid bitrate."); 148 | // } 149 | // 150 | // 151 | // std::vector wave; 152 | // for (int i = 0; i < sample_rate*11; i++) { 153 | // double ampl = max_ampl * sin(static_cast(i)/sample_rate*2*M_PI*wave_hz); 154 | // wave.push_back(static_cast(ampl)); 155 | // } 156 | // 157 | // ope_encoder_write(file, wave.data(), wave.size()); 158 | // ope_encoder_drain(file); 159 | // ope_encoder_destroy(file); 160 | //} 161 | 162 | 163 | 164 | PYBIND11_MODULE(opuspy, m) { 165 | 166 | m.def("write", &OpusWrite, py::arg("path"), py::arg("waveform_tc"), py::arg("sample_rate"), py::arg("bitrate")=OPUS_AUTO, py::arg("signal_type")=0, py::arg("encoder_complexity")=10, 167 | "Saves the waveform_tc as the opus-encoded file at the specified path. The waveform must be a numpy array of np.int16 type, and shape [samples (time axis), channels]. Recommended sample rate is 48000. You can specify the bitrate in bits/s, as well as encoder_complexity (in range [0, 10] inclusive, the higher the better quality at given bitrate, but more CPU usage, 10 is recommended). Finally, there is signal_type option, that can help to improve quality for specific audio, types (0 = AUTO (default), 1 = MUSIC, 2 = SPEECH)."); 168 | m.def("read", &OpusRead, py::arg("path"), "Returns the waveform_tc as the int16 np.array of shape [samples, channels] and the original sample rate. NOTE: the waveform returned is ALWAYS at 48khz as this is how opus stores any waveform, the sample rate returned is just the original sample rate of encoded audio that you might witch to resample the returned waveform to."); 169 | } --------------------------------------------------------------------------------