Skip to content

Commit

Permalink
implement unit test & ci
Browse files Browse the repository at this point in the history
  • Loading branch information
leng-yue committed Aug 24, 2024
1 parent 70402dc commit a5ce0fb
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 189 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: build

on:
push:
branches:
- main

jobs:
build:
strategy:
matrix:
platform: [ubuntu-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]

runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Add requirements
run: apt install -y libopus-dev libopusfile-dev libopusenc-dev && python -m pip install --upgrade wheel setuptools
- name: Build and install
run: pip install --verbose .
- name: Test
run: python tests/test.py
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,4 @@ dmypy.json
# Cython debug symbols
cython_debug/
libs
.vscode
1 change: 1 addition & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python setup.py install && python tests/test.py
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
define_macros=[("VERSION_INFO", __version__)],
include_dirs=[
"/usr/include/opus",
"/usr/include/opusfile",
"/usr/include/opusenc",
],
library_dirs=["/usr/lib/x86_64-linux-gnu"],
libraries=["opus", "opusenc"],
),
]

Expand Down
244 changes: 119 additions & 125 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,53 +1,65 @@
#include <iostream>
#include <opus/opus.h>
#include <opus/opusfile.h>
#include <opus/opusenc.h>
#include <vector>
#include <cmath>
#include <stdexcept>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include <memory>
#include <cstdio>

namespace py = pybind11;

