From c3c452ff5d85168bcf37f67c6f8d961b8ff1d0c7 Mon Sep 17 00:00:00 2001 From: faraphel Date: Tue, 22 Oct 2024 20:14:45 +0200 Subject: [PATCH] audio settings can now change dynamically --- CMakeLists.txt | 2 ++ source/Client.cpp | 52 ++++++++++++++++++++++++++-------- source/Client.hpp | 18 ++++++++---- source/Server.cpp | 39 ++++++++++++------------- source/Server.hpp | 24 +++------------- source/main.cpp | 2 +- source/packets/AudioPacket.hpp | 5 ++++ source/utils/audio.cpp | 27 ++++++++++++++++++ source/utils/audio.hpp | 6 ++++ 9 files changed, 117 insertions(+), 58 deletions(-) create mode 100644 source/utils/audio.cpp create mode 100644 source/utils/audio.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 47ab947..cc277b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,8 @@ add_executable(M2-PT-DRP source/Server.cpp source/Server.hpp source/packets/AudioPacket.hpp + source/utils/audio.cpp + source/utils/audio.hpp ) target_include_directories(M2-PT-DRP PRIVATE ${MPG123_INCLUDE_DIRS} diff --git a/source/Client.cpp b/source/Client.cpp index eaac886..20f4fb2 100644 --- a/source/Client.cpp +++ b/source/Client.cpp @@ -13,24 +13,45 @@ #include "packets/AudioPacket.hpp" -Client::Client(const int channels, const double rate) { +Client::Client() { this->stream = nullptr; this->audioLock = std::unique_lock(this->audioMutex); - this->channels = channels; - // TODO(Faraphel): make the sampleFormat an argument. - // open a PortAudio stream + this->streamChannels = 0; + this->streamSampleFormat = 0; + this->streamRate = 0; +} + +void Client::updateStream(const int channels, const std::uint32_t sampleFormat, const double sampleRate) { + // check if any information changed. If no, ignore this + if ( + this->streamChannels == channels && + this->streamSampleFormat == sampleFormat && + this->streamRate == sampleRate + ) + return; + + // close the current stream + // ignore errors that could happen if no audio is currently playing + Pa_CloseStream(&this->stream); + + // open a new stream with the new settings if (const PaError error = Pa_OpenDefaultStream( &this->stream, 0, channels, - paInt16, - rate, + sampleFormat, + sampleRate, paFramesPerBufferUnspecified, nullptr, nullptr ) != paNoError) throw std::runtime_error("[Client] Could not open the stream: " + std::string(Pa_GetErrorText(error))); + + // update the new audio values + this->streamChannels = channels; + this->streamSampleFormat = sampleFormat; + this->streamRate = sampleRate; } Client::~Client() { @@ -126,13 +147,18 @@ void Client::loopPlayer() { [this] { return !this->audioQueue.empty(); } ); // get the most recent audio chunk - const auto audioPacket = audioQueue.top(); + const auto audioPacket = this->audioQueue.top(); + + // update the stream with the new audio settings + this->updateStream( + audioPacket.channels, + audioPacket.sampleFormat, + audioPacket.sampleRate + ); + // wait until it must be played std::this_thread::sleep_until(audioPacket.timePlay); - // TODO(Faraphel) / 2 => / encoding size - // TODO(Faraphel): the number of frames could be improved - std::cout << "[Client] Playing: " << audioPacket.timePlay << std::endl; // immediately stop playing music @@ -145,15 +171,17 @@ void Client::loopPlayer() { throw std::runtime_error("[Client] Could not start the PortAudio stream: " + std::string(Pa_GetErrorText(error))); // write the new audio data into the audio buffer + // TODO(Faraphel) / 2 => / encoding size + // TODO(Faraphel): the number of frames could be improved const int error = Pa_WriteStream( this->stream, audioPacket.content.data(), - audioPacket.contentSize / 2 / this->channels + audioPacket.contentSize / 2 / this->streamChannels ); switch (error) { // success case paNoError: - // the output might be very slightly underflown, + // the output might be very slightly underflowed, // causing a very small period where no noise will be played. case paOutputUnderflowed: break; diff --git a/source/Client.hpp b/source/Client.hpp index 5210a59..2c91c4d 100644 --- a/source/Client.hpp +++ b/source/Client.hpp @@ -24,13 +24,17 @@ struct AudioPacketsComparator { */ class Client { public: - /** - * :param channels: the number of channel in the audio - * :param rate: the rate of the audio - */ - explicit Client(int channels, double rate); + explicit Client(); ~Client(); + /** + * Update the current audio stream + * @param channels the number of channels + * @param sampleFormat the sample format type + * @param sampleRate the audio rate + */ + void updateStream(int channels, std::uint32_t sampleFormat, double sampleRate); + /** * Indefinitely receive and play audio data. */ @@ -48,7 +52,9 @@ private: void loopPlayer(); PaStream* stream; - int channels; + int streamChannels; + std::uint32_t streamSampleFormat; + double streamRate; std::priority_queue, AudioPacketsComparator> audioQueue; std::mutex audioMutex; diff --git a/source/Server.cpp b/source/Server.cpp index 6b1b0fc..1488ec2 100644 --- a/source/Server.cpp +++ b/source/Server.cpp @@ -11,9 +11,14 @@ #include #include "packets/AudioPacket.hpp" +#include "utils/audio.hpp" Server::Server() { + this->channels = 0; + this->encoding = 0; + this->sampleRate = 0; + // create a new mpg123 handle int error; this->mpgHandle = mpg123_new(nullptr, &error); @@ -21,16 +26,18 @@ Server::Server() { throw std::runtime_error("[Server] Could not create a mpg123 handle."); // open the mp3 file + // TODO(Faraphel): mp3 file as argument if (mpg123_open( this->mpgHandle, - "./assets/Caravan Palace - Wonderland.mp3" - )) + // "./assets/Caravan Palace - Wonderland.mp3" + "./assets/Queen - Another One Bites the Dust.mp3" + ) != MPG123_OK) throw std::runtime_error("[Server] Could not open file."); // get the format of the file if (mpg123_getformat( this->mpgHandle, - &this->rate, + &this->sampleRate, &this->channels, &this->encoding ) != MPG123_OK) @@ -43,7 +50,7 @@ Server::~Server() { mpg123_delete(this->mpgHandle); } -void Server::loop() { +void Server::loop() const { // get the broadcast address addrinfo broadcastHints {}; broadcastHints.ai_family = AF_INET6; @@ -79,13 +86,20 @@ void Server::loop() { &done ) == MPG123_OK) { // set the target time + // TODO(Faraphel): dynamically change this delay to be the lowest possible audioPacket.timePlay = std::chrono::high_resolution_clock::now() + std::chrono::milliseconds(5000); + // set the audio settings + audioPacket.channels = this->channels; + audioPacket.sampleFormat = encoding_mpg123_to_PulseAudio(this->encoding); + audioPacket.sampleRate = this->sampleRate; + // set the size of the content audioPacket.contentSize = done; + // broadcast the audio data if (sendto( broadcastSocket, &audioPacket, @@ -100,10 +114,10 @@ void Server::loop() { std::cout << "[Server] Sent: " << done << " bytes" << std::endl; - // wait 10ms to simulate lag // TODO(Faraphel): should be extended to simulate live music streaming + // wait for the duration of the audio chunk std::this_thread::sleep_for(std::chrono::milliseconds(static_cast( - (1 / static_cast(this->rate * this->channels * mpg123_encsize(this->encoding))) * + (1 / static_cast(this->sampleRate * this->channels * mpg123_encsize(this->encoding))) * 1000 * static_cast(done) ))); @@ -112,16 +126,3 @@ void Server::loop() { // free the server address freeaddrinfo(broadcastInfo); } - - -long Server::getRate() const { - return this->rate; -} - -int Server::getChannels() const { - return this->channels; -} - -int Server::getEncoding() const { - return this->encoding; -} diff --git a/source/Server.hpp b/source/Server.hpp index bb7d11d..396e661 100644 --- a/source/Server.hpp +++ b/source/Server.hpp @@ -14,28 +14,12 @@ public: /** * Indefinitely read and broadcast audio data. */ - void loop(); - - /** - * get the current rate of the audio data. - * @return the current rate of the audio data. - */ - long getRate() const; - /** - * get the current number of channels of the audio data. - * @return the current number of channels of the audio data. - */ - int getChannels() const; - /** - * get the current encoding of the audio data. - * @return the current encoding of the audio data. - */ - int getEncoding() const; + void loop() const; private: mpg123_handle* mpgHandle; - long rate{}; - int channels{}; - int encoding{}; + long sampleRate; + int channels; + int encoding; }; diff --git a/source/main.cpp b/source/main.cpp index 84b8a3e..b2318be 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -20,7 +20,7 @@ int main(int argc, char* argv[]) { // start the client and server Server server; - Client client(server.getChannels(), static_cast(server.getRate())); + Client client; std::thread serverThread(&Server::loop, &server); std::thread clientThread(&Client::loop, &client); diff --git a/source/packets/AudioPacket.hpp b/source/packets/AudioPacket.hpp index f0ee918..a437041 100644 --- a/source/packets/AudioPacket.hpp +++ b/source/packets/AudioPacket.hpp @@ -6,7 +6,12 @@ struct AudioPacket { // scheduling + // TODO(Faraphel): use a more "fixed" size format ? std::chrono::time_point timePlay; + // audio settings + std::uint8_t channels; + std::uint32_t sampleFormat; + std::uint32_t sampleRate; // content std::uint16_t contentSize; std::array content; diff --git a/source/utils/audio.cpp b/source/utils/audio.cpp new file mode 100644 index 0000000..bfee1e0 --- /dev/null +++ b/source/utils/audio.cpp @@ -0,0 +1,27 @@ +#include "audio.hpp" + +#include + +#include +#include + + +std::uint32_t encoding_mpg123_to_PulseAudio(const int encoding_mpg123) { + switch (encoding_mpg123) { + case MPG123_ENC_UNSIGNED_8: + return paUInt8; + case MPG123_ENC_SIGNED_8: + return paInt8; + case MPG123_ENC_SIGNED_16: + return paInt16; + case MPG123_ENC_SIGNED_24: + return paInt24; + case MPG123_ENC_SIGNED_32: + return paInt32; + case MPG123_ENC_FLOAT: + case MPG123_ENC_FLOAT_32: + return paFloat32; + default: + throw std::runtime_error("Invalid encoding value."); + } +} diff --git a/source/utils/audio.hpp b/source/utils/audio.hpp new file mode 100644 index 0000000..3e6526d --- /dev/null +++ b/source/utils/audio.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include + + +std::uint32_t encoding_mpg123_to_PulseAudio(int encoding_mpg123); \ No newline at end of file