Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opus codec #7675

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions examples/oggzmodule/micropython.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This top-level micropython.cmake is responsible for listing
# the individual modules we want to include.
# Paths are absolute, and ${CMAKE_CURRENT_LIST_DIR} can be
# used to prefix subdirectories.

# Add opus
include(${CMAKE_CURRENT_LIST_DIR}/oggz/micropython.cmake)
57 changes: 57 additions & 0 deletions examples/oggzmodule/oggz/micropython.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Create an INTERFACE library for our C module.
add_library(usermod_opus INTERFACE)

# Add our source files to the lib
target_sources(usermod_opus INTERFACE
# ${CMAKE_CURRENT_LIST_DIR}/opusmodule.c
${CMAKE_CURRENT_LIST_DIR}/oggzmodule.c
)

# Add the current directory as an include directory.
target_include_directories(usermod_opus INTERFACE
${CMAKE_CURRENT_LIST_DIR}
)

add_compile_definitions(INCLUDE_STDINT_H HAVE_CONFIG_H ARDUINO)

# opus
set(OPUS_FIXED_POINT ON)
set(OPUS_DISABLE_FLOAT_API ON)
set(OPUS_DISABLE_EXAMPLES ON)
set(OPUS_DISABLE_DOCS ON)

# opusfile
set(OP_DISABLE_HTTP ON)
set(OP_DISABLE_FLOAT_API ON)
set(OP_FIXED_POINT ON)
set(OP_DISABLE_EXAMPLES ON)
set(OP_DISABLE_DOCS ON)
set(OP_HAVE_LIBM OFF)

# set_property(TARGET ogg PROPERTY C_STANDARD 99)
# set_property(TARGET opus PROPERTY C_STANDARD 99)

# build with libopus
include(FetchContent)
FetchContent_Declare(arduino_libopus GIT_REPOSITORY "https://github.com/pschatzmann/arduino-libopus.git" GIT_TAG main )
FetchContent_GetProperties(arduino_libopus)
if(NOT arduino_libopus_POPULATED)
FetchContent_Populate(arduino_libopus)
add_subdirectory(${arduino_libopus_SOURCE_DIR})
endif()

# # the arduino-libopus distribution doesn't include opusfile
# include(FetchContent)
# FetchContent_Declare(opusfile GIT_REPOSITORY "https://github.com/dholth/opusfile.git" GIT_TAG micropython )
# FetchContent_GetProperties(opusfile)
# if(NOT opusfile)
# FetchContent_Populate(opusfile)
# add_subdirectory(${opusfile_SOURCE_DIR})
# endif()

# set_property(TARGET opusfile PROPERTY C_STANDARD 99)

# target_include_directories(opusfile PUBLIC $<BUILD_INTERFACE:${arduino_libopus_SOURCE_DIR}/src>)

# Link our INTERFACE library to the usermod target.
target_link_libraries(usermod INTERFACE usermod_opus arduino_libopus)
14 changes: 14 additions & 0 deletions examples/oggzmodule/oggz/micropython.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
OGGZ_MOD_DIR := $(USERMOD_DIR)

# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(OGGZ_MOD_DIR)/oggzmodule.c

ARDUINO_LIBOPUS_DIR = $(TOP)/../arduino-libopus
include $(ARDUINO_LIBOPUS_DIR)/micropython.mk

CFLAGS_USERMOD += -DHAVE_CONFIG_H \
-I$(ARDUINO_LIBOPUS_DIR)/src \
-Wno-unused-but-set-variable \
-Wno-unused-function \
-fsingle-precision-constant \
-Wno-sizeof-array-div -Wno-stringop-overread
223 changes: 223 additions & 0 deletions examples/oggzmodule/oggz/oggzmodule.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Include MicroPython API.
#include "py/runtime.h"
#include "py/stream.h"

#include <string.h>

#include "oggz/oggz.h"
#include "opus.h"

// small standard library
int _fstat;
int __gnu_thumb1_case_shi;
int _close;
int _isatty;
int _lseek;

typedef struct _mp_obj_oggz_t
{
mp_obj_base_t base;
mp_obj_t stream; // retain a reference to prevent GC from reclaiming it
OGGZ *oggz;
OpusDecoder *opus;
oggz_packet *current_packet; // NULL unless currently decoding opus
const char *content_type;
} mp_obj_oggz_t;

