snapcast/server/encoder/flac_encoder.cpp
2024-04-04 19:02:45 +02:00

240 lines
8.4 KiB
C++

/***
This file is part of snapcast
Copyright (C) 2014-2024 Johannes Pohl
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// prototype/interface header file
#include "flac_encoder.hpp"
// local headers
#include "common/aixlog.hpp"
#include "common/snap_exception.hpp"
#include "common/str_compat.hpp"
// 3rd party headers
#include "FLAC/metadata.h"
// standard headers
#include <iostream>
using namespace std;
namespace encoder
{
static constexpr auto LOG_TAG = "FlacEnc";
FlacEncoder::FlacEncoder(const std::string& codecOptions) : Encoder(codecOptions), encoder_(nullptr), pcmBufferSize_(0), encodedSamples_(0), flacChunk_(nullptr)
{
headerChunk_.reset(new msg::CodecHeader("flac"));
pcmBuffer_ = static_cast<FLAC__int32*>(malloc(pcmBufferSize_ * sizeof(FLAC__int32)));
metadata_[0] = nullptr;
metadata_[1] = nullptr;
}
FlacEncoder::~FlacEncoder()
{
if (encoder_ != nullptr)
{
FLAC__stream_encoder_finish(encoder_);
FLAC__metadata_object_delete(metadata_[0]);
FLAC__metadata_object_delete(metadata_[1]);
FLAC__stream_encoder_delete(encoder_);
}
free(pcmBuffer_);
}
std::string FlacEncoder::getAvailableOptions() const
{
return "compression level: [0..8]";
}
std::string FlacEncoder::getDefaultOptions() const
{
return "2";
}
std::string FlacEncoder::name() const
{
return "flac";
}
void FlacEncoder::encode(const msg::PcmChunk& chunk)
{
if (flacChunk_ == nullptr)
flacChunk_ = make_shared<msg::PcmChunk>(chunk.format, 0);
int samples = chunk.getSampleCount();
int frames = chunk.getFrameCount();
// LOG(TRACE, LOG_TAG) << "payload: " << chunk.payloadSize << "\tframes: " << frames << "\tsamples: " << samples
// << "\tduration: " << chunk.duration<chronos::msec>().count() << ", format: " << chunk.format.toString() << "\n";
if (pcmBufferSize_ < samples)
{
pcmBufferSize_ = samples;
pcmBuffer_ = static_cast<FLAC__int32*>(realloc(pcmBuffer_, pcmBufferSize_ * sizeof(FLAC__int32)));
}
auto clip = [](int32_t min, int32_t max, int32_t value) -> int32_t
{
if (value < min)
return min;
if (value > max)
return max;
return value;
};
if (sampleFormat_.sampleSize() == 1)
{
auto* buffer = reinterpret_cast<FLAC__int8*>(chunk.payload);
for (int i = 0; i < samples; i++)
pcmBuffer_[i] = clip(-128, 127, static_cast<FLAC__int32>(buffer[i]));
}
else if (sampleFormat_.sampleSize() == 2)
{
auto* buffer = reinterpret_cast<FLAC__int16*>(chunk.payload);
for (int i = 0; i < samples; i++)
pcmBuffer_[i] = clip(-32768, 32767, static_cast<FLAC__int32>(buffer[i]));
}
else if (sampleFormat_.sampleSize() == 4)
{
auto* buffer = reinterpret_cast<FLAC__int32*>(chunk.payload);
if (sampleFormat_.bits() == 24)
{
for (int i = 0; i < samples; i++)
pcmBuffer_[i] = clip(-8388608, 8388607, buffer[i]);
}
else
{
for (int i = 0; i < samples; i++)
pcmBuffer_[i] = buffer[i];
}
}
FLAC__stream_encoder_process_interleaved(encoder_, pcmBuffer_, frames);
if (encodedSamples_ > 0)
{
double resMs = static_cast<double>(encodedSamples_) / sampleFormat_.msRate();
// LOG(INFO, LOG_TAG) << "encoded: " << chunk->payloadSize << "\tframes: " << encodedSamples_ << "\tres: " << resMs << "\n";
encodedSamples_ = 0;
encoded_callback_(*this, flacChunk_, resMs);
flacChunk_ = make_shared<msg::PcmChunk>(chunk.format, 0);
}
}
FLAC__StreamEncoderWriteStatus FlacEncoder::write_callback(const FLAC__StreamEncoder* /*encoder*/, const FLAC__byte buffer[], size_t bytes, unsigned samples,
unsigned current_frame)
{
// LOG(INFO, LOG_TAG) << "write_callback: " << bytes << ", " << samples << ", " << current_frame << "\n";
if ((current_frame == 0) && (bytes > 0) && (samples == 0))
{
headerChunk_->payload = static_cast<char*>(realloc(headerChunk_->payload, headerChunk_->payloadSize + bytes));
memcpy(headerChunk_->payload + headerChunk_->payloadSize, buffer, bytes);
headerChunk_->payloadSize += bytes;
}
else
{
flacChunk_->payload = static_cast<char*>(realloc(flacChunk_->payload, flacChunk_->payloadSize + bytes));
memcpy(flacChunk_->payload + flacChunk_->payloadSize, buffer, bytes);
flacChunk_->payloadSize += bytes;
encodedSamples_ += samples;
}
return FLAC__STREAM_ENCODER_WRITE_STATUS_OK;
}
namespace callback
{
FLAC__StreamEncoderWriteStatus write_callback(const FLAC__StreamEncoder* encoder, const FLAC__byte buffer[], size_t bytes, unsigned samples,
unsigned current_frame, void* client_data)
{
auto* flacEncoder = static_cast<FlacEncoder*>(client_data);
return flacEncoder->write_callback(encoder, buffer, bytes, samples, current_frame);
}
} // namespace callback
void FlacEncoder::initEncoder()
{
int compression_level(2);
try
{
compression_level = cpt::stoi(codecOptions_);
}
catch (...)
{
throw SnapException("Invalid codec option: \"" + codecOptions_ + "\"");
}
if ((compression_level < 0) || (compression_level > 8))
{
throw SnapException("compression level has to be between 0 and 8");
}
LOG(INFO, LOG_TAG) << "Init - compression level: " << compression_level << "\n";
FLAC__bool ok = 1;
FLAC__StreamEncoderInitStatus init_status;
FLAC__StreamMetadata_VorbisComment_Entry entry;
// allocate the encoder
if ((encoder_ = FLAC__stream_encoder_new()) == nullptr)
throw SnapException("error allocating encoder");
ok &= FLAC__stream_encoder_set_verify(encoder_, 1);
// compression levels (0-8):
// https://xiph.org/flac/api/group__flac__stream__encoder.html#gae49cf32f5256cb47eecd33779493ac85
// latency:
// 0-2: 1152 frames, ~26.1224ms
// 3-8: 4096 frames, ~92.8798ms
ok &= FLAC__stream_encoder_set_compression_level(encoder_, compression_level);
ok &= FLAC__stream_encoder_set_channels(encoder_, sampleFormat_.channels());
ok &= FLAC__stream_encoder_set_bits_per_sample(encoder_, sampleFormat_.bits());
ok &= FLAC__stream_encoder_set_sample_rate(encoder_, sampleFormat_.rate());
if (ok == 0)
throw SnapException("error setting up encoder");
// now add some metadata; we'll add some tags and a padding block
if ((metadata_[0] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT)) == nullptr ||
(metadata_[1] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_PADDING)) == nullptr ||
// there are many tag (vorbiscomment) functions but these are convenient for this particular use:
(FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, "TITLE", "SnapStream") == 0) ||
(FLAC__metadata_object_vorbiscomment_append_comment(metadata_[0], entry, 0) == 0) ||
(FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, "VERSION", VERSION) == 0) ||
(FLAC__metadata_object_vorbiscomment_append_comment(metadata_[0], entry, 0) == 0))
throw SnapException("out of memory or tag error");
metadata_[1]->length = 1234; // set the padding length
ok = FLAC__stream_encoder_set_metadata(encoder_, metadata_, 2);
if (ok == 0)
throw SnapException("error setting meta data");
// initialize encoder
init_status = FLAC__stream_encoder_init_stream(encoder_, callback::write_callback, nullptr, nullptr, nullptr, this);
if (init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK)
throw SnapException("ERROR: initializing encoder: " + string(FLAC__StreamEncoderInitStatusString[init_status]));
}
} // namespace encoder