300 lines
8.1 KiB
C++
300 lines
8.1 KiB
C++
/**
|
|
* @file CodecAptx.h
|
|
* @author Phil Schatzmann
|
|
* @brief Codec for aptx using https://github.com/pschatzmann/libopenaptx
|
|
* @version 0.1
|
|
* @date 2022-04-24
|
|
*
|
|
* @copyright Copyright (c) 2022
|
|
*
|
|
*/
|
|
#pragma once
|
|
#include "AudioToolsConfig.h"
|
|
#include "AudioTools/AudioCodecs/AudioCodecsBase.h"
|
|
#include "openaptx.h"
|
|
|
|
namespace audio_tools {
|
|
|
|
/**
|
|
* @brief Decoder for OpenAptx. Depends on
|
|
* https://github.com/pschatzmann/libopenaptx
|
|
* @ingroup codecs
|
|
* @ingroup decoder
|
|
* @author Phil Schatzmann
|
|
* @copyright GPLv3
|
|
*/
|
|
class APTXDecoder : public AudioDecoder {
|
|
public:
|
|
APTXDecoder(bool isHd = false) {
|
|
is_hd = isHd;
|
|
info.sample_rate = 44100;
|
|
info.channels = 2;
|
|
info.bits_per_sample = isHd ? 24 : 16;
|
|
}
|
|
|
|
bool begin() override {
|
|
TRACEI();
|
|
ctx = aptx_init(is_hd);
|
|
is_first_write = true;
|
|
notifyAudioChange(info);
|
|
return ctx != nullptr;
|
|
}
|
|
|
|
void end() override {
|
|
TRACEI();
|
|
bool dropped = aptx_decode_sync_finish(ctx);
|
|
aptx_finish(ctx);
|
|
ctx = nullptr;
|
|
}
|
|
|
|
virtual void setOutput(Print &out_stream) { p_print = &out_stream; }
|
|
|
|
operator bool() { return ctx != nullptr; }
|
|
|
|
virtual size_t write(const uint8_t *data, size_t len) {
|
|
LOGI("write: %d", len);
|
|
bool is_ok = true;
|
|
size_t dropped;
|
|
int synced;
|
|
|
|
if (is_first_write) {
|
|
is_first_write = false;
|
|
if (!checkPrefix(data, len)) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
output_buffer.resize(len * 10);
|
|
memset(output_buffer.data(), 0, output_buffer.size());
|
|
processed = aptx_decode_sync(ctx, (const uint8_t *)data, len,
|
|
output_buffer.data(), output_buffer.size(),
|
|
&written, &synced, &dropped);
|
|
|
|
checkSync(synced, dropped, is_ok);
|
|
|
|
// If we have not decoded all supplied samples then decoding unrecoverable
|
|
// failed
|
|
if (processed != len) {
|
|
LOGE("aptX decoding reqested: %d eff: %d", len, processed);
|
|
is_ok = false;
|
|
}
|
|
|
|
writeData(written, is_ok);
|
|
|
|
return is_ok ? len : 0;
|
|
}
|
|
|
|
protected:
|
|
struct aptx_context *ctx = nullptr;
|
|
Print *p_print = nullptr;
|
|
bool is_first_write = true;
|
|
Vector<uint8_t> output_buffer;
|
|
bool is_hd;
|
|
size_t processed;
|
|
size_t written;
|
|
bool syncing;
|
|
|
|
/// Converts the data to 16 bit and writes it to final output
|
|
void writeData(size_t written, bool &is_ok) {
|
|
if (written > 0) {
|
|
int samples = written / 3;
|
|
LOGI("written: %d", written);
|
|
LOGI("samples: %d", samples);
|
|
int24_t *p_int24 = (int24_t *)output_buffer.data();
|
|
int16_t *p_int16 = (int16_t *)output_buffer.data();
|
|
for (int j = 0; j < samples; j++) {
|
|
p_int16[j] = p_int24[j].getAndScale16();
|
|
}
|
|
|
|
if (p_print->write((uint8_t *)output_buffer.data(), samples * 2) !=
|
|
samples * 2) {
|
|
LOGE("aptX decoding failed to write decoded data");
|
|
is_ok = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Checks the syncronization
|
|
void checkSync(bool synced, bool dropped, bool &is_ok) {
|
|
/* Check all possible states of synced, syncing and dropped status */
|
|
if (!synced) {
|
|
if (!syncing) {
|
|
LOGE("aptX decoding failed, synchronizing");
|
|
syncing = true;
|
|
is_ok = false;
|
|
}
|
|
if (dropped) {
|
|
LOGE("aptX synchronization successful, dropped %lu byte%s",
|
|
(unsigned long)dropped, (dropped != 1) ? "s" : "");
|
|
syncing = false;
|
|
is_ok = true;
|
|
}
|
|
if (!syncing) {
|
|
LOGE("aptX decoding failed, synchronizing");
|
|
syncing = true;
|
|
is_ok = false;
|
|
}
|
|
} else {
|
|
if (dropped) {
|
|
if (!syncing) LOGE("aptX decoding failed, synchronizing");
|
|
LOGE("aptX synchronization successful, dropped %lu byte%s",
|
|
(unsigned long)dropped, (dropped != 1) ? "s" : "");
|
|
syncing = false;
|
|
is_ok = false;
|
|
} else if (syncing) {
|
|
LOGI("aptX synchronization successful");
|
|
syncing = false;
|
|
is_ok = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Checks the prefix of the received data
|
|
bool checkPrefix(const void *input_buffer, size_t length) {
|
|
bool result = true;
|
|
if (length >= 4 && memcmp(input_buffer, "\x4b\xbf\x4b\xbf", 4) == 0) {
|
|
if (is_hd) {
|
|
LOGE("aptX audio stream (not aptX HD)");
|
|
result = false;
|
|
}
|
|
} else if (length >= 6 &&
|
|
memcmp(input_buffer, "\x73\xbe\xff\x73\xbe\xff", 6) == 0) {
|
|
if (!is_hd) {
|
|
LOGE("aptX HD audio stream");
|
|
result = false;
|
|
}
|
|
} else {
|
|
if (length >= 4 && memcmp(input_buffer, "\x6b\xbf\x6b\xbf", 4) == 0) {
|
|
LOGE("standard aptX audio stream - not supported");
|
|
result = false;
|
|
} else {
|
|
LOGE("No aptX nor aptX HD audio stream");
|
|
result = false;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief Encoder for OpenAptx - Depends on
|
|
* https://github.com/pschatzmann/libopenaptx
|
|
* @ingroup codecs
|
|
* @ingroup encoder
|
|
* @author Phil Schatzmann
|
|
* @copyright GPLv3
|
|
*/
|
|
class APTXEncoder : public AudioEncoder {
|
|
public:
|
|
APTXEncoder(bool isHd = false) {
|
|
is_hd = isHd;
|
|
info.sample_rate = 44100;
|
|
info.channels = 2;
|
|
info.bits_per_sample = isHd ? 24 : 16;
|
|
}
|
|
|
|
bool begin() {
|
|
TRACEI();
|
|
input_buffer.resize(4 * 2);
|
|
output_buffer.resize(100 * (is_hd ? 6 : 4));
|
|
|
|
LOGI("input_buffer.size: %d", input_buffer.size());
|
|
LOGI("output_buffer.size: %d", output_buffer.size());
|
|
LOGI("is_hd: %s", is_hd ? "true" : "false");
|
|
ctx = aptx_init(is_hd);
|
|
return ctx!=nullptr;
|
|
}
|
|
|
|
virtual void end() {
|
|
TRACEI();
|
|
if (ctx != nullptr) {
|
|
size_t output_written = 0;
|
|
aptx_encode_finish(ctx, output_buffer.data(), output_buffer.size(),
|
|
&output_written);
|
|
if (output_written > 0) {
|
|
// write result to final output
|
|
int written = p_print->write((const uint8_t *)output_buffer.data(),
|
|
output_written);
|
|
if (written != output_written) {
|
|
LOGE("write requested: %d eff: %d", output_written, written);
|
|
}
|
|
}
|
|
aptx_finish(ctx);
|
|
ctx = nullptr;
|
|
}
|
|
}
|
|
|
|
virtual const char *mime() { return "audio/aptx"; }
|
|
|
|
virtual void setAudioInfo(AudioInfo info) {
|
|
AudioEncoder::setAudioInfo(info);
|
|
switch (info.bits_per_sample) {
|
|
case 16:
|
|
is_hd = false;
|
|
break;
|
|
case 24:
|
|
is_hd = true;
|
|
break;
|
|
default:
|
|
LOGE("invalid bits_per_sample: %d", info.bits_per_sample);
|
|
}
|
|
}
|
|
|
|
virtual void setOutput(Print &out_stream) { p_print = &out_stream; }
|
|
|
|
operator bool() { return ctx != nullptr; }
|
|
|
|
virtual size_t write(const uint8_t *data, size_t len) {
|
|
LOGI("write: %d", len);
|
|
if (ctx == nullptr) return 0;
|
|
size_t output_written = 0;
|
|
|
|
// process all bytes
|
|
int16_t *in_ptr16 = (int16_t *)data;
|
|
int in_samples = len / 2;
|
|
for (int j = 0; j < in_samples; j++) {
|
|
input_buffer[input_pos++].setAndScale16(in_ptr16[j]);
|
|
|
|
// if input_buffer is full we encode
|
|
if (input_pos >= input_buffer.size()) {
|
|
size_t result = aptx_encode(
|
|
ctx, (const uint8_t *)input_buffer.data(), input_buffer.size() * 3,
|
|
output_buffer.data() + output_pos,
|
|
output_buffer.size() - output_pos, &output_written);
|
|
|
|
output_pos += output_written;
|
|
|
|
if (result != input_buffer.size() * 3) {
|
|
LOGW("encode requested: %d, eff: %d", input_buffer.size() * 3,
|
|
result);
|
|
}
|
|
|
|
// if output buffer is full we write the result
|
|
if (output_pos + output_pos >= output_buffer.size()) {
|
|
int written =
|
|
p_print->write((const uint8_t *)output_buffer.data(), output_pos);
|
|
if (written != output_pos) {
|
|
LOGE("write requested: %d eff: %d", output_pos, written);
|
|
}
|
|
// restart at beginning of output buffer
|
|
output_pos = 0;
|
|
}
|
|
// restart at beginning of input buffer
|
|
input_pos = 0;
|
|
}
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
protected:
|
|
bool is_hd;
|
|
Vector<int24_t> input_buffer{4 * 2};
|
|
Vector<uint8_t> output_buffer;
|
|
int input_pos = 0;
|
|
int output_pos = 0;
|
|
Print *p_print = nullptr;
|
|
struct aptx_context *ctx = nullptr;
|
|
};
|
|
|
|
} // namespace audio_tools
|