Files
klubhaus-doorbell/libraries/audio-tools/src/AudioTools/AudioCodecs/ContainerOgg.h
2026-02-12 21:00:02 -08:00

422 lines
11 KiB
C++

#pragma once
#include "AudioTools/AudioCodecs/AudioCodecsBase.h"
#include "AudioTools/AudioCodecs/CodecOpus.h"
#include "AudioTools/CoreAudio/Buffers.h"
#include "oggz.h"
#define OGG_READ_SIZE (1024)
#define OGG_DEFAULT_BUFFER_SIZE (OGG_READ_SIZE)
// #define OGG_DEFAULT_BUFFER_SIZE (246)
// #define OGG_READ_SIZE (512)
namespace audio_tools {
/**
* @brief Decoder for Ogg Container. Decodes a packet from an Ogg
* container. The Ogg begin segment contains the AudioInfo structure. You can
* subclass and overwrite the beginOfSegment() method to implement your own
* headers
* Dependency: https://github.com/pschatzmann/arduino-libopus
* @ingroup codecs
* @ingroup decoder
* @author Phil Schatzmann
* @copyright GPLv3
*/
class OggContainerDecoder : public ContainerDecoder {
public:
/**
* @brief Construct a new OggContainerDecoder object
*/
OggContainerDecoder() {
p_codec = &dec_copy;
out.setDecoder(p_codec);
}
OggContainerDecoder(AudioDecoder *decoder) { setDecoder(decoder); }
OggContainerDecoder(AudioDecoder &decoder) { setDecoder(&decoder); }
void setDecoder(AudioDecoder *decoder) {
p_codec = decoder;
out.setDecoder(p_codec);
}
/// Defines the output Stream
void setOutput(Print &print) override { out.setOutput(&print); }
void addNotifyAudioChange(AudioInfoSupport &bi) override {
out.addNotifyAudioChange(bi);
ContainerDecoder::addNotifyAudioChange(bi);
}
AudioInfo audioInfo() override { return out.audioInfo(); }
bool begin(AudioInfo info) override {
TRACED();
this->info = info;
return begin();
}
bool begin() override {
TRACED();
out.setAudioInfo(info);
out.begin();
if (p_oggz == nullptr) {
p_oggz = oggz_new(OGGZ_READ | OGGZ_AUTO); // OGGZ_NONSTRICT
is_open = true;
// Callback to Replace standard IO
if (oggz_io_set_read(p_oggz, ogg_io_read, this) != 0) {
LOGE("oggz_io_set_read");
is_open = false;
}
// Callback
if (oggz_set_read_callback(p_oggz, -1, read_packet, this) != 0) {
LOGE("oggz_set_read_callback");
is_open = false;
}
if (oggz_set_read_page(p_oggz, -1, read_page, this) != 0) {
LOGE("oggz_set_read_page");
is_open = false;
}
}
return is_open;
}
void end() override {
TRACED();
flush();
out.end();
is_open = false;
oggz_close(p_oggz);
p_oggz = nullptr;
}
void flush() {
LOGD("oggz_read...");
while ((oggz_read(p_oggz, OGG_READ_SIZE)) > 0)
;
}
virtual size_t write(const uint8_t *data, size_t len) override {
LOGD("write: %d", (int)len);
// fill buffer
size_t size_consumed = buffer.writeArray((uint8_t *)data, len);
if (buffer.availableForWrite() == 0) {
// Read all bytes into oggz, calling any read callbacks on the fly.
flush();
}
// write remaining bytes
if (size_consumed < len) {
size_consumed += buffer.writeArray((uint8_t *)data + size_consumed,
len - size_consumed);
flush();
}
return size_consumed;
}
virtual operator bool() override { return is_open; }
protected:
EncodedAudioOutput out;
CopyDecoder dec_copy;
AudioDecoder *p_codec = nullptr;
RingBuffer<uint8_t> buffer{OGG_DEFAULT_BUFFER_SIZE};
OGGZ *p_oggz = nullptr;
bool is_open = false;
long pos = 0;
// Final Stream Callback -> provide data to ogg
static size_t ogg_io_read(void *user_handle, void *buf, size_t n) {
LOGD("ogg_io_read: %d", (int)n);
size_t result = 0;
OggContainerDecoder *self = (OggContainerDecoder *)user_handle;
if (self->buffer.available() >= n) {
OggContainerDecoder *self = (OggContainerDecoder *)user_handle;
result = self->buffer.readArray((uint8_t *)buf, n);
self->pos += result;
} else {
result = 0;
}
return result;
}
// Process full packet
static int read_packet(OGGZ *oggz, oggz_packet *zp, long serialno,
void *user_data) {
LOGD("read_packet: %d", (int)zp->op.bytes);
OggContainerDecoder *self = (OggContainerDecoder *)user_data;
ogg_packet *op = &zp->op;
int result = op->bytes;
if (op->b_o_s) {
self->beginOfSegment(op);
} else if (op->e_o_s) {
self->endOfSegment(op);
} else {
if (memcmp(op->packet, "OpusTags", 8) == 0) {
self->beginOfSegment(op);
} else {
LOGD("process audio packet");
int eff = self->out.write(op->packet, op->bytes);
if (eff != result) {
LOGE("Incomplere write");
}
}
}
// 0 = success
return 0;
}
static int read_page(OGGZ *oggz, const ogg_page *og, long serialno,
void *user_data) {
LOGD("read_page: %d", (int)og->body_len);
// 0 = success
return 0;
}
virtual void beginOfSegment(ogg_packet *op) {
LOGD("bos");
if (op->bytes == sizeof(AudioInfo)) {
AudioInfo cfg(*(AudioInfo*)op->packet);
cfg.logInfo();
if (cfg.bits_per_sample == 16 || cfg.bits_per_sample == 24 ||
cfg.bits_per_sample == 32) {
setAudioInfo(cfg);
} else {
LOGE("Invalid AudioInfo")
}
} else {
LOGE("Invalid Header")
}
}
virtual void endOfSegment(ogg_packet *op) {
// end segment not supported
LOGW("e_o_s");
}
};
/**
* @brief Output class for the OggContainerEncoder. Each
* write is ending up as container entry
* @author Phil Schatzmann
* @copyright GPLv3
*/
class OggContainerOutput : public AudioOutput {
public:
// Empty Constructor - the output stream must be provided with begin()
OggContainerOutput() = default;
/// Defines the output Stream
void setOutput(Print &print) { p_out = &print; }
/// starts the processing using the actual AudioInfo
virtual bool begin() override {
TRACED();
assert(cfg.channels != 0);
assert(cfg.sample_rate != 0);
is_open = true;
if (p_oggz == nullptr) {
p_oggz = oggz_new(OGGZ_WRITE | OGGZ_NONSTRICT | OGGZ_AUTO);
serialno = oggz_serialno_new(p_oggz);
oggz_io_set_write(p_oggz, ogg_io_write, this);
packetno = 0;
granulepos = 0;
if (!writeHeader()) {
is_open = false;
LOGE("writeHeader");
}
}
return is_open;
}
/// stops the processing
void end() override {
TRACED();
writeFooter();
is_open = false;
oggz_close(p_oggz);
p_oggz = nullptr;
}
/// Writes raw data to be encoded and packaged
virtual size_t write(const uint8_t *data, size_t len) override {
if (data == nullptr) return 0;
LOGD("OggContainerOutput::write: %d", (int)len);
assert(cfg.channels != 0);
// encode the data
op.packet = (uint8_t *)data;
op.bytes = len;
if (op.bytes > 0) {
int bytes_per_sample = cfg.bits_per_sample / 8;
granulepos += op.bytes / bytes_per_sample; // sample
op.granulepos = granulepos;
op.b_o_s = false;
op.e_o_s = false;
op.packetno = packetno++;
is_audio = true;
if (!writePacket(op, OGGZ_FLUSH_AFTER)) {
return 0;
}
}
// trigger pysical write
while ((oggz_write(p_oggz, len)) > 0)
;
return len;
}
bool isOpen() { return is_open; }
protected:
Print *p_out = nullptr;
bool is_open = false;
OGGZ *p_oggz = nullptr;
ogg_packet op;
ogg_packet oh;
size_t granulepos = 0;
size_t packetno = 0;
long serialno = -1;
bool is_audio = false;
virtual bool writePacket(ogg_packet &op, int flag = 0) {
LOGD("writePacket: %d", (int)op.bytes);
long result = oggz_write_feed(p_oggz, &op, serialno, flag, NULL);
if (result < 0 && result != OGGZ_ERR_OUT_OF_MEMORY) {
LOGE("oggz_write_feed: %d", (int)result);
return false;
}
return true;
}
virtual bool writeHeader() {
TRACED();
oh.packet = (uint8_t *)&cfg;
oh.bytes = sizeof(AudioInfo);
oh.granulepos = 0;
oh.packetno = packetno++;
oh.b_o_s = true;
oh.e_o_s = false;
is_audio = false;
return writePacket(oh);
}
virtual bool writeFooter() {
TRACED();
op.packet = (uint8_t *)nullptr;
op.bytes = 0;
op.granulepos = granulepos;
op.packetno = packetno++;
op.b_o_s = false;
op.e_o_s = true;
is_audio = false;
return writePacket(op, OGGZ_FLUSH_AFTER);
}
// Final Stream Callback
static size_t ogg_io_write(void *user_handle, void *buf, size_t n) {
LOGD("ogg_io_write: %d", (int)n);
OggContainerOutput *self = (OggContainerOutput *)user_handle;
if (self == nullptr) {
LOGE("self is null");
return 0;
}
// self->out.write((uint8_t *)buf, n);
writeData<uint8_t>(self->p_out, (uint8_t *)buf, n);
// 0 = continue
return 0;
}
};
/**
* @brief Encoder for Ogg Container. Encodes a packet for an Ogg
* container. The Ogg begin segment contains the AudioInfo structure. You can
* subclass ond overwrite the writeHeader() method to implement your own header
* logic. When an optional encoder is specified in the constructor we package
* the encoded data.
* Dependency: https://github.com/pschatzmann/arduino-libopus
* @ingroup codecs
* @ingroup encoder
* @author Phil Schatzmann
* @copyright GPLv3
*/
class OggContainerEncoder : public AudioEncoder {
public:
// Empty Constructor - the output stream must be provided with begin()
OggContainerEncoder() = default;
OggContainerEncoder(AudioEncoder *encoder) { setEncoder(encoder); }
OggContainerEncoder(AudioEncoder &encoder) { setEncoder(&encoder); }
/// Defines the output Stream
void setOutput(Print &print) override { p_ogg->setOutput(print); }
/// Provides "audio/pcm"
const char *mime() override { return mime_pcm; }
/// We actually do nothing with this
virtual void setAudioInfo(AudioInfo info) override {
AudioEncoder::setAudioInfo(info);
p_ogg->setAudioInfo(info);
if (p_codec != nullptr) p_codec->setAudioInfo(info);
}
virtual bool begin(AudioInfo from) override {
setAudioInfo(from);
return begin();
}
/// starts the processing using the actual AudioInfo
virtual bool begin() override {
TRACED();
p_ogg->begin();
if (p_codec==nullptr) return false;
p_codec->setOutput(*p_ogg);
return p_codec->begin(p_ogg->audioInfo());
}
/// stops the processing
void end() override {
TRACED();
if (p_codec != nullptr) p_codec->end();
p_ogg->end();
}
/// Writes raw data to be encoded and packaged
virtual size_t write(const uint8_t *data, size_t len) override {
if (!p_ogg->isOpen() || data == nullptr) return 0;
LOGD("OggContainerEncoder::write: %d", (int)len);
size_t result = 0;
if (p_codec == nullptr) {
result = p_ogg->write((const uint8_t *)data, len);
} else {
result = p_codec->write(data, len);
}
return result;
}
operator bool() override { return p_ogg->isOpen(); }
bool isOpen() { return p_ogg->isOpen(); }
protected:
AudioEncoder *p_codec = nullptr;
OggContainerOutput ogg;
OggContainerOutput *p_ogg = &ogg;
void setEncoder(AudioEncoder *enc) { p_codec = enc; }
/// Replace the ogg output class
void setOggOutput(OggContainerOutput *out) { p_ogg = out; }
};
} // namespace audio_tools