/*** 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 . ***/ // 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 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(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(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().count() << ", format: " << chunk.format.toString() << "\n"; if (pcmBufferSize_ < samples) { pcmBufferSize_ = samples; pcmBuffer_ = static_cast(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(chunk.payload); for (int i = 0; i < samples; i++) pcmBuffer_[i] = clip(-128, 127, static_cast(buffer[i])); } else if (sampleFormat_.sampleSize() == 2) { auto* buffer = reinterpret_cast(chunk.payload); for (int i = 0; i < samples; i++) pcmBuffer_[i] = clip(-32768, 32767, static_cast(buffer[i])); } else if (sampleFormat_.sampleSize() == 4) { auto* buffer = reinterpret_cast(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(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(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(realloc(headerChunk_->payload, headerChunk_->payloadSize + bytes)); memcpy(headerChunk_->payload + headerChunk_->payloadSize, buffer, bytes); headerChunk_->payloadSize += bytes; } else { flacChunk_->payload = static_cast(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(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