Skip to content

Commit

Permalink
Merge pull request #794 from Samt43/flac_lossless_streaming
Browse files Browse the repository at this point in the history
Flac lossless streaming
  • Loading branch information
AlbrechtL committed Feb 25, 2024
2 parents 4d6751f + 069bf08 commit 552ccae
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 71 deletions.
20 changes: 20 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Thank you for your interest in contributing to welle.io, it's highly appreciated!
Please send PRs only against the branch "next".
Describe your PR further using the template provided below.
The more details the better, but please delete unsed sections!
-->

#### What does this PR do and why is it necessary?

#### How was it tested? How can it be tested by the reviewer?

#### Any background context you want to provide?

#### What are the relevant tickets if any?

#### Screenshots (if appropriate)

#### Further notes
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ option(PROFILING "Enable profiling (see README.md)" OFF )
option(AIRSPY "Compile with Airspy support" OFF )
option(RTLSDR "Compile with RTL-SDR support" OFF )
option(SOAPYSDR "Compile with SoapySDR support" OFF )
option(FLAC "Compile with flac support for streaming" OFF )

add_definitions(-Wall)
add_definitions(-g)
Expand Down Expand Up @@ -75,6 +76,10 @@ endif()

if(BUILD_WELLE_CLI AND NOT ANDROID)
find_package(Lame REQUIRED)
if (FLAC)
find_package(FLACPP REQUIRED) # test if FLAC is installed on the system
add_definitions(-DHAVE_FLAC)
endif()
endif()

find_package(Threads REQUIRED)
Expand Down Expand Up @@ -212,6 +217,7 @@ include_directories(
${FAAD_INCLUDE_DIRS}
${LIBRTLSDR_INCLUDE_DIRS}
${SoapySDR_INCLUDE_DIRS}
${FLACPP_INCLUDE_DIRS}
)

set(backend_sources
Expand Down Expand Up @@ -468,6 +474,7 @@ if(BUILD_WELLE_CLI AND NOT ANDROID)
${LAME_LIBRARIES}
${SoapySDR_LIBRARIES}
${MPG123_LIBRARIES}
${FLACPP_LIBRARIES}
Threads::Threads
)

Expand Down
6 changes: 2 additions & 4 deletions HomebrewFormula/welle.io.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
class WelleIo < Formula
desc "DAB/DAB+ Software Radio"
homepage "https://www.welle.io"
url "https://github.com/AlbrechtL/welle.io/archive/v2.2.tar.gz"
sha256 "4b72c2984a884cc2f02d1e501ead2a8b0323900f37cebf4aed016e84474e0259"
url "https://github.com/AlbrechtL/welle.io/archive/v2.4.tar.gz"
sha256 "7c2a2ff7b6e0780aee8a30a2beedfa831ce67683e1d076a73cebc897637d0202"
license "GPL-2.0-or-later"
head "https://github.com/AlbrechtL/welle.io.git"

Expand All @@ -17,8 +17,6 @@ class WelleIo < Formula
depends_on "librtlsdr"
depends_on "libusb"
depends_on "mpg123"
depends_on "pothosware/homebrew-pothos/soapysdr"
depends_on "pothosware/homebrew-pothos/soapyuhd"
depends_on "qt@5"

def install
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ sudo apt install git build-essential
2. Install the following non-Qt packages

```
sudo apt install libfaad-dev libmpg123-dev libfftw3-dev librtlsdr-dev libusb-1.0-0-dev mesa-common-dev libglu1-mesa-dev libpulse-dev libsoapysdr-dev libairspy-dev libmp3lame-dev
sudo apt install libfaad-dev libmpg123-dev libfftw3-dev librtlsdr-dev libusb-1.0-0-dev mesa-common-dev libglu1-mesa-dev libpulse-dev libsoapysdr-dev libairspy-dev libmp3lame-dev libflac++-dev
```

3. Install the following Qt packages
Expand Down Expand Up @@ -354,6 +354,12 @@ With the `-P` option, welle-cli will switch once DLS and a slide were decoded, s

Example: `welle-cli -c 12A -C 1 -w 7979` enables the webserver on channel 12A, please then go to http://localhost:7979/ where you can observe all necessary details for every service ID in the ensemble, see the slideshows, stream the audio (by clicking on the Play-Button), check spectrum, constellation, TII information and CIR peak diagramme.

Streaming output options
---

By default, `welle-cli` will output in mp3 if in webserver mode.
With the `-O` option, you can choose between mp3 and flac (lossless) if FLAC support is enabled at build time.

Backend options
---

Expand Down
17 changes: 17 additions & 0 deletions cmake/Modules/FindFLACPP.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# - Find libFLAC++
#
# This module defines
# FLACPP_FOUND - True if libFLAC++ has been found.
# FLACPP_LIBRARIES - List of libraries when using libFLAC++.
# FLACPP_INCLUDE_DIRS - libFLAC++ include directories.