template<typename T>
py::array_t<T> MakeNpArray(std::vector<ssize_t> shape, T* data) {

std::vector<ssize_t> strides(shape.size());
size_t v = sizeof(T);
size_t i = shape.size();
while (i--) {
strides[i] = v;
v *= shape[i];
}
py::capsule free_when_done(data, [](void* f) {
auto* foo = reinterpret_cast<T*>(f);
delete[] foo;
});
return py::array_t<T>(shape, strides, data, free_when_done);
}
class OpusBufferedEncoder
{
private:
OggOpusEnc *encoder;
OggOpusComments *comments;
int channels;

public:
OpusBufferedEncoder(int sample_rate, int channels, int bitrate = OPUS_AUTO, int signal_type = 0, int encoder_complexity = 10, int decision_delay = 0)
: encoder(nullptr), comments(nullptr), channels(channels)
{
if (channels < 1 || channels > 8)
{
throw py::value_error("Invalid channels, must be in range [1, 8] inclusive.");
}
if ((bitrate < 500 or bitrate > 512000) && bitrate != OPUS_BITRATE_MAX && bitrate != OPUS_AUTO)
{
throw py::value_error("Invalid bitrate, must be at least 512 and at most 512k bits/s.");
}
if (sample_rate < 8000 or sample_rate > 48000)
{
throw py::value_error("Invalid sample_rate, must be at least 8k and at most 48k.");
}
if (encoder_complexity > 10 || encoder_complexity < 0)
{
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.");
}
if (decision_delay < 0)
{
throw py::value_error("Invalid decision_delay, must be at least 0.");
}

int error;
comments = ope_comments_create();
encoder = ope_encoder_create_pull(comments, sample_rate, channels, 0, &error);
if (error != OPE_OK)
{
throw py::value_error("Failed to create Opus encoder");
}

if (ope_encoder_ctl(encoder, OPUS_SET_BITRATE(bitrate)) != OPE_OK)
{
throw py::value_error("Could not set bitrate.");
}

void OpusWrite(const std::string& path, const py::array_t<int16_t>& waveform_tc, const int sample_rate, const int bitrate=OPUS_AUTO, const int signal_type = 0, const int encoder_complexity = 10) {
if (waveform_tc.ndim() != 2) {
throw py::value_error("waveform_tc must have exactly 2 dimension: [time, channels].");
}
if (waveform_tc.shape(1) > 8 || waveform_tc.shape(1) < 1) {
throw py::value_error("waveform_tc must have at least 1 channel, and no more than 8.");
}
if ((bitrate < 500 or bitrate > 512000) && bitrate != OPUS_BITRATE_MAX && bitrate != OPUS_AUTO) {
throw py::value_error("Invalid bitrate, must be at least 512 and at most 512k bits/s.");
}
if (sample_rate < 8000 or sample_rate > 48000) {
throw py::value_error("Invalid sample_rate, must be at least 8k and at most 48k.");
}
if (encoder_complexity > 10 || encoder_complexity < 0) {
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.");
}
opus_int32 opus_signal_type;
switch (signal_type) {
opus_int32 opus_signal_type;
switch (signal_type)
{
case 0:
opus_signal_type = OPUS_AUTO;
break;
Expand All @@ -59,111 +71,93 @@ void OpusWrite(const std::string& path, const py::array_t<int16_t>& waveform_tc,
break;
default:
throw py::value_error("Invalid signal type, must be 0 (auto), 1 (music) or 2 (voice).");
}

OggOpusComments* comments = ope_comments_create();
// ope_comments_add(comments, "hello", "world");
int error;
// We set family == 1, and channels based on waveform.
OggOpusEnc* enc = ope_encoder_create_file(
path.data(), comments, sample_rate, waveform_tc.shape(1), 0, &error);
if (error != 0) {
throw py::value_error("Unexpected error, is the provided path valid?");
}
}

if (ope_encoder_ctl(enc, OPUS_SET_BITRATE_REQUEST, bitrate) != 0) {
throw py::value_error("This should not happen. Could not set bitrate...");
}
if (ope_encoder_ctl(encoder, OPUS_SET_SIGNAL(opus_signal_type)) != OPE_OK)
{
throw py::value_error("Could not set signal type.");
}

if (ope_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(encoder_complexity)) != OPE_OK)
{
throw py::value_error("Could not set encoder complexity.");
}

if (ope_encoder_ctl(enc, OPUS_SET_SIGNAL_REQUEST, opus_signal_type) != 0) {
throw py::value_error("This should not happen. Could not set signal type...");
}
if (ope_encoder_ctl(enc, OPUS_SET_COMPLEXITY_REQUEST, encoder_complexity) != 0) {
throw py::value_error("This should not happen. Could not set encoder complexity...");
// Set decision delay
if (ope_encoder_ctl(encoder, OPE_SET_DECISION_DELAY(decision_delay)) != OPE_OK)
{
throw py::value_error("Could not set decision delay.");
}
}

// OK, now we are all configured. Let's write!
if (ope_encoder_write(enc, waveform_tc.data(), waveform_tc.shape(0)) != 0) {
throw py::value_error("Could not write audio data.");
}
if (ope_encoder_drain(enc) != 0) {
throw py::value_error("Could not finalize write.");
}
py::bytes write(const py::array_t<int16_t> &buffer)
{
if (buffer.ndim() != 2 || buffer.shape(1) != channels)
{
throw py::value_error("Buffer must have shape [samples, channels] and match the number of channels specified in the constructor.");
}

// Cleanup.
ope_encoder_destroy(enc);
ope_comments_destroy(comments);
}
const int16_t *data = buffer.data();
int samples = buffer.shape(0);

std::tuple<py::array_t<opus_int16>, int> OpusRead(const std::string& path) {
int error;
OggOpusFile* file = op_open_file(path.data(), &error);
if (error != 0) {
throw py::value_error("Could not open opus file.");
}
const ssize_t num_chans = op_channel_count(file, -1);
const ssize_t num_samples = op_pcm_total(file, -1) / num_chans;
std::vector<unsigned char> encoded_data;

const OpusHead* meta = op_head(file, -1); // unowned
const int sample_rate = meta->input_sample_rate;
if (ope_encoder_write(encoder, data, samples) != OPE_OK)
{
throw py::value_error("Encoding failed");
}

auto* data = static_cast<opus_int16 *>(malloc(sizeof(opus_int16) * num_chans * num_samples));
auto waveform_tc = MakeNpArray<opus_int16>({num_samples, num_chans}, data);
size_t num_read = 0;
unsigned char *packet;
opus_int32 len;
while (ope_encoder_get_page(encoder, &packet, &len, 1) != 0)
{
encoded_data.insert(encoded_data.end(), packet, packet + len);
}

return py::bytes(reinterpret_cast<char *>(encoded_data.data()), encoded_data.size());
}

while (true) {
int chunk = op_read(file, data + num_read*num_chans, num_samples-num_read*num_chans, nullptr);
if (chunk < 0) {
throw py::value_error("Could not read opus file.");
py::bytes flush()
{
ope_encoder_drain(encoder);
opus_int32 len;
unsigned char *packet;
std::vector<unsigned char> encoded_data;
while (ope_encoder_get_page(encoder, &packet, &len, 1) != 0)
{
encoded_data.insert(encoded_data.end(), packet, packet + len);
}
if (chunk == 0) {
break;
return py::bytes(reinterpret_cast<char *>(encoded_data.data()), encoded_data.size());
}

void close()
{
if (encoder)
{
ope_encoder_drain(encoder);
ope_encoder_destroy(encoder);
encoder = nullptr;
}
num_read += chunk;
if (num_read > num_samples) {
throw py::value_error("Read too much???");
if (comments)
{
ope_comments_destroy(comments);
comments = nullptr;
}
}

if (num_read < num_samples-10) {
std::cout << num_read << " " << num_samples << " " << num_chans;
throw py::value_error("Could not complete read...");
~OpusBufferedEncoder()
{
close();
}
op_free(file);
return std::make_tuple(std::move(waveform_tc), sample_rate);
}
};

//int main(int argc, char *argv[])
//{
// int err;
// const int sample_rate = 48000;
// const int wave_hz = 330;
// const opus_int16 max_ampl = std::numeric_limits<opus_int16>::max() / 2;
// OggOpusComments* a = ope_comments_create();
// OggOpusEnc* file = ope_encoder_create_file(
// "hello.opus", a, sample_rate, 1, 0, &err);
// if (ope_encoder_ctl(file, OPUS_SET_BITRATE_REQUEST, 10000) != 0) {
// throw std::invalid_argument("Invalid bitrate.");
// }
//
//
// std::vector<int16_t> wave;
// for (int i = 0; i < sample_rate*11; i++) {
// double ampl = max_ampl * sin(static_cast<double>(i)/sample_rate*2*M_PI*wave_hz);
// wave.push_back(static_cast<opus_int16>(ampl));
// }
//
// ope_encoder_write(file, wave.data(), wave.size());
// ope_encoder_drain(file);
// ope_encoder_destroy(file);
//}



PYBIND11_MODULE(opusenc, m) {

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,
"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).");
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.");
PYBIND11_MODULE(opusenc, m)
{
py::class_<OpusBufferedEncoder>(m, "OpusBufferedEncoder")
.def(py::init<int, int, int, int, int, int>(),
py::arg("sample_rate"), py::arg("channels"),
py::arg("bitrate") = OPUS_AUTO, py::arg("signal_type") = 0, py::arg("encoder_complexity") = 10, py::arg("decision_delay") = 0)
.def("write", &OpusBufferedEncoder::write)
.def("flush", &OpusBufferedEncoder::flush)
.def("close", &OpusBufferedEncoder::close);
}
Loading

0 comments on commit a5ce0fb

Please sign in to comment.