Skip to content

Commit

Permalink
Add a simple class to handle G.722 encode / decode task.
Browse files Browse the repository at this point in the history
  • Loading branch information
sobomax committed Apr 22, 2024
1 parent 70af5c6 commit 9ec80fc
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 1 deletion.
34 changes: 33 additions & 1 deletion .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Install Dependencies
run: |
Expand All @@ -54,3 +54,35 @@ jobs:
- name: Test with CMake
run: bmake -C build test

build_and_test_python:
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11'. '3.12']
compiler: ['gcc', 'clang']

runs-on: ubuntu-latest
env:
COMPILER: ${{ matrix.compiler }}
PYTHON_CMD: "python${{ matrix.python-version }}"

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: before_install
run: ${PYTHON_CMD} python/setup.py build

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel
- name: script
run: pip install python/
242 changes: 242 additions & 0 deletions python/G722_mod.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
#include <stdbool.h>

#include <Python.h>

#include "g722_encoder.h"
#include "g722_decoder.h"

#define MODULE_BASENAME G722

#define CONCATENATE_DETAIL(x, y) x##y
#define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y)

#if !defined(DEBUG_MOD)
#define MODULE_NAME MODULE_BASENAME
#else
#define MODULE_NAME CONCATENATE(MODULE_BASENAME, _debug)
#endif

#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)

#define MODULE_NAME_STR TOSTRING(MODULE_NAME)
#define PY_INIT_FUNC CONCATENATE(PyInit_, MODULE_NAME)

typedef struct {
PyObject_HEAD
G722_DEC_CTX *g722_dctx;
G722_ENC_CTX *g722_ectx;
int sample_rate;
int bit_rate;
} PyG722;

static int PyG722_init(PyG722* self, PyObject* args, PyObject* kwds) {
int sample_rate, bit_rate, options;
static char *kwlist[] = {"sample_rate", "bit_rate", NULL};

if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", kwlist, &sample_rate, &bit_rate)) {
return -1;
}

if (sample_rate != 8000 && sample_rate != 16000) {
PyErr_SetString(PyExc_ValueError, "Sample rate must be 8000 or 16000");
return -1;
}

if (bit_rate != 48000 && bit_rate != 56000 && bit_rate != 64000) {
PyErr_SetString(PyExc_ValueError, "Bit rate must be 48000, 56000 or 64000");
return -1;
}
options = (sample_rate == 8000) ? G722_SAMPLE_RATE_8000 : G722_DEFAULT;
self->g722_ectx = g722_encoder_new(bit_rate, options);
if(self->g722_ectx == NULL) {
PyErr_SetString(PyExc_RuntimeError, "Error initializing G.722 encoder");
return -1;
}
self->g722_dctx = g722_decoder_new(bit_rate, options);
if(self->g722_dctx == NULL) {
g722_encoder_destroy(self->g722_ectx);
PyErr_SetString(PyExc_RuntimeError, "Error initializing G.722 decoder");
return -1;
}
self->sample_rate = sample_rate;
self->bit_rate = bit_rate;

return 0;
}

// The __del__ method for PyG722 objects
static void PyG722_dealloc(PyG722* self) {
g722_encoder_destroy(self->g722_ectx);
g722_decoder_destroy(self->g722_dctx);
Py_TYPE(self)->tp_free((PyObject*)self);
}

// The encode method for PyG722 objects
static PyObject *
PyG722_encode(PyG722* self, PyObject* args) {
PyObject* item;
PyObject* seq;
int16_t* array;
Py_ssize_t length, i, olength;

PyObject *rval = NULL;
if (!PyArg_ParseTuple(args, "O", &item)) {
PyErr_SetString(PyExc_TypeError, "Takes exactly one argument");
goto e0;
}

// Convert PyObject to a sequence if possible
seq = PySequence_Fast(item, "Expected a sequence");
if (seq == NULL) {
PyErr_SetString(PyExc_TypeError, "Expected a sequence");
goto e0;
}

// Get the length of the sequence
length = PySequence_Size(seq);
if (length == -1) {
PyErr_SetString(PyExc_TypeError, "Error getting sequence length");
goto e1;
}

// Allocate memory for the int array
array = (int16_t*) malloc(length * sizeof(array[0]));
if (!array) {
rval = PyErr_NoMemory();
goto e1;
}
for (i = 0; i < length; i++) {
PyObject* temp_item = PySequence_Fast_GET_ITEM(seq, i); // Borrowed reference, no need to Py_DECREF
long tv = PyLong_AsLong(temp_item);
if (PyErr_Occurred()) {
goto e2;
}
if (tv < -32768 || tv > 32767) {
PyErr_SetString(PyExc_ValueError, "Value out of range");
goto e2;
}
array[i] = (int16_t)tv;
}
olength = self->sample_rate == 8000 ? length : length / 2;
PyObject *obuf_obj = PyBytes_FromStringAndSize(NULL, olength);
if (obuf_obj == NULL) {
rval = PyErr_NoMemory();
goto e2;
}
uint8_t *buffer = (uint8_t *)PyBytes_AsString(obuf_obj);
if (!buffer) {
goto e3;
}
g722_encode(self->g722_ectx, array, length, buffer);
rval = obuf_obj;
goto e2;
e3:
Py_DECREF(obuf_obj);
e2:
free(array);
e1:
Py_DECREF(seq);
e0:
return rval;
}