STATIC int op_stream_posix_read(void *_stream, unsigned char *_ptr,
int _nbytes)
{
int errcode;
int bytes_read;
bytes_read = mp_stream_rw(_stream, _ptr, _nbytes, &errcode, MP_STREAM_RW_READ);
return bytes_read;
}

STATIC int oggz_packet_cb(OGGZ *oggz, oggz_packet *packet, long serialno,
void *user_data)
{
mp_obj_oggz_t *self = (mp_obj_oggz_t *)user_data;
self->current_packet = packet;
self->content_type = oggz_stream_get_content_type(oggz, serialno);
return OGGZ_STOP_OK;
}

// e.g.
// STATIC mp_obj_t framebuf_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {

STATIC mp_obj_t mp_oggz_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args)
{
mp_arg_check_num(n_args, n_kw, 1, 1, false);
mp_obj_oggz_t *self = m_new_obj(mp_obj_oggz_t);

// make sure we have a stream
mp_get_stream_raise(args[0], MP_STREAM_OP_READ);

mp_obj_t *stream = args[0];

self->base.type = (mp_obj_type_t *)type;
self->stream = stream;

self->oggz = oggz_new(OGGZ_READ);
if (self->oggz == NULL)
{
mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("oggz error"));
}

int error;

self->opus = opus_decoder_create(48000, 2, &error);
if (error != 0)
{
mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("opus error %d"),
error);
}

error = oggz_io_set_read(self->oggz, (OggzIORead)&op_stream_posix_read,
self->stream);
if (error != 0)
{
mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("oggz error %d"),
error);
}

error = oggz_set_read_callback(self->oggz, -1, oggz_packet_cb, self);
if (error != 0)
{
mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("oggz error %d"),
error);
}

return MP_OBJ_FROM_PTR(self);
}

STATIC mp_obj_t mp_oggz_close(mp_obj_t self_in)
{
mp_obj_oggz_t *self = MP_OBJ_TO_PTR(self_in);
int error = oggz_close(self->oggz);
self->oggz = NULL;
opus_decoder_destroy(self->opus);
self->opus = NULL;
if (error != 0)
{
mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("oggz error %d"),
error);
}
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_oggz_close_obj, mp_oggz_close);

STATIC mp_obj_t mp_oggz_read(mp_obj_t self_in, mp_obj_t n)
{
mp_obj_oggz_t *self = MP_OBJ_TO_PTR(self_in);
int bytes_read = oggz_read(self->oggz, mp_obj_get_int(n));
if (bytes_read < 0 && bytes_read != OGGZ_ERR_STOP_OK)
{
mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("oggz error %d"),
bytes_read);
}
return mp_obj_new_int(bytes_read);
}
// Define a Python reference to the function above.
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_oggz_read_obj, mp_oggz_read);

// Decode opus samples to buffer
// Return number of samples decoded (bytes = samples * 16 bit * 2)
STATIC mp_obj_t mp_oggz_decode_opus(mp_obj_t self_in, mp_obj_t buf_obj)
{
mp_obj_oggz_t *self = MP_OBJ_TO_PTR(self_in);
int bytes_read, samples_read;
mp_buffer_info_t bufinfo;

mp_get_buffer_raise(buf_obj, &bufinfo, MP_BUFFER_READ);

self->current_packet = NULL;
bytes_read = oggz_read(self->oggz, 512);
if ((bytes_read < 0) && (bytes_read != OGGZ_ERR_STOP_OK))
{
mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("oggz error %d"),
bytes_read);
}
if (self->current_packet == NULL)
{
samples_read = 0;
}
else
{
samples_read = opus_decode(self->opus, self->current_packet->op.packet,
self->current_packet->op.bytes, bufinfo.buf,
bufinfo.len / 4, 0);

// OPUS_BAD_ARG -1, BUFFER_TOO_SMALL -2, INTERNAL_ERROR -3,
// INVALID_PACKET -4, UNIMPL -5, INVALID_STATE -6 ALLOC_FAIL -7
}

return mp_obj_new_tuple(
4, ((mp_obj_t[]){
mp_obj_new_str(self->content_type, strlen(self->content_type)),
mp_obj_new_int(samples_read), mp_obj_new_int(bytes_read),
mp_obj_new_int(bufinfo.len)}));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_oggz_decode_opus_obj, mp_oggz_decode_opus);

