mirror of
https://github.com/badaix/snapcast.git
synced 2025-04-29 18:27:12 +02:00
Opus encoder resamples to 48000:16:2
This commit is contained in:
parent
199c30b69d
commit
2d88ee85cd
13 changed files with 367 additions and 174 deletions
|
@ -123,7 +123,6 @@ if(NOT WIN32)
|
|||
add_definitions(-DFREEBSD -DHAS_DAEMON)
|
||||
link_directories("/usr/local/lib")
|
||||
list(APPEND INCLUDE_DIRS "/usr/local/include")
|
||||
list(APPEND CMAKE_REQUIRED_INCLUDES "${INCLUDE_DIRS}")
|
||||
elseif(ANDROID)
|
||||
# add_definitions("-DNO_CPP11_STRING")
|
||||
else()
|
||||
|
@ -138,6 +137,8 @@ if(NOT WIN32)
|
|||
pkg_search_module(AVAHI avahi-client)
|
||||
if (AVAHI_FOUND)
|
||||
add_definitions(-DHAS_AVAHI)
|
||||
else()
|
||||
message(STATUS "avahi-client not found")
|
||||
endif (AVAHI_FOUND)
|
||||
endif(BUILD_WITH_AVAHI)
|
||||
|
||||
|
@ -147,15 +148,22 @@ if(NOT WIN32)
|
|||
add_definitions(-DFREEBSD)
|
||||
link_directories("/usr/local/lib")
|
||||
list(APPEND INCLUDE_DIRS "/usr/local/include")
|
||||
list(APPEND CMAKE_REQUIRED_INCLUDES "${INCLUDE_DIRS}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
pkg_search_module(SOXR soxr)
|
||||
if (SOXR_FOUND)
|
||||
add_definitions("-DHAS_SOXR")
|
||||
else()
|
||||
message(STATUS "soxr not found")
|
||||
endif (SOXR_FOUND)
|
||||
|
||||
if(BUILD_WITH_FLAC)
|
||||
pkg_search_module(FLAC flac)
|
||||
if (FLAC_FOUND)
|
||||
add_definitions("-DHAS_FLAC")
|
||||
else()
|
||||
message(STATUS "flac not found")
|
||||
endif (FLAC_FOUND)
|
||||
endif()
|
||||
|
||||
|
@ -163,6 +171,8 @@ if(NOT WIN32)
|
|||
pkg_search_module(OGG ogg)
|
||||
if (OGG_FOUND)
|
||||
add_definitions("-DHAS_OGG")
|
||||
else()
|
||||
message(STATUS "ogg not found")
|
||||
endif (OGG_FOUND)
|
||||
endif()
|
||||
|
||||
|
@ -180,10 +190,16 @@ if(NOT WIN32)
|
|||
endif (TREMOR_FOUND)
|
||||
endif()
|
||||
|
||||
if ((BUILD_WITH_VORBIS OR BUILD_WITH_TREMOR) AND NOT VORBIS_FOUND AND NOT TREMOR_FOUND)
|
||||
message(STATUS "tremor and vorbis not found")
|
||||
endif()
|
||||
|
||||
if(BUILD_WITH_VORBIS)
|
||||
pkg_search_module(VORBISENC vorbisenc)
|
||||
if (VORBISENC_FOUND)
|
||||
add_definitions("-DHAS_VORBIS_ENC")
|
||||
else()
|
||||
message(STATUS "vorbisenc not found")
|
||||
endif(VORBISENC_FOUND)
|
||||
endif()
|
||||
|
||||
|
@ -191,6 +207,8 @@ if(NOT WIN32)
|
|||
pkg_search_module(OPUS opus)
|
||||
if (OPUS_FOUND)
|
||||
add_definitions("-DHAS_OPUS")
|
||||
else()
|
||||
message(STATUS "opus not found")
|
||||
endif (OPUS_FOUND)
|
||||
endif()
|
||||
|
||||
|
@ -211,24 +229,30 @@ if(WIN32)
|
|||
|
||||
find_path(FLAC_INCLUDE_DIRS FLAC/all.h)
|
||||
find_library(FLAC_LIBRARIES FLAC)
|
||||
find_package_handle_standard_args(FLAC DEFAULT_MSG FLAC_INCLUDE_DIRS FLAC_LIBRARIES)
|
||||
find_package_handle_standard_args(FLAC REQUIRED FLAC_INCLUDE_DIRS FLAC_LIBRARIES)
|
||||
|
||||
find_path(OGG_INCLUDE_DIRS ogg/ogg.h)
|
||||
find_library(OGG_LIBRARIES ogg)
|
||||
find_package_handle_standard_args(Ogg DEFAULT_MSG OGG_INCLUDE_DIRS OGG_LIBRARIES)
|
||||
find_package_handle_standard_args(Ogg REQUIRED OGG_INCLUDE_DIRS OGG_LIBRARIES)
|
||||
|
||||
find_path(VORBIS_INCLUDE_DIRS vorbis/vorbisenc.h)
|
||||
find_library(VORBIS_LIBRARIES vorbis)
|
||||
find_package_handle_standard_args(Vorbis DEFAULT_MSG VORBIS_INCLUDE_DIRS VORBIS_LIBRARIES)
|
||||
find_package_handle_standard_args(Vorbis REQUIRED VORBIS_INCLUDE_DIRS VORBIS_LIBRARIES)
|
||||
|
||||
find_path(OPUS_INCLUDE_DIRS opus/opus.h)
|
||||
find_library(OPUS_LIBRARIES opus)
|
||||
find_package_handle_standard_args(Opus REQUIRED OPUS_INCLUDE_DIRS OPUS_LIBRARIES)
|
||||
|
||||
find_path(SOXR_INCLUDE_DIRS soxr.h)
|
||||
find_library(SOXR_LIBRARIES soxr)
|
||||
find_package_handle_standard_args(Soxr REQUIRED SOXR_INCLUDE_DIRS SOXR_LIBRARIES)
|
||||
|
||||
add_definitions(-DNTDDI_VERSION=0x06020000 -D_WIN32_WINNT=0x0602 -DWINVER=0x0602 -DWINDOWS -DWIN32_LEAN_AND_MEAN -DUNICODE -D_UNICODE -D_CRT_SECURE_NO_WARNINGS )
|
||||
add_definitions(-DHAS_OGG -DHAS_VORBIS -DHAS_FLAC -DHAS_VORBIS_ENC -DHAS_OPUS -DHAS_WASAPI)
|
||||
add_definitions(-DHAS_OGG -DHAS_VORBIS -DHAS_FLAC -DHAS_VORBIS_ENC -DHAS_OPUS -DHAS_WASAPI -DHAS_SOXR)
|
||||
endif()
|
||||
|
||||
list(APPEND CMAKE_REQUIRED_INCLUDES "${INCLUDE_DIRS}")
|
||||
|
||||
add_subdirectory(common)
|
||||
|
||||
if (BUILD_SERVER)
|
||||
|
|
|
@ -48,22 +48,6 @@ else()
|
|||
endif (ALSA_FOUND)
|
||||
endif (MACOSX)
|
||||
|
||||
#pkg_search_module(SOXR soxr)
|
||||
if(WIN32)
|
||||
find_path(SOXR_INCLUDE_DIRS soxr.h)
|
||||
find_library(SOXR_LIBRARIES soxr)
|
||||
find_package_handle_standard_args(Soxr DEFAULT_MSG SOXR_INCLUDE_DIRS SOXR_LIBRARIES)
|
||||
add_definitions("-DHAS_SOXR")
|
||||
else()
|
||||
find_package(soxr)
|
||||
endif()
|
||||
|
||||
if (SOXR_FOUND)
|
||||
add_definitions("-DHAS_SOXR")
|
||||
list(APPEND CLIENT_LIBRARIES ${SOXR_LIBRARIES})
|
||||
list(APPEND CLIENT_INCLUDE ${SOXR_INCLUDE_DIRS})
|
||||
endif (SOXR_FOUND)
|
||||
|
||||
# if OGG then tremor or vorbis
|
||||
if (OGG_FOUND)
|
||||
list(APPEND CLIENT_SOURCES decoder/ogg_decoder.cpp)
|
||||
|
|
|
@ -44,7 +44,7 @@ endif
|
|||
|
||||
CXXFLAGS += $(ADD_CFLAGS) -std=c++14 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_OPUS -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
|
||||
LDFLAGS += $(ADD_LDFLAGS) -logg -lFLAC -lopus -lsoxr
|
||||
OBJ = snapclient.o stream.o client_connection.o time_provider.o player/player.o player/file_player.o decoder/pcm_decoder.o decoder/ogg_decoder.o decoder/flac_decoder.o decoder/opus_decoder.o controller.o ../common/sample_format.o
|
||||
OBJ = snapclient.o stream.o client_connection.o time_provider.o player/player.o player/file_player.o decoder/pcm_decoder.o decoder/ogg_decoder.o decoder/flac_decoder.o decoder/opus_decoder.o controller.o ../common/sample_format.o ../common/resampler.o
|
||||
|
||||
|
||||
ifneq (,$(TARGET))
|
||||
|
|
|
@ -49,10 +49,10 @@ OpusDecoder::~OpusDecoder()
|
|||
|
||||
bool OpusDecoder::decode(msg::PcmChunk* chunk)
|
||||
{
|
||||
int frame_size = 0;
|
||||
int decoded_frames = 0;
|
||||
|
||||
while ((frame_size = opus_decode(dec_, (unsigned char*)chunk->payload, chunk->payloadSize, pcm_.data(),
|
||||
static_cast<int>(pcm_.size()) / sample_format_.channels(), 0)) == OPUS_BUFFER_TOO_SMALL)
|
||||
while ((decoded_frames = opus_decode(dec_, (unsigned char*)chunk->payload, chunk->payloadSize, pcm_.data(),
|
||||
static_cast<int>(pcm_.size()) / sample_format_.channels(), 0)) == OPUS_BUFFER_TOO_SMALL)
|
||||
{
|
||||
if (pcm_.size() < const_max_frame_size * sample_format_.channels())
|
||||
{
|
||||
|
@ -63,18 +63,19 @@ bool OpusDecoder::decode(msg::PcmChunk* chunk)
|
|||
break;
|
||||
}
|
||||
|
||||
if (frame_size < 0)
|
||||
if (decoded_frames < 0)
|
||||
{
|
||||
LOG(ERROR, LOG_TAG) << "Failed to decode chunk: " << opus_strerror(frame_size) << ", IN size: " << chunk->payloadSize << ", OUT size: " << pcm_.size()
|
||||
<< '\n';
|
||||
LOG(ERROR, LOG_TAG) << "Failed to decode chunk: " << opus_strerror(decoded_frames) << ", IN size: " << chunk->payloadSize
|
||||
<< ", OUT size: " << pcm_.size() << '\n';
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(DEBUG, LOG_TAG) << "Decoded chunk: size " << chunk->payloadSize << " bytes, decoded " << frame_size << " samples" << '\n';
|
||||
LOG(DEBUG, LOG_TAG) << "Decode chunk: " << decoded_frames << " frames, size: " << chunk->payloadSize
|
||||
<< " bytes, decoded: " << decoded_frames * sample_format_.frameSize() << " bytes\n";
|
||||
|
||||
// copy encoded data to chunk
|
||||
chunk->payloadSize = frame_size * sample_format_.channels() * sizeof(opus_int16);
|
||||
chunk->payloadSize = decoded_frames * sample_format_.frameSize(); // decoded_frames * sample_format_.channels() * sizeof(opus_int16);
|
||||
chunk->payload = (char*)realloc(chunk->payload, chunk->payloadSize);
|
||||
memcpy(chunk->payload, (char*)pcm_.data(), chunk->payloadSize);
|
||||
return true;
|
||||
|
@ -96,7 +97,7 @@ SampleFormat OpusDecoder::setHeader(msg::CodecHeader* chunk)
|
|||
|
||||
// decode the sampleformat
|
||||
uint32_t rate;
|
||||
memcpy(&rate, chunk->payload + 4, sizeof(id_opus));
|
||||
memcpy(&rate, chunk->payload + 4, sizeof(rate));
|
||||
uint16_t bits;
|
||||
memcpy(&bits, chunk->payload + 8, sizeof(bits));
|
||||
uint16_t channels;
|
||||
|
|
|
@ -50,49 +50,20 @@ Stream::Stream(const SampleFormat& in_format, const SampleFormat& out_format)
|
|||
out_format.channels() != 0 ? out_format.channels() : format_.channels());
|
||||
}
|
||||
|
||||
/*
|
||||
48000 x
|
||||
------- = -----
|
||||
47999,2 x - 1
|
||||
/*
|
||||
48000 x
|
||||
------- = -----
|
||||
47999,2 x - 1
|
||||
|
||||
x = 1,000016667 / (1,000016667 - 1)
|
||||
*/
|
||||
// setRealSampleRate(format_.rate());
|
||||
#ifdef HAS_SOXR
|
||||
soxr_ = nullptr;
|
||||
if ((format_.rate() != in_format_.rate()) || (format_.bits() != in_format_.bits()))
|
||||
{
|
||||
LOG(INFO, LOG_TAG) << "Resampling from " << in_format_.toString() << " to " << format_.toString() << "\n";
|
||||
soxr_error_t error;
|
||||
|
||||
soxr_datatype_t in_type = SOXR_INT16_I;
|
||||
soxr_datatype_t out_type = SOXR_INT16_I;
|
||||
if (in_format_.sampleSize() > 2)
|
||||
in_type = SOXR_INT32_I;
|
||||
if (format_.sampleSize() > 2)
|
||||
out_type = SOXR_INT32_I;
|
||||
soxr_io_spec_t iospec = soxr_io_spec(in_type, out_type);
|
||||
// HQ should be fine: http://sox.sourceforge.net/Docs/FAQ
|
||||
soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0);
|
||||
soxr_ = soxr_create(static_cast<double>(in_format_.rate()), static_cast<double>(format_.rate()), format_.channels(), &error, &iospec, &q_spec, NULL);
|
||||
if (error)
|
||||
{
|
||||
LOG(ERROR, LOG_TAG) << "Error soxr_create: " << error << "\n";
|
||||
soxr_ = nullptr;
|
||||
}
|
||||
// initialize the buffer with 20ms (~latency of the reampler)
|
||||
resample_buffer_.resize(format_.frameSize() * static_cast<uint16_t>(ceil(format_.msRate() * 20)));
|
||||
}
|
||||
#endif
|
||||
x = 1,000016667 / (1,000016667 - 1)
|
||||
*/
|
||||
// setRealSampleRate(format_.rate());
|
||||
resampler_ = std::make_unique<Resampler>(in_format_, format_);
|
||||
}
|
||||
|
||||
|
||||
Stream::~Stream()
|
||||
{
|
||||
#ifdef HAS_SOXR
|
||||
if (soxr_)
|
||||
soxr_delete(soxr_);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
@ -127,92 +98,15 @@ void Stream::clearChunks()
|
|||
void Stream::addChunk(unique_ptr<msg::PcmChunk> chunk)
|
||||
{
|
||||
// drop chunk if it's too old. Just in case, this shouldn't happen.
|
||||
cs::usec age = std::chrono::duration_cast<cs::usec>(TimeProvider::serverNow() - chunk->start());
|
||||
auto age = std::chrono::duration_cast<cs::msec>(TimeProvider::serverNow() - chunk->start());
|
||||
if (age > 5s + bufferMs_)
|
||||
return;
|
||||
|
||||
// LOG(DEBUG, LOG_TAG) << "new chunk: " << chunk->durationMs() << " ms, Chunks: " << chunks_.size() << "\n";
|
||||
LOG(DEBUG, LOG_TAG) << "new chunk: " << chunk->durationMs() << " ms, age: " << age.count() << " ms, Chunks: " << chunks_.size() << "\n";
|
||||
|
||||
#ifndef HAS_SOXR
|
||||
chunks_.push(move(chunk));
|
||||
#else
|
||||
if (soxr_ == nullptr)
|
||||
{
|
||||
chunks_.push(move(chunk));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (in_format_.bits() == 24)
|
||||
{
|
||||
// sox expects 32 bit input, shift 8 bits left
|
||||
int32_t* frames = (int32_t*)chunk->payload;
|
||||
for (size_t n = 0; n < chunk->getSampleCount(); ++n)
|
||||
frames[n] = frames[n] << 8;
|
||||
}
|
||||
|
||||
size_t idone;
|
||||
size_t odone;
|
||||
auto resample_buffer_framesize = resample_buffer_.size() / format_.frameSize();
|
||||
auto error = soxr_process(soxr_, chunk->payload, chunk->getFrameCount(), &idone, resample_buffer_.data(), resample_buffer_framesize, &odone);
|
||||
if (error)
|
||||
{
|
||||
LOG(ERROR, LOG_TAG) << "Error soxr_process: " << error << "\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(TRACE, LOG_TAG) << "Resample idone: " << idone << "/" << chunk->getFrameCount() << ", odone: " << odone << "/"
|
||||
<< resample_buffer_.size() / format_.frameSize() << ", delay: " << soxr_delay(soxr_) << "\n";
|
||||
|
||||
// some data has been resampled (odone frames) and some is still in the pipe (soxr_delay frames)
|
||||
if (odone > 0)
|
||||
{
|
||||
// get the resamples ts from the input ts
|
||||
auto input_end_ts = chunk->start() + chunk->duration<std::chrono::microseconds>();
|
||||
double resampled_ms = (odone + soxr_delay(soxr_)) / format_.msRate();
|
||||
auto resampled_start = input_end_ts - std::chrono::microseconds(static_cast<int>(resampled_ms * 1000.));
|
||||
|
||||
auto resampled_chunk = new msg::PcmChunk(format_, 0);
|
||||
auto us = chrono::duration_cast<chrono::microseconds>(resampled_start.time_since_epoch()).count();
|
||||
resampled_chunk->timestamp.sec = static_cast<int32_t>(us / 1000000);
|
||||
resampled_chunk->timestamp.usec = static_cast<int32_t>(us % 1000000);
|
||||
|
||||
// copy from the resample_buffer to the resampled chunk
|
||||
resampled_chunk->payloadSize = static_cast<uint32_t>(odone * format_.frameSize());
|
||||
resampled_chunk->payload = (char*)realloc(resampled_chunk->payload, resampled_chunk->payloadSize);
|
||||
memcpy(resampled_chunk->payload, resample_buffer_.data(), resampled_chunk->payloadSize);
|
||||
|
||||
if (format_.bits() == 24)
|
||||
{
|
||||
// sox has quantized to 32 bit, shift 8 bits right
|
||||
int32_t* frames = (int32_t*)resampled_chunk->payload;
|
||||
for (size_t n = 0; n < resampled_chunk->getSampleCount(); ++n)
|
||||
{
|
||||
// +128 to round to the nearest so that quantisation steps are distributed evenly
|
||||
frames[n] = (frames[n] + 128) >> 8;
|
||||
if (frames[n] > 0x7fffffff)
|
||||
frames[n] = 0x7fffffff;
|
||||
}
|
||||
}
|
||||
chunks_.push(shared_ptr<msg::PcmChunk>(resampled_chunk));
|
||||
|
||||
// check if the resample_buffer is large enough, or if soxr was using all available space
|
||||
if (odone == resample_buffer_framesize)
|
||||
{
|
||||
// buffer for resampled data too small, add space for 5ms
|
||||
resample_buffer_.resize(resample_buffer_.size() + format_.frameSize() * static_cast<uint16_t>(ceil(format_.msRate() * 5)));
|
||||
LOG(DEBUG, LOG_TAG) << "Resample buffer completely filled, adding space for 5ms; new buffer size: " << resample_buffer_.size()
|
||||
<< " bytes\n";
|
||||
}
|
||||
|
||||
// //LOG(TRACE, LOG_TAG) << "ts: " << out->timestamp.sec << "s, " << out->timestamp.usec/1000.f << " ms, duration: " << odone / format_.msRate()
|
||||
// << "\n";
|
||||
// int64_t next_us = us + static_cast<int64_t>(odone / format_.msRate() * 1000);
|
||||
// LOG(TRACE, LOG_TAG) << "ts: " << us << ", next: " << next_us << ", diff: " << next_us_ - us << "\n";
|
||||
// next_us_ = next_us;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
auto resampled = resampler_->resample(std::move(chunk));
|
||||
if (resampled)
|
||||
chunks_.push(move(resampled));
|
||||
}
|
||||
|
||||
|
||||
|
@ -372,6 +266,9 @@ bool Stream::getPlayerChunk(void* outputBuffer, const cs::usec& outputBufferDacT
|
|||
{
|
||||
if (age.count() > 0)
|
||||
{
|
||||
// TODO: should be enough to check if "age.count() > chunk->duration"
|
||||
// if "age.count > 0 && age.count < chunk->duration" then
|
||||
// the current chunk could be fast forwarded by age.count, instead of dropping the whole chunk
|
||||
LOG(DEBUG, LOG_TAG) << "age > 0: " << age.count() / 1000 << "ms\n";
|
||||
// age > 0: the top of the stream is too old. We must fast foward.
|
||||
// delete the current chunk, it's too old. This will avoid an endless loop if there is no chunk in the queue.
|
||||
|
|
|
@ -16,14 +16,15 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
***/
|
||||
|
||||
#ifndef STREAM_H
|
||||
#define STREAM_H
|
||||
#ifndef STREAM_HPP
|
||||
#define STREAM_HPP
|
||||
|
||||
#include "common/queue.h"
|
||||
#include "common/sample_format.hpp"
|
||||
#include "double_buffer.hpp"
|
||||
#include "message/message.hpp"
|
||||
#include "message/pcm_chunk.hpp"
|
||||
#include "resampler.hpp"
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#ifdef HAS_SOXR
|
||||
|
@ -102,9 +103,8 @@ private:
|
|||
int32_t correctAfterXFrames_;
|
||||
chronos::msec bufferMs_;
|
||||
|
||||
#ifdef HAS_SOXR
|
||||
soxr_t soxr_;
|
||||
#endif
|
||||
std::unique_ptr<Resampler> resampler_;
|
||||
|
||||
std::vector<char> resample_buffer_;
|
||||
std::vector<char> read_buffer_;
|
||||
int frame_delta_;
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
set(SOURCES
|
||||
resampler.cpp
|
||||
sample_format.cpp)
|
||||
|
||||
if(NOT WIN32)
|
||||
add_library(common STATIC daemon.cpp sample_format.cpp)
|
||||
else()
|
||||
add_library(common STATIC sample_format.cpp)
|
||||
list(APPEND SOURCES daemon.cpp)
|
||||
endif()
|
||||
|
||||
if (SOXR_FOUND)
|
||||
include_directories(${SOXR_INCLUDE_DIRS})
|
||||
endif (SOXR_FOUND)
|
||||
|
||||
add_library(common STATIC ${SOURCES})
|
||||
|
||||
if (SOXR_FOUND)
|
||||
target_link_libraries(common ${SOXR_LIBRARIES})
|
||||
endif (SOXR_FOUND)
|
||||
|
|
|
@ -58,6 +58,19 @@ public:
|
|||
}
|
||||
#endif
|
||||
|
||||
// std::unique_ptr<PcmChunk> consume(uint32_t frameCount)
|
||||
// {
|
||||
// auto result = std::make_unique<PcmChunk>(format, 0);
|
||||
// if (frameCount * format.frameSize() > payloadSize)
|
||||
// frameCount = payloadSize / format.frameSize();
|
||||
// result->payload = payload;
|
||||
// result->payloadSize = frameCount * format.frameSize();
|
||||
// payloadSize -= result->payloadSize;
|
||||
// payload = (char*)realloc(payload + result->payloadSize, payloadSize);
|
||||
// // payload += result->payloadSize;
|
||||
// return result;
|
||||
// }
|
||||
|
||||
int readFrames(void* outputBuffer, uint32_t frameCount)
|
||||
{
|
||||
// logd << "read: " << frameCount << ", total: " << (wireChunk->length / format.frameSize()) << ", idx: " << idx;// << std::endl;
|
||||
|
@ -87,7 +100,6 @@ public:
|
|||
return idx_;
|
||||
}
|
||||
|
||||
|
||||
chronos::time_point_clk start() const override
|
||||
{
|
||||
return chronos::time_point_clk(chronos::sec(timestamp.sec) + chronos::usec(timestamp.usec) +
|
||||
|
@ -105,6 +117,14 @@ public:
|
|||
return std::chrono::duration_cast<T>(chronos::nsec(static_cast<chronos::nsec::rep>(1000000 * getFrameCount() / format.msRate())));
|
||||
}
|
||||
|
||||
// void append(const PcmChunk& chunk)
|
||||
// {
|
||||
// auto newSize = payloadSize + chunk.payloadSize;
|
||||
// payload = (char*)realloc(payload, newSize);
|
||||
// memcpy(payload + payloadSize, chunk.payload, chunk.payloadSize);
|
||||
// payloadSize = newSize;
|
||||
// }
|
||||
|
||||
double durationMs() const
|
||||
{
|
||||
return static_cast<double>(getFrameCount()) / format.msRate();
|
||||
|
|
180
common/resampler.cpp
Normal file
180
common/resampler.cpp
Normal file
|
@ -0,0 +1,180 @@
|
|||
/***
|
||||
This file is part of snapcast
|
||||
Copyright (C) 2014-2020 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/>.
|
||||
***/
|
||||
|
||||
#include "resampler.hpp"
|
||||
#include "common/aixlog.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
static constexpr auto LOG_TAG = "Resampler";
|
||||
|
||||
Resampler::Resampler(const SampleFormat& in_format, const SampleFormat& out_format) : in_format_(in_format), out_format_(out_format)
|
||||
{
|
||||
#ifdef HAS_SOXR
|
||||
soxr_ = nullptr;
|
||||
if ((out_format_.rate() != in_format_.rate()) || (out_format_.bits() != in_format_.bits()))
|
||||
{
|
||||
LOG(INFO, LOG_TAG) << "Resampling from " << in_format_.toString() << " to " << out_format_.toString() << "\n";
|
||||
soxr_error_t error;
|
||||
|
||||
soxr_datatype_t in_type = SOXR_INT16_I;
|
||||
soxr_datatype_t out_type = SOXR_INT16_I;
|
||||
if (in_format_.sampleSize() > 2)
|
||||
in_type = SOXR_INT32_I;
|
||||
if (out_format_.sampleSize() > 2)
|
||||
out_type = SOXR_INT32_I;
|
||||
soxr_io_spec_t iospec = soxr_io_spec(in_type, out_type);
|
||||
// HQ should be fine: http://sox.sourceforge.net/Docs/FAQ
|
||||
soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0);
|
||||
soxr_ =
|
||||
soxr_create(static_cast<double>(in_format_.rate()), static_cast<double>(out_format_.rate()), in_format_.channels(), &error, &iospec, &q_spec, NULL);
|
||||
if (error)
|
||||
{
|
||||
LOG(ERROR, LOG_TAG) << "Error soxr_create: " << error << "\n";
|
||||
soxr_ = nullptr;
|
||||
}
|
||||
// initialize the buffer with 20ms (~latency of the reampler)
|
||||
resample_buffer_.resize(out_format_.frameSize() * static_cast<uint16_t>(ceil(out_format_.msRate() * 20)));
|
||||
}
|
||||
#endif
|
||||
// resampled_chunk_ = std::make_unique<msg::PcmChunk>(out_format_, 0);
|
||||
}
|
||||
|
||||
|
||||
// std::shared_ptr<msg::PcmChunk> Resampler::resample(std::shared_ptr<msg::PcmChunk> chunk, chronos::usec duration)
|
||||
// {
|
||||
// auto resampled_chunk = resample(chunk);
|
||||
// if (!resampled_chunk)
|
||||
// return nullptr;
|
||||
// std::cerr << "1\n";
|
||||
// resampled_chunk_->append(*resampled_chunk);
|
||||
// std::cerr << "2\n";
|
||||
// while (resampled_chunk_->duration<chronos::usec>() >= duration)
|
||||
// {
|
||||
// LOG(DEBUG, LOG_TAG) << "resampled duration: " << resampled_chunk_->durationMs() << ", consuming: " << out_format_.usRate() * duration.count() <<
|
||||
// "\n";
|
||||
// auto chunk = resampled_chunk_->consume(out_format_.usRate() * duration.count());
|
||||
// LOG(DEBUG, LOG_TAG) << "consumed: " << chunk->durationMs() << ", resampled duration: " << resampled_chunk_->durationMs() << "\n";
|
||||
// return chunk;
|
||||
// }
|
||||
// // resampled_chunks_.push_back(resampled_chunk);
|
||||
// // chronos::usec avail;
|
||||
// // for (const auto& chunk: resampled_chunks_)
|
||||
// // {
|
||||
// // avail += chunk->durationLeft<chronos::usec>();
|
||||
// // if (avail >= duration)
|
||||
// // {
|
||||
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
|
||||
shared_ptr<msg::PcmChunk> Resampler::resample(shared_ptr<msg::PcmChunk> chunk)
|
||||
{
|
||||
#ifndef HAS_SOXR
|
||||
return chunk;
|
||||
#else
|
||||
if (soxr_ == nullptr)
|
||||
{
|
||||
return chunk;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (in_format_.bits() == 24)
|
||||
{
|
||||
// sox expects 32 bit input, shift 8 bits left
|
||||
int32_t* frames = (int32_t*)chunk->payload;
|
||||
for (size_t n = 0; n < chunk->getSampleCount(); ++n)
|
||||
frames[n] = frames[n] << 8;
|
||||
}
|
||||
|
||||
size_t idone;
|
||||
size_t odone;
|
||||
auto resample_buffer_framesize = resample_buffer_.size() / out_format_.frameSize();
|
||||
auto error = soxr_process(soxr_, chunk->payload, chunk->getFrameCount(), &idone, resample_buffer_.data(), resample_buffer_framesize, &odone);
|
||||
if (error)
|
||||
{
|
||||
LOG(ERROR, LOG_TAG) << "Error soxr_process: " << error << "\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(TRACE, LOG_TAG) << "Resample idone: " << idone << "/" << chunk->getFrameCount() << ", odone: " << odone << "/"
|
||||
<< resample_buffer_.size() / out_format_.frameSize() << ", delay: " << soxr_delay(soxr_) << "\n";
|
||||
|
||||
// some data has been resampled (odone frames) and some is still in the pipe (soxr_delay frames)
|
||||
if (odone > 0)
|
||||
{
|
||||
// get the resampled ts from the input ts
|
||||
auto input_end_ts = chunk->start() + chunk->duration<std::chrono::microseconds>();
|
||||
double resampled_ms = (odone + soxr_delay(soxr_)) / out_format_.msRate();
|
||||
auto resampled_start = input_end_ts - std::chrono::microseconds(static_cast<int>(resampled_ms * 1000.));
|
||||
|
||||
auto resampled_chunk = std::make_shared<msg::PcmChunk>(out_format_, 0);
|
||||
auto us = chrono::duration_cast<chrono::microseconds>(resampled_start.time_since_epoch()).count();
|
||||
resampled_chunk->timestamp.sec = static_cast<int32_t>(us / 1000000);
|
||||
resampled_chunk->timestamp.usec = static_cast<int32_t>(us % 1000000);
|
||||
|
||||
// copy from the resample_buffer to the resampled chunk
|
||||
resampled_chunk->payloadSize = static_cast<uint32_t>(odone * out_format_.frameSize());
|
||||
resampled_chunk->payload = (char*)realloc(resampled_chunk->payload, resampled_chunk->payloadSize);
|
||||
memcpy(resampled_chunk->payload, resample_buffer_.data(), resampled_chunk->payloadSize);
|
||||
|
||||
if (out_format_.bits() == 24)
|
||||
{
|
||||
// sox has quantized to 32 bit, shift 8 bits right
|
||||
int32_t* frames = (int32_t*)resampled_chunk->payload;
|
||||
for (size_t n = 0; n < resampled_chunk->getSampleCount(); ++n)
|
||||
{
|
||||
// +128 to round to the nearest so that quantisation steps are distributed evenly
|
||||
frames[n] = (frames[n] + 128) >> 8;
|
||||
if (frames[n] > 0x7fffffff)
|
||||
frames[n] = 0x7fffffff;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the resample_buffer is large enough, or if soxr was using all available space
|
||||
if (odone == resample_buffer_framesize)
|
||||
{
|
||||
// buffer for resampled data too small, add space for 5ms
|
||||
resample_buffer_.resize(resample_buffer_.size() + out_format_.frameSize() * static_cast<uint16_t>(ceil(out_format_.msRate() * 5)));
|
||||
LOG(DEBUG, LOG_TAG) << "Resample buffer completely filled, adding space for 5ms; new buffer size: " << resample_buffer_.size()
|
||||
<< " bytes\n";
|
||||
}
|
||||
|
||||
// //LOG(TRACE, LOG_TAG) << "ts: " << out->timestamp.sec << "s, " << out->timestamp.usec/1000.f << " ms, duration: " << odone / format_.msRate()
|
||||
// << "\n";
|
||||
// int64_t next_us = us + static_cast<int64_t>(odone / format_.msRate() * 1000);
|
||||
// LOG(TRACE, LOG_TAG) << "ts: " << us << ", next: " << next_us << ", diff: " << next_us_ - us << "\n";
|
||||
// next_us_ = next_us;
|
||||
|
||||
return resampled_chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
Resampler::~Resampler()
|
||||
{
|
||||
#ifdef HAS_SOXR
|
||||
if (soxr_)
|
||||
soxr_delete(soxr_);
|
||||
#endif
|
||||
}
|
50
common/resampler.hpp
Normal file
50
common/resampler.hpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
/***
|
||||
This file is part of snapcast
|
||||
Copyright (C) 2014-2020 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/>.
|
||||
***/
|
||||
|
||||
#ifndef RESAMPLER_HPP
|
||||
#define RESAMPLER_HPP
|
||||
|
||||
#include "common/message/pcm_chunk.hpp"
|
||||
#include "common/sample_format.hpp"
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#ifdef HAS_SOXR
|
||||
#include <soxr.h>
|
||||
#endif
|
||||
|
||||
|
||||
class Resampler
|
||||
{
|
||||
public:
|
||||
Resampler(const SampleFormat& in_format, const SampleFormat& out_format);
|
||||
virtual ~Resampler();
|
||||
|
||||
// std::shared_ptr<msg::PcmChunk> resample(std::shared_ptr<msg::PcmChunk> chunk, chronos::usec duration);
|
||||
std::shared_ptr<msg::PcmChunk> resample(std::shared_ptr<msg::PcmChunk> chunk);
|
||||
|
||||
private:
|
||||
std::vector<char> resample_buffer_;
|
||||
// std::unique_ptr<msg::PcmChunk> resampled_chunk_;
|
||||
SampleFormat in_format_;
|
||||
SampleFormat out_format_;
|
||||
#ifdef HAS_SOXR
|
||||
soxr_t soxr_;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif
|
|
@ -43,8 +43,8 @@ ifneq ($(SANITIZE), )
|
|||
endif
|
||||
|
||||
CXXFLAGS += $(ADD_CFLAGS) -std=c++14 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_VORBIS -DHAS_VORBIS_ENC -DHAS_OPUS -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
|
||||
LDFLAGS += $(ADD_LDFLAGS) -lvorbis -lvorbisenc -logg -lFLAC -lopus
|
||||
OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/librespot_stream.o streamreader/watchdog.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/ogg_encoder.o ../common/sample_format.o
|
||||
LDFLAGS += $(ADD_LDFLAGS) -lvorbis -lvorbisenc -logg -lFLAC -lopus -lsoxr
|
||||
OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/librespot_stream.o streamreader/watchdog.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/ogg_encoder.o ../common/sample_format.o ../common/resampler.o
|
||||
|
||||
ifneq (,$(TARGET))
|
||||
CXXFLAGS += -D$(TARGET)
|
||||
|
|
|
@ -31,6 +31,8 @@ namespace encoder
|
|||
static constexpr opus_int32 const_min_bitrate = 6000;
|
||||
static constexpr opus_int32 const_max_bitrate = 512000;
|
||||
|
||||
static constexpr auto LOG_TAG = "OpusEncoder";
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename T>
|
||||
|
@ -77,8 +79,16 @@ void OpusEncoder::initEncoder()
|
|||
{
|
||||
// Opus is quite restrictive in sample rate and bit depth
|
||||
// It can handle mono signals, but we will check for stereo
|
||||
if ((sampleFormat_.rate() != 48000) || (sampleFormat_.bits() != 16) || (sampleFormat_.channels() != 2))
|
||||
throw SnapException("Opus sampleformat must be 48000:16:2");
|
||||
// if ((sampleFormat_.rate() != 48000) || (sampleFormat_.bits() != 16) || (sampleFormat_.channels() != 2))
|
||||
// throw SnapException("Opus sampleformat must be 48000:16:2");
|
||||
if (sampleFormat_.channels() != 2)
|
||||
throw SnapException("Opus requires a stereo signal");
|
||||
SampleFormat out{48000, 16, 2};
|
||||
if ((sampleFormat_.rate() != 48000) || (sampleFormat_.bits() != 16))
|
||||
LOG(INFO, LOG_TAG) << "Resampling input from " << sampleFormat_.toString() << " to " << out.toString() << " as required by Opus\n";
|
||||
|
||||
resampler_ = make_unique<Resampler>(sampleFormat_, out);
|
||||
sampleFormat_ = out;
|
||||
|
||||
opus_int32 bitrate = 192000;
|
||||
opus_int32 complexity = 10;
|
||||
|
@ -132,7 +142,7 @@ void OpusEncoder::initEncoder()
|
|||
throw SnapException("Opus error parsing options: " + codecOptions_);
|
||||
}
|
||||
|
||||
LOG(INFO) << "Opus bitrate: " << bitrate << " bps, complexity: " << complexity << "\n";
|
||||
LOG(INFO, LOG_TAG) << "Opus bitrate: " << bitrate << " bps, complexity: " << complexity << "\n";
|
||||
|
||||
int error;
|
||||
enc_ = opus_encoder_create(sampleFormat_.rate(), sampleFormat_.channels(), OPUS_APPLICATION_RESTRICTED_LOWDELAY, &error);
|
||||
|
@ -166,7 +176,15 @@ void OpusEncoder::initEncoder()
|
|||
// and encode the buffer content in the next iteration
|
||||
void OpusEncoder::encode(const msg::PcmChunk* chunk)
|
||||
{
|
||||
// LOG(TRACE) << "encode " << chunk->duration<std::chrono::milliseconds>().count() << "ms\n";
|
||||
// chunk =
|
||||
// resampler_->resample(std::make_shared<msg::PcmChunk>(chunk)).get();
|
||||
auto in = std::make_shared<msg::PcmChunk>(*chunk);
|
||||
auto out = resampler_->resample(in); //, std::chrono::milliseconds(20));
|
||||
if (out == nullptr)
|
||||
return;
|
||||
chunk = out.get();
|
||||
|
||||
// LOG(TRACE, LOG_TAG) << "encode " << chunk->duration<std::chrono::milliseconds>().count() << "ms\n";
|
||||
uint32_t offset = 0;
|
||||
|
||||
// check if there is something left from the last call to encode and fill the remainder buffer to
|
||||
|
@ -175,13 +193,13 @@ void OpusEncoder::encode(const msg::PcmChunk* chunk)
|
|||
{
|
||||
offset = std::min(static_cast<uint32_t>(remainder_max_size_ - remainder_->payloadSize), chunk->payloadSize);
|
||||
memcpy(remainder_->payload + remainder_->payloadSize, chunk->payload, offset);
|
||||
// LOG(TRACE) << "remainder buffer size: " << remainder_->payloadSize << "/" << remainder_max_size_ << ", appending " << offset << " bytes\n";
|
||||
// LOG(TRACE, LOG_TAG) << "remainder buffer size: " << remainder_->payloadSize << "/" << remainder_max_size_ << ", appending " << offset << " bytes\n";
|
||||
remainder_->payloadSize += offset;
|
||||
|
||||
if (remainder_->payloadSize < remainder_max_size_)
|
||||
{
|
||||
LOG(DEBUG) << "not enough data to encode (" << remainder_->payloadSize << " of " << remainder_max_size_ << " bytes)"
|
||||
<< "\n";
|
||||
LOG(DEBUG, LOG_TAG) << "not enough data to encode (" << remainder_->payloadSize << " of " << remainder_max_size_ << " bytes)"
|
||||
<< "\n";
|
||||
return;
|
||||
}
|
||||
encode(chunk->format, remainder_->payload, remainder_->payloadSize);
|
||||
|
@ -196,7 +214,8 @@ void OpusEncoder::encode(const msg::PcmChunk* chunk)
|
|||
uint32_t bytes = ms2bytes(duration);
|
||||
while (chunk->payloadSize - offset >= bytes)
|
||||
{
|
||||
// LOG(TRACE) << "encoding " << duration << "ms (" << bytes << "), offset: " << offset << ", chunk size: " << chunk->payloadSize - offset << "\n";
|
||||
// LOG(TRACE, LOG_TAG) << "encoding " << duration << "ms (" << bytes << "), offset: " << offset << ", chunk size: " << chunk->payloadSize - offset
|
||||
// << "\n";
|
||||
encode(chunk->format, chunk->payload + offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
@ -216,13 +235,13 @@ void OpusEncoder::encode(const msg::PcmChunk* chunk)
|
|||
void OpusEncoder::encode(const SampleFormat& format, const char* data, size_t size)
|
||||
{
|
||||
// void* buffer;
|
||||
// LOG(INFO) << "frames: " << chunk->readFrames(buffer, std::chrono::milliseconds(10)) << "\n";
|
||||
// LOG(INFO, LOG_TAG) << "frames: " << chunk->readFrames(buffer, std::chrono::milliseconds(10)) << "\n";
|
||||
int samples_per_channel = size / format.frameSize();
|
||||
if (encoded_.size() < size)
|
||||
encoded_.resize(size);
|
||||
|
||||
opus_int32 len = opus_encode(enc_, (opus_int16*)data, samples_per_channel, encoded_.data(), size);
|
||||
// LOG(TRACE) << "Encode " << samples_per_channel << " frames, size " << size << " bytes, encoded: " << len << " bytes" << '\n';
|
||||
LOG(TRACE, LOG_TAG) << "Encode " << samples_per_channel << " frames, size " << size << " bytes, encoded: " << len << " bytes" << '\n';
|
||||
|
||||
if (len > 0)
|
||||
{
|
||||
|
@ -235,7 +254,8 @@ void OpusEncoder::encode(const SampleFormat& format, const char* data, size_t si
|
|||
}
|
||||
else
|
||||
{
|
||||
LOG(ERROR) << "Failed to encode chunk: " << opus_strerror(len) << ", samples / channel: " << samples_per_channel << ", bytes: " << size << '\n';
|
||||
LOG(ERROR, LOG_TAG) << "Failed to encode chunk: " << opus_strerror(len) << ", samples / channel: " << samples_per_channel << ", bytes: " << size
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
***/
|
||||
|
||||
#pragma once
|
||||
#ifndef OPUS_ENCODER_HPP
|
||||
#define OPUS_ENCODER_HPP
|
||||
|
||||
#include "common/resampler.hpp"
|
||||
#include "encoder.hpp"
|
||||
#include <opus/opus.h>
|
||||
|
||||
|
@ -43,6 +45,9 @@ protected:
|
|||
std::vector<unsigned char> encoded_;
|
||||
std::unique_ptr<msg::PcmChunk> remainder_;
|
||||
size_t remainder_max_size_;
|
||||
std::unique_ptr<Resampler> resampler_;
|
||||
};
|
||||
|
||||
} // namespace encoder
|
||||
|
||||
#endif
|
||||
|
|
Loading…
Add table
Reference in a new issue