#include "AudioEvent.hpp" #include #include #include namespace drp::event { AudioEvent::AudioEvent() { this->stream = nullptr; this->audioLock = std::unique_lock(this->audioMutex); this->streamChannels = 0; this->streamSampleFormat = 0; this->streamRate = 0; // start a thread for the player this->playerThread = std::thread(&AudioEvent::loopPlay, this); } AudioEvent::~AudioEvent() { // stop any currently playing audio Pa_StopStream(this->stream); // close the audio stream if (const PaError error = Pa_CloseStream(this->stream)) std::cerr << "[Client] Could not close the stream: " << std::string(Pa_GetErrorText(error)) << std::endl; } void AudioEvent::handle( Context& context, const packet::GenericPacketContent& content, sockaddr* fromAddress, socklen_t fromAddressLength ) { // get the audio data in the content packet::AudioPacketData audioData; std::memcpy(&audioData, content.data.data(), sizeof(packet::AudioPacketData)); // save it in the audio queue this->audioQueue.push(audioData); // notify that a new audio chunk is available this->audioCondition.notify_one(); } void AudioEvent::updateAudioStream(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, 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; } void AudioEvent::loopPlay() { while (true) { // wait for a new element in the audio queue this->audioCondition.wait( this->audioLock, [this] { return !this->audioQueue.empty(); } ); // get the most recent audio chunk const auto audioPacket = this->audioQueue.top(); // update the stream with the new audio settings this->updateAudioStream( audioPacket.channels, audioPacket.sampleFormat, audioPacket.sampleRate ); // wait until it must be played std::this_thread::sleep_until(audioPacket.timePlay); auto cTimePlay = std::chrono::high_resolution_clock::to_time_t(audioPacket.timePlay); std::cout << "[Client] Playing: " << std::ctime(&cTimePlay) << std::endl; // immediately stop playing music // this avoids an offset created if this client's clock is too ahead of the others // don't handle errors since audio might not be playing before Pa_AbortStream(this->stream); // play the new audio data if (const int error = Pa_StartStream(this->stream) != paNoError) 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 const int error = Pa_WriteStream( this->stream, audioPacket.content.data(), audioPacket.contentSize / Pa_GetSampleSize(this->streamSampleFormat) / this->streamChannels ); switch (error) { // success case paNoError: // the output might be very slightly underflown, // causing a very small period where no noise will be played. case paOutputUnderflowed: break; default: std::cerr << "[Client] Could not write to the audio stream: " << Pa_GetErrorText(error) << std::endl; } // remove the audio chunk this->audioQueue.pop(); } } }