422 lines
11 KiB
C++
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
|