STATIC const mp_rom_map_elem_t oggz_locals_dict_table[] = {
{MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mp_oggz_close_obj)},
{MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_oggz_read_obj)},
{MP_ROM_QSTR(MP_QSTR_decode_opus), MP_ROM_PTR(&mp_oggz_decode_opus_obj)},
};
STATIC MP_DEFINE_CONST_DICT(oggz_locals_dict, oggz_locals_dict_table);

// This defines the type(opus) object.
#ifdef MP_DEFINE_CONST_OBJ_TYPE

MP_DEFINE_CONST_OBJ_TYPE(mp_oggz_type, MP_QSTR_oggz, MP_TYPE_FLAG_NONE,
make_new, mp_oggz_make_new, locals_dict,
&oggz_locals_dict);

#else

STATIC const mp_obj_type_t mp_oggz_type = {
{&mp_type_type},
.name = MP_QSTR_oggz,
.make_new = mp_oggz_make_new,
.locals_dict = (mp_obj_dict_t *)&oggz_locals_dict,
};

// e.g.
// STATIC const mp_obj_type_t uhashlib_md5_type = {
// { &mp_type_type },
// .name = MP_QSTR_md5,
// .make_new = uhashlib_md5_make_new,
// .locals_dict = (void *)&uhashlib_md5_locals_dict,
// };

#endif

// Define all properties of the module.
// Table entries are key/value pairs of the attribute name (a string)
// and the MicroPython object reference.
// All identifiers and strings are written as MP_QSTR_xxx and will be
// optimized to word-sized integers by the build system (interned strings).
STATIC const mp_rom_map_elem_t oggz_module_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_oggz)},
{MP_ROM_QSTR(MP_QSTR_oggz), MP_ROM_PTR(&mp_oggz_type)},
};
STATIC MP_DEFINE_CONST_DICT(oggz_module_globals, oggz_module_globals_table);

// Define module object.
const mp_obj_module_t oggz_module = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t *)&oggz_module_globals,
};

// Register the module to make it available in Python.
MP_REGISTER_MODULE(MP_QSTR_oggz, oggz_module, 1);

// Otherwise missing symbols
// int fprintf (void *__restrict, const char *__restrict, ...) {}

// extern int __errno = 0;

// void abort_(void) {
// nlr_raise(mp_obj_new_exception(mp_load_global(MP_QSTR_RuntimeError)));
// }
17 changes: 17 additions & 0 deletions examples/oggzmodule/opusdec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import opus

music = open("ehren-paper_lights-96.opus", "rb")

op = opus.opus(music)
buffer = memoryview(bytearray(23040))
output = open("decoded.raw", "w+b")

min_samples = 10000000
max_samples = 0

while samples := op.read_stereo(buffer):
min_samples = min(samples, min_samples)
max_samples = max(samples, max_samples)
output.write(buffer[:samples*4])

print(f"{min_samples}, {max_samples} minimum and maximum samples decoded per call")
2 changes: 1 addition & 1 deletion ports/raspberrypi/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ endif
# Remove -Wno-stringop-overflow after we can test with CI's GCC 10. Mac's looks weird.
DISABLE_WARNINGS = -Wno-stringop-overflow -Wno-cast-align

CFLAGS += $(INC) -Wall -Werror -std=gnu11 -nostdlib -fshort-enums $(BASE_CFLAGS) $(CFLAGS_MOD) $(COPT) $(DISABLE_WARNINGS) -Werror=missing-prototypes
CFLAGS += $(INC) -Wall -Werror -std=gnu11 -nostdlib -fshort-enums $(BASE_CFLAGS) $(CFLAGS_MOD) $(COPT) $(DISABLE_WARNINGS)

CFLAGS += \
-march=armv6-m \
Expand Down
3 changes: 3 additions & 0 deletions ports/raspberrypi/mpconfigport.mk
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# All raspberrypi ports have longints.
LONGINT_IMPL = MPZ

CFLAGS += -DMODULE_OGGZ_ENABLED=1
USER_C_MODULES += ../../examples/oggzmodule

CIRCUITPY_OPTIMIZE_PROPERTY_FLASH_SIZE ?= 1
# CYW43 support does not provide settable MAC addresses for station or AP.
CIRCUITPY_WIFI_RADIO_SETTABLE_MAC_ADDRESS = 0
Expand Down