# Look for the header file.
find_path(FLACPP_INCLUDE_DIRS
NAMES FLAC++/all.h FLAC++/decoder.h FLAC++/encoder.h FLAC++/export.h FLAC++/metadata.h)

# Find the library.
find_library(FLACPP_LIBRARIES
NAMES FLAC++)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(FLACPP DEFAULT_MSG FLACPP_LIBRARIES FLACPP_INCLUDE_DIRS)
2 changes: 1 addition & 1 deletion src/welle-cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ function playerLoad() {
}

function setPlayerSource(sid) {
document.getElementById("player").src = "/mp3/" + sid;
document.getElementById("player").src = "/stream/" + sid;
playerLoad();
}

Expand Down
198 changes: 165 additions & 33 deletions src/welle-cli/webprogrammehandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,135 @@
#include "webprogrammehandler.h"
#include <iostream>
#include <algorithm>
#include <functional>

#include <lame/lame.h>

using namespace std;

#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL 0
#endif

class IEncoder
{
public:
virtual bool process_interleaved(std::vector<int16_t>& audioData) = 0;
virtual ~IEncoder() = default;
};

#ifdef HAVE_FLAC
#include <FLAC++/encoder.h>
class FlacEncoder : protected FLAC::Encoder::Stream, public IEncoder
{
private:
std::function<void(const std::vector<uint8_t>& headerData, const std::vector<uint8_t>& data)> handlerFunc;
std::vector<uint8_t> flacHeader;
bool streamHeaderInitialised = false;
// The audio decoders always upconvert to stereo
const int channels = 2;

public :
FlacEncoder(int sample_rate, std::function<void(const std::vector<uint8_t>& headerData, const std::vector<uint8_t>& data)> handler) : handlerFunc(handler)
{
set_streamable_subset(true);
set_channels(2);
set_sample_rate(sample_rate);
set_compression_level(5);
// Header created during this call
init();
// Header data finished
streamHeaderInitialised = true;

}

bool process_interleaved(std::vector<int16_t>& audioData) override
{
std::vector<int32_t> pcm_32(audioData.size());

// Convert 16bit samples to 32bit samples
for(long unsigned int i = 0; i < audioData.size(); i++)
{
pcm_32[i] = (int)audioData[i];
}

return FLAC::Encoder::Stream::process_interleaved(pcm_32.data(), pcm_32.size()/channels);
}

FLAC__StreamEncoderWriteStatus write_callback(const FLAC__byte buffer[], size_t bytes, uint32_t samples, uint32_t current_frame) override
{
if (!streamHeaderInitialised)
{
flacHeader.insert(flacHeader.end(), buffer, buffer + bytes);
}
else
{
std::vector<uint8_t> vectdata(buffer, buffer + bytes);
handlerFunc(flacHeader, vectdata);
}

return FLAC__STREAM_ENCODER_WRITE_STATUS_OK;
}

~FlacEncoder()
{
finish();
}
};

#endif

class LameEncoder : public IEncoder {
std::function<void(const std::vector<uint8_t>& headerData, const std::vector<uint8_t>& data)> handlerFunc;
lame_t lame;
// The audio decoders always upconvert to stereo
const int channels = 2;

public:

LameEncoder(int sample_rate, std::function<void(const std::vector<uint8_t>& headerData, const std::vector<uint8_t>& data)> handler) : handlerFunc(handler)
{
lame = lame_init();
lame_set_in_samplerate(lame, sample_rate);
lame_set_num_channels(lame, channels);
lame_set_VBR(lame, vbr_default);
lame_set_VBR_q(lame, 2);
lame_init_params(lame);
}

LameEncoder(const LameEncoder& other) = delete;
LameEncoder& operator=(const LameEncoder& other) = delete;
LameEncoder(LameEncoder&& other) = default;
LameEncoder& operator=(LameEncoder&& other) = default;

bool process_interleaved(std::vector<int16_t>& audioData) override
{
vector<uint8_t> mp3buf(16384);

int written = lame_encode_buffer_interleaved(lame,
audioData.data(), audioData.size()/channels,
mp3buf.data(), mp3buf.size());

if (written < 0) {
cerr << "Failed to encode mp3: " << written << endl;
}
else if (written > (ssize_t)mp3buf.size()) {
cerr << "mp3 encoder wrote more than buffer size!" << endl;
}
else if (written > 0) {
mp3buf.resize(written);
handlerFunc(std::vector<uint8_t>(), mp3buf);
}

return true;
}

~LameEncoder() {
lame_close(lame);
}
};