// The get method for PyG722 objects
static PyObject *
PyG722_decode(PyG722* self, PyObject* args) {
PyObject* item;
uint8_t* buffer;
int16_t* array;
Py_ssize_t length, olength, i;

// Parse the input tuple to get a bytes object
if (!PyArg_ParseTuple(args, "O", &item)) {
PyErr_SetString(PyExc_TypeError, "Argument must be a bytes object");
return NULL;
}

// Ensure the object is a bytes object
if (!PyBytes_Check(item)) {
PyErr_SetString(PyExc_TypeError, "Argument must be a bytes object");
return NULL;
}

// Get the buffer and its length from the bytes object
buffer = (uint8_t *)PyBytes_AsString(item);
if (!buffer) {
return NULL; // PyErr_SetString is called by PyBytes_AsString if something goes wrong
}
length = PyBytes_Size(item);
if (length < 0) {
return NULL; // PyErr_SetString is called by PyBytes_Size if something goes wrong
}
olength = self->sample_rate == 8000 ? length : length * 2;
array = (int16_t*) malloc(olength * sizeof(array[0]));
if (array == NULL) {
return PyErr_NoMemory();
}
g722_decode(self->g722_dctx, buffer, length, array);
// Create a new list to hold the integers
PyObject *listObj = PyList_New(0);
if (listObj == NULL) goto e0;

// Convert each int16_t in array to a Python integer and append to list
for (i = 0; i < olength; i++) {
PyObject* intObj = PyLong_FromLong(array[i]);
if (intObj == NULL) goto e1;
PyList_Append(listObj, intObj);
Py_DECREF(intObj); // PyList_Append increments the ref count
}

// Cleanup and return the list
free(array);
return listObj;
e1:
Py_DECREF(listObj);
e0:
free(array);
return PyErr_NoMemory();
}

static PyMethodDef PyG722_methods[] = {
{"encode", (PyCFunction)PyG722_encode, METH_VARARGS, "Encode signed linear PCM samples to G.722 format"},
{"decode", (PyCFunction)PyG722_decode, METH_VARARGS, "Decode G.722 format to signed linear PCM samples"},
{NULL} // Sentinel
};

static PyTypeObject PyG722Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = MODULE_NAME_STR "." MODULE_NAME_STR,
.tp_doc = "Implementation of ITU-T G.722 audio codec in Python using C extension.",
.tp_basicsize = sizeof(PyG722),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
.tp_init = (initproc)PyG722_init,
.tp_dealloc = (destructor)PyG722_dealloc,
.tp_methods = PyG722_methods,
};

static struct PyModuleDef G722_module = {
PyModuleDef_HEAD_INIT,
.m_name = MODULE_NAME_STR,
.m_doc = "Python interface for the ITU-T G.722 audio codec.",
.m_size = -1,
};

// Module initialization function
PyMODINIT_FUNC PY_INIT_FUNC(void) {
PyObject* module;
if (PyType_Ready(&PyG722Type) < 0)
return NULL;

module = PyModule_Create(&G722_module);
if (module == NULL)
return NULL;

Py_INCREF(&PyG722Type);
PyModule_AddObject(module, MODULE_NAME_STR, (PyObject*)&PyG722Type);

return module;
}

46 changes: 46 additions & 0 deletions python/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from sys import exit
from distutils.core import setup, Extension
from setuptools.command.test import test as TestCommand
from os.path import realpath, dirname, join as path_join
from sys import argv as sys_argv

mod_name = 'G722'
mod_name_dbg = mod_name + '_debug'

mod_dir = dirname(realpath(sys_argv[0]))
src_dir = '../'

compile_args = [f'-I{src_dir}', '-flto']
link_args = ['-flto', '-Wl,--version-script=symbols.map']
debug_cflags = ['-g3', '-O0', '-DDEBUG_MOD']
mod_common_args = {
'sources': [mod_name + '_mod.c', src_dir + 'g722_decode.c', src_dir + 'g722_encode.c'],
'extra_compile_args': compile_args,
'extra_link_args': link_args
}
mod_debug_args = mod_common_args.copy()
mod_debug_args['extra_compile_args'] = mod_debug_args['extra_compile_args'] + debug_cflags

module1 = Extension(mod_name, **mod_common_args)
module2 = Extension(mod_name_dbg, **mod_debug_args)

class PyTest(TestCommand):
user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")]

def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = []

def run_tests(self):
import pytest
errno = pytest.main(self.pytest_args)
exit(errno)

setup (name = mod_name,
version = '1.0',
description = 'This is a package for G.722 module',
ext_modules = [module1, module2],
tests_require=['pytest'],
cmdclass={'test': PyTest},
)

6 changes: 6 additions & 0 deletions python/symbols.map
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
global:
PyInit_G722*;
local:
*;
};

0 comments on commit 9ec80fc

Please sign in to comment.