ProgrammeSender::ProgrammeSender(Socket&& s) :
s(move(s))
{
Expand All @@ -49,15 +171,23 @@ ProgrammeSender& ProgrammeSender::operator=(ProgrammeSender&& other)
return *this;
}

bool ProgrammeSender::send_mp3(const std::vector<uint8_t>& mp3Data)
bool ProgrammeSender::send_stream(const std::vector<uint8_t>& headerdata, const std::vector<uint8_t>& mp3Data)
{
if (not s.valid()) {
return false;
}

const int flags = MSG_NOSIGNAL;
ssize_t ret = 0;

if (!headerSent)
{
ret = s.send(headerdata.data(), headerdata.size(), flags);
headerSent = true;
}

ret = s.send(mp3Data.data(), mp3Data.size(), flags);

ssize_t ret = s.send(mp3Data.data(), mp3Data.size(), flags);
if (ret == -1) {
s.close();
std::unique_lock<std::mutex> lock(mutex);
Expand All @@ -84,8 +214,8 @@ void ProgrammeSender::cancel()
running = false;
}

WebProgrammeHandler::WebProgrammeHandler(uint32_t serviceId) :
serviceId(serviceId)
WebProgrammeHandler::WebProgrammeHandler(uint32_t serviceId, OutputCodec codecID) :
serviceId(serviceId), codec(codecID)
{
const auto now = chrono::system_clock::now();
time_label = now;
Expand All @@ -96,6 +226,7 @@ WebProgrammeHandler::WebProgrammeHandler(uint32_t serviceId) :

WebProgrammeHandler::WebProgrammeHandler(WebProgrammeHandler&& other) :
serviceId(other.serviceId),
codec(other.codec),
senders(move(other.senders))
{
other.senders.clear();
Expand All @@ -108,6 +239,11 @@ WebProgrammeHandler::WebProgrammeHandler(WebProgrammeHandler&& other) :
time_mot_change = now;
}

WebProgrammeHandler::~WebProgrammeHandler()
{

}

void WebProgrammeHandler::registerSender(ProgrammeSender *sender)
{
std::unique_lock<std::mutex> lock(senders_mutex);
Expand Down Expand Up @@ -200,9 +336,6 @@ void WebProgrammeHandler::onNewAudio(std::vector<int16_t>&& audioData,
return;
}

// The audio decoders always upconvert to stereo
const int channels = 2;

int last_audioLevel_L = 0;
int last_audioLevel_R = 0;
{
Expand All @@ -223,37 +356,36 @@ void WebProgrammeHandler::onNewAudio(std::vector<int16_t>&& audioData,
audiolevels.last_audioLevel_R = last_audioLevel_R;
}

if (not lame_initialised) {
lame_set_in_samplerate(lame.lame, rate);
lame_set_num_channels(lame.lame, channels);
lame_set_VBR(lame.lame, vbr_default);
lame_set_VBR_q(lame.lame, 2);
lame_init_params(lame.lame);
lame_initialised = true;
if (encoder == nullptr)
{
switch (codec)
{
case OutputCodec::MP3 :
encoder = make_unique<LameEncoder>(rate, [&](const vector<uint8_t>& headerData, const vector<uint8_t>& vectData){send_to_all_clients(headerData, vectData);});
break;
#ifdef HAVE_FLAC
case OutputCodec::FLAC :
encoder = make_unique<FlacEncoder>(rate, [&](const vector<uint8_t>& headerData, const vector<uint8_t>& vectData){send_to_all_clients(headerData, vectData);});
break;
#endif
default:
throw runtime_error("OutputCodec not handled, did you compile with flac support ?");
break;
}
}

vector<uint8_t> mp3buf(16384);

int written = lame_encode_buffer_interleaved(lame.lame,
audioData.data(), audioData.size()/channels,
mp3buf.data(), mp3buf.size());
encoder->process_interleaved(audioData);

if (written < 0) {
cerr << "Failed to encode mp3: " << written << endl;
}
else if (written > (ssize_t)mp3buf.size()) {
cerr << "mp3 encoder wrote more than buffer size!" << endl;
}
else if (written > 0) {
mp3buf.resize(written);
}

std::unique_lock<std::mutex> lock(senders_mutex);
void WebProgrammeHandler::send_to_all_clients(const std::vector<uint8_t>& headerData, const std::vector<uint8_t>& data)
{
std::unique_lock<std::mutex> lock(senders_mutex);

for (auto& s : senders) {
bool success = s->send_mp3(mp3buf);
if (not success) {
cerr << "Failed to send audio for " << serviceId << endl;
}
for (auto& s : senders) {
bool success = s->send_stream(headerData, data);
if (not success) {
cerr << "Failed to send audio for " << serviceId << endl;
}
}
}
Expand Down
Loading

0 comments on commit 552ccae

Please sign in to comment.