From d982fd74691523a9b593e371660e41c64354c510 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Wed, 15 Apr 2020 13:30:20 +0300 Subject: [PATCH] Io service per song (#1) --- CMakeLists.txt | 2 +- src/alsa_frames_transfer.h | 169 ---- src/current_song_controller.cc | 131 +-- src/current_song_controller.h | 7 +- .../alsa_service.cc} | 784 ++++++++++-------- src/services/alsa_service.h | 55 ++ src/wavplayeralsa.cpp | 34 +- 7 files changed, 573 insertions(+), 609 deletions(-) delete mode 100644 src/alsa_frames_transfer.h rename src/{alsa_frames_transfer.cc => services/alsa_service.cc} (66%) create mode 100644 src/services/alsa_service.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 769d598..9153794 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,12 +10,12 @@ include_directories(thirdparty/mqtt_cpp) set (SOURCES src/wavplayeralsa.cpp - src/alsa_frames_transfer.cc src/web_sockets_api.cc src/http_api.cc src/mqtt_api.cc src/audio_files_manager.cc src/current_song_controller.cc + src/services/alsa_service.cc ) set(CMAKE_BUILD_TYPE Debug) diff --git a/src/alsa_frames_transfer.h b/src/alsa_frames_transfer.h deleted file mode 100644 index 5aff509..0000000 --- a/src/alsa_frames_transfer.h +++ /dev/null @@ -1,169 +0,0 @@ -#ifndef WAVPLAYERALSA_ALSA_FRAMES_TRANSFER_H_ -#define WAVPLAYERALSA_ALSA_FRAMES_TRANSFER_H_ - -#include -#include -#include -#include -#include -#include - -#include "alsa/asoundlib.h" -#include "sndfile.hh" -#include "spdlog/spdlog.h" - -#include "player_events_ifc.h" - -/* -Detailed explanations on how this module operate. -This it the part that actually speak with ALSA, and send the audio data that should be played by the audio hardware. -How do we send audio to be played? - * we use ALSA library API, - -> which speak with ALSA kernel API - -> which speaks with the audio driver - -> which send the audio data to the sound card - -> which send the audio data to the hardware - -> which produces the actual sound that we hear - -ALSA (Advanced Linux Sound Architecture) is the low level software framework that handles sound in linux. -Why did I choose to use ALSA? first I discuss what was the goals I wanted to achive. - -Objectives: -This application was written to handle the job of running LED shows, synchronized to music. -Like watching a movie, it is extremely important for a good synchronization of the visual and the audio. -If a gap of more than few tens of milliseconds is present, the user expirence is severily damaged. -So the first objective of the application is to achive very accurate offset in the played audio (few ms at most). -The other issue is reliability. The application should be able to efficiently and reliabliy handle -reasonable file formats and data representaions on many platforms. -The third issue is to allow other parts of the software stack to be written in a "microservice" approch, -in any programming language, and in any architecture (not necessarely as a single process or on the same machine). - -Why alsa then? -Alsa is the low level part of sound handling in linux, so using it would allow for the most -reliable, efficient, and precise handling of audio. -Other higher-level libraries that are built on top of ALSA, can introduce problems which we've seen: -- incorrect audio offset reporting -- non supporting format (like Java's javax.sound.sampled.AudioSystem which had no support for mono - files on raspberry pi, causing waste of hours of debuging, and prone to error limitation to - convert all used files to stereo). -- efficient use of the resources - working with c++ and removing other layers that might add "surprises" - and have different objectives then us. - -So how do we speak with ALSA? - -First, we open the raw wav file with the libsndfile library, which we use to: -1. read file metadata from the header: - 1.1 frame rate - how many frames per seconds should be played - 1.2 channels - how many samples are in each frame - 1.3 sample size in bytes - how many bytes are used to save each sample - 1.4 sample type - how is each sampled represented in the file? signed / unsigned / float? - 1.5 Endianness - is each sample stored as little or big endian order. -2. read raw frames from the file, and give it to ALSA for playback. - sndfile allows us to conveniently read frames in file, instead of calculating buffers offsets - that should take into account the file header, number of channels, and sample byte size. - -Then we open an alsa pcm device which is used to configure and transfer frames for playback by alsa. -*/ - -namespace wavplayeralsa { - - class AlsaFramesTransfer { - - public: - - AlsaFramesTransfer(); - ~AlsaFramesTransfer(); - - // can throw exception - void Initialize(std::shared_ptr logger, - PlayerEventsIfc *player_events_callback, - const std::string &audio_device); - - // file id is a string identifier that is used to refer to the file being played. - // it is something like: beep.wav or beatles/let_it_be.wav - // what should *NOT* be used as fileId is: ../../songs/beep.wav songs//beep.wav ./beep.wav - // it should be canonical, so that each file is always identify uniquely - const std::string &GetFileId() const; - bool LoadNewFile(const std::string &full_file_name, const std::string &file_id); - void StartPlay(uint32_t position_in_ms, uint32_t play_seq_id); - bool Stop(); - - private: - void InitSndFile(); - void InitAlsa(); - bool GetFormatForAlsa(snd_pcm_format_t &out_format) const; - - private: - void TransferFramesWrapper(uint32_t play_seq_id); - bool IsAlsaStatePlaying(); - void CheckSongStartTime(uint32_t play_seq_id); - // once all frames are written to pcm, this function runs until pcm is empty - void PcmDrainLoop(boost::system::error_code error_code, uint32_t play_seq_id); - void FramesToPcmTransferLoop(boost::system::error_code error_code, uint32_t play_seq_id); - void PcmDrop(); - - private: - std::shared_ptr logger_; - - private: - // initialization detection - bool initialized_ = false; - bool snd_initialized_ = false; // if true -> sound file is loaded successfully, and alsa is configured to support it - - private: - // current loaded file - std::string file_id_; - std::string full_file_name_; - - private: - // snd file stuff - - enum SampleType { - SampleTypeSigned = 0, - SampleTypeUnsigned = 1, - SampleTypeFloat = 2 - }; - static const char *SampleTypeToString(SampleType sample_type); - - SndfileHandle snd_file_; - - // from file - unsigned int frame_rate_ = 44100; - unsigned int num_of_channels_ = 2; - bool is_endian_little_ = true; // if false the endian is big :) - SampleType sample_type_ = SampleTypeSigned; - unsigned int bytes_per_sample_ = 2; - uint64_t total_frame_in_file_ = 0; - - // calculated - unsigned int bytes_per_frame_ = 1; - snd_pcm_sframes_t frames_capacity_in_buffer_ = 0; // how many frames can be stored in a buffer with size TRANSFER_BUFFER_SIZE - - private: - // alsa frames transfer stuff - snd_pcm_t *alsa_playback_handle_; - - static const int AVAIL_MIN = 4096; // tells alsa to return from wait when buffer has this size of free space - static const int TRANSFER_BUFFER_SIZE = 4096 * 16; // 64KB this is the buffer used to pass frames to alsa. this is the maximum number of frames to pass as one chunk - - // what is the next frame to be delivered to alsa - uint64_t curr_position_frames_ = 0; - - private: - // transfer thread - boost::asio::io_service alsa_ios_; - boost::asio::deadline_timer alsa_wait_timer_; - std::thread playing_thread_; - - private: - // position reporting - uint64_t audio_start_time_ms_since_epoch_ = 0; - PlayerEventsIfc *player_events_callback_ = nullptr; - - - }; - -} - - -#endif // WAVPLAYERALSA_ALSA_FRAMES_TRANSFER_H_ \ No newline at end of file diff --git a/src/current_song_controller.cc b/src/current_song_controller.cc index d9e6be5..5a22428 100644 --- a/src/current_song_controller.cc +++ b/src/current_song_controller.cc @@ -10,17 +10,17 @@ namespace wavplayeralsa { CurrentSongController::CurrentSongController( - boost::asio::io_service &io_service, - MqttApi *mqtt_service, - WebSocketsApi *ws_service, - AlsaFramesTransfer *alsa_service) - : - ios_(io_service), - mqtt_service_(mqtt_service), - ws_service_(ws_service), - alsa_service_(alsa_service), - play_seq_id_(0), - throttle_timer_(io_service) + boost::asio::io_service &io_service, + MqttApi *mqtt_service, + WebSocketsApi *ws_service, + AlsaPlaybackServiceFactory *alsa_playback_service_factory + ) : + ios_(io_service), + mqtt_service_(mqtt_service), + ws_service_(ws_service), + alsa_playback_service_factory_(alsa_playback_service_factory), + play_seq_id_(0), + throttle_timer_(io_service) { } @@ -61,56 +61,65 @@ namespace wavplayeralsa std::stringstream &out_msg, uint32_t *play_seq_id) { + bool prev_file_was_playing = false; + std::string prev_file_id; + + if(alsa_service_ != nullptr) { + prev_file_id = alsa_service_->GetFileId(); + prev_file_was_playing = alsa_service_->Stop(); + delete alsa_service_; + alsa_service_ = nullptr; + } + + // create a new unique id for this play + uint32_t new_play_seq_id = play_seq_id_ + 1; + play_seq_id_ = new_play_seq_id; + if(play_seq_id != nullptr) + { + *play_seq_id = play_seq_id_; + } - if(file_id == alsa_service_->GetFileId()) { + boost::filesystem::path songPathInWavDir(file_id); + boost::filesystem::path songFullPath = wav_dir_ / songPathInWavDir; + std::string canonicalFullPath; + try { + canonicalFullPath = boost::filesystem::canonical(songFullPath).string(); + alsa_service_ = alsa_playback_service_factory_->CreateAlsaPlaybackService( + canonicalFullPath, + file_id, + new_play_seq_id + ); + } + catch(const std::runtime_error &e) { + out_msg << "failed loading new audio file '" << file_id << "'. currently no audio file is loaded in the player and it is not playing. " << + "reason for failure: " << e.what(); + return false; + } + + if(file_id == prev_file_id) { out_msg << "changed position of the current file '" << file_id << "'. new position in ms is: " << start_offset_ms << std::endl; } else { - - // create the canonical full path of the file to play - boost::filesystem::path songPathInWavDir(file_id); - boost::filesystem::path songFullPath = wav_dir_ / songPathInWavDir; - std::string canonicalFullPath; - try { - canonicalFullPath = boost::filesystem::canonical(songFullPath).string(); + static const int SECONDS_PER_HOUR = (60 * 60); + uint64_t start_offset_sec = start_offset_ms / 1000; + uint64_t hours = start_offset_sec / SECONDS_PER_HOUR; + start_offset_sec = start_offset_sec - hours * SECONDS_PER_HOUR; + uint64_t minutes = start_offset_sec / 60; + uint64_t seconds = start_offset_sec % 60; + if(prev_file_was_playing && !prev_file_id.empty()) { + out_msg << "audio file successfully changed from '" << prev_file_id << "' to '" << file_id << "' and will be played "; } - catch (const std::exception &e) { - out_msg << "loading new audio file '" << file_id << "' failed. error: " << e.what(); - return false; - } - - try { - const std::string prev_file = alsa_service_->GetFileId(); - bool prev_file_was_playing = alsa_service_->LoadNewFile(canonicalFullPath, file_id); - - // message printing - const int SECONDS_PER_HOUR = (60 * 60); - uint64_t start_offset_sec = start_offset_ms / 1000; - uint64_t hours = start_offset_sec / SECONDS_PER_HOUR; - start_offset_sec = start_offset_sec - hours * SECONDS_PER_HOUR; - uint64_t minutes = start_offset_sec / 60; - uint64_t seconds = start_offset_sec % 60; - if(prev_file_was_playing && !prev_file.empty()) { - out_msg << "audio file successfully changed from '" << prev_file << "' to '" << file_id << "' and will be played "; - } - else { - out_msg << "will play audio file '" << file_id << "' "; - } - out_msg << "starting at position " << start_offset_ms << " ms " << - "(" << hours << ":" << - std::setfill('0') << std::setw(2) << minutes << ":" << - std::setfill('0') << std::setw(2) << seconds << ")"; - } - catch(const std::runtime_error &e) { - out_msg << "loading new audio file '" << file_id << "' failed. currently no audio file is loaded in the player and it is not playing. " << - "reason for failure: " << e.what(); - return false; + else { + out_msg << "will play audio file '" << file_id << "' "; } + out_msg << "starting at position " << start_offset_ms << " ms " << + "(" << hours << ":" << + std::setfill('0') << std::setw(2) << minutes << ":" << + std::setfill('0') << std::setw(2) << seconds << ")"; } - uint32_t new_play_seq_id = play_seq_id_ + 1; try { - alsa_service_->StartPlay(start_offset_ms, new_play_seq_id); + alsa_service_->Play(start_offset_ms); } catch(const std::runtime_error &e) { out_msg << "playing new audio file '" << file_id << "' failed. currently player is not playing. " << @@ -118,12 +127,6 @@ namespace wavplayeralsa return false; } - play_seq_id_ = new_play_seq_id; - if(play_seq_id != nullptr) - { - *play_seq_id = play_seq_id_; - } - return true; } @@ -131,17 +134,15 @@ namespace wavplayeralsa std::stringstream &out_msg, uint32_t *play_seq_id) { - bool was_playing = false; - try { + std::string current_file_id; + if(alsa_service_ != nullptr) { + current_file_id = alsa_service_->GetFileId(); was_playing = alsa_service_->Stop(); - } - catch(const std::runtime_error &e) { - out_msg << "Unable to stop current audio file successfully, error: " << e.what(); - return false; + delete alsa_service_; + alsa_service_ = nullptr; } - const std::string ¤t_file_id = alsa_service_->GetFileId(); if(current_file_id.empty() || !was_playing) { out_msg << "no audio file is being played, so stop had no effect"; } diff --git a/src/current_song_controller.h b/src/current_song_controller.h index 945af54..73ecb43 100644 --- a/src/current_song_controller.h +++ b/src/current_song_controller.h @@ -10,7 +10,7 @@ #include "player_actions_ifc.h" #include "mqtt_api.h" #include "web_sockets_api.h" -#include "alsa_frames_transfer.h" +#include "services/alsa_service.h" using json = nlohmann::json; @@ -25,7 +25,7 @@ namespace wavplayeralsa { CurrentSongController(boost::asio::io_service &io_service, MqttApi *mqtt_service, WebSocketsApi *ws_service, - AlsaFramesTransfer *alsa_service); + AlsaPlaybackServiceFactory *alsa_playback_service_factory); void Initialize(const std::string &player_uuid, const std::string &wav_dir); @@ -55,7 +55,8 @@ namespace wavplayeralsa { boost::asio::io_service &ios_; MqttApi *mqtt_service_; WebSocketsApi *ws_service_; - AlsaFramesTransfer *alsa_service_ = nullptr; + AlsaPlaybackServiceFactory *alsa_playback_service_factory_; + IAlsaPlaybackService *alsa_service_ = nullptr; private: // static config diff --git a/src/alsa_frames_transfer.cc b/src/services/alsa_service.cc similarity index 66% rename from src/alsa_frames_transfer.cc rename to src/services/alsa_service.cc index 79b9714..0a21baf 100644 --- a/src/alsa_frames_transfer.cc +++ b/src/services/alsa_service.cc @@ -1,301 +1,147 @@ -#include "alsa_frames_transfer.h" +#include "services/alsa_service.h" -#include -#include -#include #include -#include -#include - -namespace wavplayeralsa { - - AlsaFramesTransfer::AlsaFramesTransfer() : - alsa_wait_timer_(alsa_ios_) - { - // we use the stop flag to indicate if a song is currently playing or not - alsa_ios_.stop(); - } - +#include +#include + +#include + +#include "alsa/asoundlib.h" +#include "sndfile.hh" +#include "spdlog/spdlog.h" +#include "spdlog/async.h" + +namespace wavplayeralsa +{ + + class AlsaPlaybackService : public IAlsaPlaybackService + { + + public: + + AlsaPlaybackService( + std::shared_ptr logger, + PlayerEventsIfc *player_events_callback_, + const std::string &full_file_name, + const std::string &file_id, + const std::string &audio_device, + uint32_t play_seq_id + ); + + ~AlsaPlaybackService(); + + public: + void Play(int32_t offset_in_ms); + bool Stop(); + const std::string GetFileId() const { return file_id_; } + + private: + + void InitSndFile(const std::string &full_file_name); + void InitAlsa(const std::string &audio_device); + + private: + void PlayingThreadMain(); + void FramesToPcmTransferLoop(boost::system::error_code error_code); + void PcmDrainLoop(boost::system::error_code error_code); + void PcmDrop(); + void CheckSongStartTime(); + bool IsAlsaStatePlaying(); + + private: + std::shared_ptr logger_; + boost::asio::io_service ios_; + boost::asio::deadline_timer alsa_wait_timer_; + std::thread playing_thread_; + bool initialized_ = false; + + // config + private: + const std::string file_id_; + const uint32_t play_seq_id_; + + // alsa + private: + static const int TRANSFER_BUFFER_SIZE = 4096 * 16; // 64KB this is the buffer used to pass frames to alsa. this is the maximum number of bytes to pass as one chunk + snd_pcm_t *alsa_playback_handle_ = nullptr; + + // what is the next frame to be delivered to alsa + uint64_t curr_position_frames_ = 0; + + // snd file + private: + SndfileHandle snd_file_; + + enum SampleType { + SampleTypeSigned = 0, + SampleTypeUnsigned = 1, + SampleTypeFloat = 2 + }; + static const char *SampleTypeToString(SampleType sample_type); + bool GetFormatForAlsa(snd_pcm_format_t &out_format) const; + + // from file + unsigned int frame_rate_ = 44100; + unsigned int num_of_channels_ = 2; + bool is_endian_little_ = true; // if false the endian is big :) + SampleType sample_type_ = SampleTypeSigned; + unsigned int bytes_per_sample_ = 2; + uint64_t total_frame_in_file_ = 0; + + // calculated + unsigned int bytes_per_frame_ = 1; + snd_pcm_sframes_t frames_capacity_in_buffer_ = 0; // how many frames can be stored in a buffer with size TRANSFER_BUFFER_SIZE + + // postions reporting + private: + uint64_t audio_start_time_ms_since_epoch_ = 0; + PlayerEventsIfc *player_events_callback_ = nullptr; + + }; + + AlsaPlaybackService::AlsaPlaybackService( + std::shared_ptr logger, + PlayerEventsIfc *player_events_callback, + const std::string &full_file_name, + const std::string &file_id, + const std::string &audio_device, + uint32_t play_seq_id + ) : + file_id_(file_id), + play_seq_id_(play_seq_id), + logger_(logger), + alsa_wait_timer_(ios_), + player_events_callback_(player_events_callback) + { + InitSndFile(full_file_name); + InitAlsa(audio_device); + initialized_ = true; + } - AlsaFramesTransfer::~AlsaFramesTransfer() { + AlsaPlaybackService::~AlsaPlaybackService() { - Stop(); + this->Stop(); if(alsa_playback_handle_ != nullptr) { snd_pcm_close(alsa_playback_handle_); alsa_playback_handle_ = nullptr; } - } - - const std::string &AlsaFramesTransfer::GetFileId() const { - return file_id_; - } - - void AlsaFramesTransfer::Initialize(std::shared_ptr logger, - PlayerEventsIfc *player_events_callback, - const std::string &audio_device) - { - - if(initialized_) { - throw std::runtime_error("Initialize called on an already initialized alsa player"); - } - - player_events_callback_ = player_events_callback; - logger_ = logger; - - int err; - std::stringstream err_desc; - - // audio_device should be somthing like "plughw:0,0", "default" - if( (err = snd_pcm_open(&alsa_playback_handle_, audio_device.c_str(), SND_PCM_STREAM_PLAYBACK, 0)) < 0) { - err_desc << "cannot open audio device " << audio_device << " (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); - } - - initialized_ = true; - } - - void AlsaFramesTransfer::CheckSongStartTime(uint32_t play_seq_id) { - int err; - snd_pcm_sframes_t delay = 0; - int64_t pos_in_frames = 0; - - if( (err = snd_pcm_delay(alsa_playback_handle_, &delay)) < 0) { - std::stringstream err_desc; - err_desc << "cannot query current offset in buffer (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); - } - - // this is a magic number test to remove end of file wrong reporting - if(delay < 4096) { - return; - } - pos_in_frames = curr_position_frames_ - delay; - int64_t ms_since_audio_file_start = ((pos_in_frames * (int64_t)1000) / (int64_t)frame_rate_); - - struct timeval tv; - gettimeofday(&tv, NULL); - // convert sec to ms and usec to ms - uint64_t curr_time_ms_since_epoch = (uint64_t)(tv.tv_sec) * 1000 + (uint64_t)(tv.tv_usec) / 1000; - uint64_t audio_file_start_time_ms_since_epoch = (int64_t)curr_time_ms_since_epoch - ms_since_audio_file_start; - - int64_t diff_from_prev = audio_file_start_time_ms_since_epoch - audio_start_time_ms_since_epoch_; - // there might be small jittering, we don't want to update the value often. - if(diff_from_prev <= 1 && diff_from_prev >= -1) - return; - - player_events_callback_->NewSongStatus(file_id_, play_seq_id, audio_file_start_time_ms_since_epoch, 1.0); - - std::stringstream msg_stream; - msg_stream << "play_seq_id: " << play_seq_id << ". "; - msg_stream << "calculated a new audio file start time: " << audio_file_start_time_ms_since_epoch << " (ms since epoch). "; - if(audio_start_time_ms_since_epoch_ > 0) { - msg_stream << "this is a change since last calculation of " << diff_from_prev << " ms. "; - } - msg_stream << "pcm delay in frames as reported by alsa: " << delay << " and position in file is " << - ms_since_audio_file_start << " ms. "; - logger_->info(msg_stream.str()); - - audio_start_time_ms_since_epoch_ = audio_file_start_time_ms_since_epoch; - } - - void AlsaFramesTransfer::TransferFramesWrapper(uint32_t play_seq_id) { - - try { - alsa_ios_.reset(); - alsa_ios_.post(std::bind(&AlsaFramesTransfer::FramesToPcmTransferLoop, this, boost::system::error_code(), play_seq_id)); - alsa_ios_.run(); - PcmDrop(); - } - catch(const std::runtime_error &e) { - logger_->error("play_seq_id: {}. error while playing current wav file. stopped transfering frames to alsa. exception is: {}", play_seq_id, e.what()); - } - logger_->info("play_seq_id: {}. handling done", play_seq_id); - player_events_callback_->NoSongPlayingStatus(file_id_, play_seq_id); - } - - void AlsaFramesTransfer::FramesToPcmTransferLoop(boost::system::error_code error_code, uint32_t play_seq_id) { - - // the function might be called from timer, in which case error_code might - // indicate the timer canceled and we should not invoke the function. - if(error_code) - return; - - std::stringstream err_desc; - int err; - - // calculate how many frames to write - snd_pcm_sframes_t frames_to_deliver; - if( (frames_to_deliver = snd_pcm_avail_update(alsa_playback_handle_)) < 0) { - if(frames_to_deliver == -EPIPE) { - throw std::runtime_error("an xrun occured"); - } - else { - err_desc << "unknown ALSA avail update return value (" << frames_to_deliver << ")"; - throw std::runtime_error(err_desc.str()); - } - } - else if(frames_to_deliver == 0) { - alsa_wait_timer_.expires_from_now(boost::posix_time::millisec(5)); - alsa_wait_timer_.async_wait(std::bind(&AlsaFramesTransfer::FramesToPcmTransferLoop, this, std::placeholders::_1, play_seq_id)); - return; - } - - // we want to deliver as many frames as possible. - // we can put frames_to_deliver number of frames, but the buffer can only hold frames_capacity_in_buffer_ frames - frames_to_deliver = std::min(frames_to_deliver, frames_capacity_in_buffer_); - unsigned int bytes_to_deliver = frames_to_deliver * bytes_per_frame_; - - // read the frames from the file. TODO: what if readRaw fails? - char buffer_for_transfer[TRANSFER_BUFFER_SIZE]; - bytes_to_deliver = snd_file_.readRaw(buffer_for_transfer, bytes_to_deliver); - if(bytes_to_deliver < 0) { - err_desc << "Failed reading raw frames from snd file. returned: " << sf_error_number(bytes_to_deliver); - throw std::runtime_error(err_desc.str()); - } - if(bytes_to_deliver == 0) { - logger_->info("play_seq_id: {}. done writing all frames to pcm. waiting for audio device to play remaining frames in the buffer", play_seq_id); - alsa_ios_.post(std::bind(&AlsaFramesTransfer::PcmDrainLoop, this, boost::system::error_code(), play_seq_id)); - return; - } - - int frames_written = snd_pcm_writei(alsa_playback_handle_, buffer_for_transfer, frames_to_deliver); - if( frames_written < 0) { - err_desc << "snd_pcm_writei failed (" << snd_strerror(frames_written) << ")"; - throw std::runtime_error(err_desc.str()); - } - - curr_position_frames_ += frames_written; - if(frames_written != frames_to_deliver) { - logger_->warn("play_seq_id: {}. transfered to alsa less frame then requested. frames_to_deliver: {}, frames_written: {}", play_seq_id, frames_to_deliver, frames_written); - snd_file_.seek(curr_position_frames_, SEEK_SET); - } - - CheckSongStartTime(play_seq_id); - - alsa_ios_.post(std::bind(&AlsaFramesTransfer::FramesToPcmTransferLoop, this, boost::system::error_code(), play_seq_id)); - } - - void AlsaFramesTransfer::PcmDrainLoop(boost::system::error_code error_code, uint32_t play_seq_id) { - - if(error_code) - return; - - bool is_currently_playing = IsAlsaStatePlaying(); - - if(!is_currently_playing) { - logger_->info("play_seq_id: {}. playing audio file ended successfully (transfered all frames to pcm and it is empty).", play_seq_id); - return; - } - - CheckSongStartTime(play_seq_id); - - alsa_wait_timer_.expires_from_now(boost::posix_time::millisec(5)); - alsa_wait_timer_.async_wait(std::bind(&AlsaFramesTransfer::PcmDrainLoop, this, std::placeholders::_1, play_seq_id)); - } - - void AlsaFramesTransfer::PcmDrop() - { - int err; - if( (err = snd_pcm_drop(alsa_playback_handle_)) < 0 ) { - std::stringstream err_desc; - err_desc << "snd_pcm_drop failed (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); - } - } - bool AlsaFramesTransfer::IsAlsaStatePlaying() - { - int status = snd_pcm_state(alsa_playback_handle_); - // the code had SND_PCM_STATE_PREPARED as well. - // it is removed, to resolve issue of song start playing after end of file. - // the drain function would not finish since the status is 'SND_PCM_STATE_PREPARED' - // because no frames were sent to alsa. - return (status == SND_PCM_STATE_RUNNING); // || (status == SND_PCM_STATE_PREPARED); - } - - void AlsaFramesTransfer::StartPlay(uint32_t position_in_ms, uint32_t play_seq_id) - { - if(!snd_initialized_) { - throw std::runtime_error("the player is not initialized with a valid sound file."); - } - - Stop(); - - double position_in_seconds = (double)position_in_ms / 1000.0; - curr_position_frames_ = position_in_seconds * (double)frame_rate_; - if(curr_position_frames_ > total_frame_in_file_) { - curr_position_frames_ = total_frame_in_file_; - } - sf_count_t seek_res = snd_file_.seek(curr_position_frames_, SEEK_SET); - - int err; - std::stringstream err_desc; - if( (err = snd_pcm_prepare(alsa_playback_handle_)) < 0 ) { - err_desc << "snd_pcm_prepare failed (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); - } - - logger_->info("start playing file {} from position {} mili-seconds ({} seconds)", file_id_, position_in_ms, position_in_seconds); - playing_thread_ = std::thread(&AlsaFramesTransfer::TransferFramesWrapper, this, play_seq_id); } /* - stop the current audio file from being played. - the following scenarios are possible: - 1. alsa's pcm is running, transfering frames to the audio device, and the monitoring thread is active - and serving frames \ waiting for buffer to drain. - 2. alsa pcm is drained, not more frames are sent to audio device, and the monitoring thread has finished - execution and waiting to be joined. - 3. the thread has been joined, thus there is noting to stop. - - The function returns true if it's invocation is what made the audio device stop playing (case 1). - It will return false if stop is called but the pcm is not running anyway (cases 2, 3) - */ - bool AlsaFramesTransfer::Stop() { - - bool is_playing = !(alsa_ios_.stopped()); - - alsa_wait_timer_.cancel(); - alsa_ios_.stop(); - - if(playing_thread_.joinable()) { - playing_thread_.join(); - } - audio_start_time_ms_since_epoch_ = 0; // invalidate old start time so on next play a new status will be sent - return is_playing; // if we were playing when we entered the function, then the invocation is what made it stop - } - - /* - return true if loading a new file caused the current audio to 'stop', return false if no file was playing anyway. - */ - bool AlsaFramesTransfer::LoadNewFile(const std::string &full_file_name, const std::string &file_id) { - - if(!initialized_) { - throw std::runtime_error("LoadNewFile called but player not initilized"); - } - - bool was_playing = this->Stop(); - - // mark snd as not initialized. after everything goes well, and no exception is thrown, it will be changed to initialized - snd_initialized_ = false; - - full_file_name_ = full_file_name; - file_id_ = file_id; - InitSndFile(); - InitAlsa(); - - snd_initialized_ = true; - - return was_playing; - } - - void AlsaFramesTransfer::InitSndFile() { - - snd_file_ = SndfileHandle(full_file_name_); + Read the file content from disk, extract relevant metadata from the + wav header, and save it to the relevant members of the class. + The function will also initialize the snd_file member, which allows to + read the wav file frames. + Will throw std::runtime_error in case of error. + */ + void AlsaPlaybackService::InitSndFile(const std::string &full_file_name) + { + snd_file_ = SndfileHandle(full_file_name); if(snd_file_.error() != 0) { std::stringstream errorDesc; - errorDesc << "The file '" << full_file_name_ << "' cannot be opened. error msg: '" << snd_file_.strError() << "'"; + errorDesc << "The file '" << full_file_name << "' cannot be opened. error msg: '" << snd_file_.strError() << "'"; throw std::runtime_error(errorDesc.str()); } @@ -367,14 +213,125 @@ namespace wavplayeralsa { "Sample type: '{}', " "Endian: '{}', " "Total frames in file: {} which are: {} ms, and {}:{} minutes", - full_file_name_, frame_rate_, num_of_channels_, major_type, minor_type, bytes_per_sample_, + full_file_name, frame_rate_, num_of_channels_, major_type, minor_type, bytes_per_sample_, SampleTypeToString(sample_type_), (is_endian_little_ ? "little" : "big"), total_frame_in_file_, number_of_ms, number_of_minutes, seconds_modulo ); + + } + + const char *AlsaPlaybackService::SampleTypeToString(SampleType sample_type) { + switch(sample_type) { + case SampleTypeSigned: return "signed integer"; + case SampleTypeUnsigned: return "unsigned integer"; + case SampleTypeFloat: return "float"; + } + std::stringstream err_desc; + err_desc << "sample type not supported. value is " << (int)sample_type; + throw std::runtime_error(err_desc.str()); + } + + /* + Init the alsa driver according to the params of the current wav file. + throw std::runtime_error in case of error + */ + void AlsaPlaybackService::InitAlsa(const std::string &audio_device) { + + int err; + std::stringstream err_desc; + + if( (err = snd_pcm_open(&alsa_playback_handle_, audio_device.c_str(), SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + err_desc << "cannot open audio device " << audio_device << " (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + // set hw parameters + + snd_pcm_hw_params_t *hw_params; + + if( (err = snd_pcm_hw_params_malloc(&hw_params)) < 0 ) { + err_desc << "cannot allocate hardware parameter structure (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + if( (err = snd_pcm_hw_params_any(alsa_playback_handle_, hw_params)) < 0) { + err_desc << "cannot initialize hardware parameter structure (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + if( (err = snd_pcm_hw_params_set_access(alsa_playback_handle_, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + err_desc << "cannot set access type (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + snd_pcm_format_t alsaFormat; + if(GetFormatForAlsa(alsaFormat) != true) { + err_desc << "the wav format is not supported by this player of alsa"; + throw std::runtime_error(err_desc.str()); + } + if( (err = snd_pcm_hw_params_set_format(alsa_playback_handle_, hw_params, alsaFormat)) < 0) { + err_desc << "cannot set sample format (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + if( (err = snd_pcm_hw_params_set_rate(alsa_playback_handle_, hw_params, frame_rate_, 0)) < 0) { + err_desc << "cannot set sample rate (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + if( (err = snd_pcm_hw_params_set_channels(alsa_playback_handle_, hw_params, num_of_channels_)) < 0) { + err_desc << "cannot set channel count (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + if( (err = snd_pcm_hw_params(alsa_playback_handle_, hw_params)) < 0) { + err_desc << "cannot set alsa hw parameters (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + snd_pcm_hw_params_free(hw_params); + hw_params = nullptr; + + + // set software parameters + + snd_pcm_sw_params_t *sw_params; + + if( (err = snd_pcm_sw_params_malloc(&sw_params)) < 0) { + err_desc << "cannot allocate software parameters structure (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + if( (err = snd_pcm_sw_params_current(alsa_playback_handle_, sw_params)) < 0) { + err_desc << "cannot initialize software parameters structure (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + // how many frames should be in the buffer before alsa start to play it. + // we set to 0 -> means start playing immediately + if( (err = snd_pcm_sw_params_set_start_threshold(alsa_playback_handle_, sw_params, 0U)) < 0) { + err_desc << "cannot set start mode (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + if( (err = snd_pcm_sw_params(alsa_playback_handle_, sw_params)) < 0) { + err_desc << "cannot set software parameters (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + snd_pcm_sw_params_free(sw_params); + sw_params = nullptr; + + if( (err = snd_pcm_prepare(alsa_playback_handle_)) < 0) { + err_desc << "cannot prepare audio interface for use (" << snd_strerror(err) << ")"; + throw std::runtime_error(err_desc.str()); + } + + initialized_ = true; } - bool AlsaFramesTransfer::GetFormatForAlsa(snd_pcm_format_t &out_format) const { + bool AlsaPlaybackService::GetFormatForAlsa(snd_pcm_format_t &out_format) const { switch(sample_type_) { case SampleTypeSigned: { @@ -438,122 +395,223 @@ namespace wavplayeralsa { return false; } + void AlsaPlaybackService::Play(int32_t offset_in_ms) { + + if(!initialized_) { + throw std::runtime_error("tried to play wav file on an uninitialzed alsa service"); + } - void AlsaFramesTransfer::InitAlsa() { + if(ios_.stopped()) { + throw std::runtime_error("this instance of alsa playback service has already played in the past. it cannot be reused. create a new instance to play again"); + } - int err; - std::stringstream err_desc; + double position_in_seconds = (double)offset_in_ms / 1000.0; + curr_position_frames_ = position_in_seconds * (double)frame_rate_; + if(curr_position_frames_ > total_frame_in_file_) { + curr_position_frames_ = total_frame_in_file_; + } + sf_count_t seek_res = snd_file_.seek(curr_position_frames_, SEEK_SET); - // set hw parameters + logger_->info("start playing file {} from position {} mili-seconds ({} seconds)", file_id_, offset_in_ms, position_in_seconds); + playing_thread_ = std::thread(&AlsaPlaybackService::PlayingThreadMain, this); + } - snd_pcm_hw_params_t *hw_params; + bool AlsaPlaybackService::Stop() { - if( (err = snd_pcm_hw_params_malloc(&hw_params)) < 0 ) { - err_desc << "cannot allocate hardware parameter structure (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); - } + bool was_playing = playing_thread_.joinable() && !ios_.stopped(); - if( (err = snd_pcm_hw_params_any(alsa_playback_handle_, hw_params)) < 0) { - err_desc << "cannot initialize hardware parameter structure (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); + alsa_wait_timer_.cancel(); + ios_.stop(); + if(playing_thread_.joinable()) { + playing_thread_.join(); } + return was_playing; + } - if( (err = snd_pcm_hw_params_set_access(alsa_playback_handle_, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { - err_desc << "cannot set access type (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); + void AlsaPlaybackService::PlayingThreadMain() { + + try { + ios_.post(std::bind(&AlsaPlaybackService::FramesToPcmTransferLoop, this, boost::system::error_code())); + ios_.run(); + PcmDrop(); } + catch(const std::runtime_error &e) { + logger_->error("play_seq_id: {}. error while playing current wav file. stopped transfering frames to alsa. exception is: {}", play_seq_id_, e.what()); + } + logger_->info("play_seq_id: {}. handling done", play_seq_id_); + player_events_callback_->NoSongPlayingStatus(file_id_, play_seq_id_); + ios_.stop(); + } - snd_pcm_format_t alsaFormat; - if(GetFormatForAlsa(alsaFormat) != true) { - err_desc << "the wav format is not supported by this player of alsa"; - throw std::runtime_error(err_desc.str()); + void AlsaPlaybackService::FramesToPcmTransferLoop(boost::system::error_code error_code) { + + // the function might be called from timer, in which case error_code might + // indicate the timer canceled and we should not invoke the function. + if(error_code) + return; + + std::stringstream err_desc; + int err; + + // calculate how many frames to write + snd_pcm_sframes_t frames_to_deliver; + if( (frames_to_deliver = snd_pcm_avail_update(alsa_playback_handle_)) < 0) { + if(frames_to_deliver == -EPIPE) { + throw std::runtime_error("an xrun occured"); + } + else { + err_desc << "unknown ALSA avail update return value (" << frames_to_deliver << ")"; + throw std::runtime_error(err_desc.str()); + } } - if( (err = snd_pcm_hw_params_set_format(alsa_playback_handle_, hw_params, alsaFormat)) < 0) { - err_desc << "cannot set sample format (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); + else if(frames_to_deliver == 0) { + alsa_wait_timer_.expires_from_now(boost::posix_time::millisec(5)); + alsa_wait_timer_.async_wait(std::bind(&AlsaPlaybackService::FramesToPcmTransferLoop, this, std::placeholders::_1)); + return; } - if( (err = snd_pcm_hw_params_set_rate(alsa_playback_handle_, hw_params, frame_rate_, 0)) < 0) { - err_desc << "cannot set sample rate (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); + // we want to deliver as many frames as possible. + // we can put frames_to_deliver number of frames, but the buffer can only hold frames_capacity_in_buffer_ frames + frames_to_deliver = std::min(frames_to_deliver, frames_capacity_in_buffer_); + unsigned int bytes_to_deliver = frames_to_deliver * bytes_per_frame_; + + // read the frames from the file. TODO: what if readRaw fails? + char buffer_for_transfer[TRANSFER_BUFFER_SIZE]; + bytes_to_deliver = snd_file_.readRaw(buffer_for_transfer, bytes_to_deliver); + if(bytes_to_deliver < 0) { + err_desc << "Failed reading raw frames from snd file. returned: " << sf_error_number(bytes_to_deliver); + throw std::runtime_error(err_desc.str()); + } + if(bytes_to_deliver == 0) { + logger_->info("play_seq_id: {}. done writing all frames to pcm. waiting for audio device to play remaining frames in the buffer", play_seq_id_); + ios_.post(std::bind(&AlsaPlaybackService::PcmDrainLoop, this, boost::system::error_code())); + return; } - if( (err = snd_pcm_hw_params_set_channels(alsa_playback_handle_, hw_params, num_of_channels_)) < 0) { - err_desc << "cannot set channel count (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); + int frames_written = snd_pcm_writei(alsa_playback_handle_, buffer_for_transfer, frames_to_deliver); + if( frames_written < 0) { + err_desc << "snd_pcm_writei failed (" << snd_strerror(frames_written) << ")"; + throw std::runtime_error(err_desc.str()); } - if( (err = snd_pcm_hw_params(alsa_playback_handle_, hw_params)) < 0) { - err_desc << "cannot set alsa hw parameters (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); + curr_position_frames_ += frames_written; + if(frames_written != frames_to_deliver) { + logger_->warn("play_seq_id: {}. transfered to alsa less frame then requested. frames_to_deliver: {}, frames_written: {}", play_seq_id_, frames_to_deliver, frames_written); + snd_file_.seek(curr_position_frames_, SEEK_SET); } - snd_pcm_hw_params_free(hw_params); - hw_params = nullptr; + CheckSongStartTime(); + ios_.post(std::bind(&AlsaPlaybackService::FramesToPcmTransferLoop, this, boost::system::error_code())); + } - // set software parameters + void AlsaPlaybackService::PcmDrainLoop(boost::system::error_code error_code) { - snd_pcm_sw_params_t *sw_params; + if(error_code) + return; - if( (err = snd_pcm_sw_params_malloc(&sw_params)) < 0) { - err_desc << "cannot allocate software parameters structure (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); - } + bool is_currently_playing = IsAlsaStatePlaying(); - if( (err = snd_pcm_sw_params_current(alsa_playback_handle_, sw_params)) < 0) { - err_desc << "cannot initialize software parameters structure (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); + if(!is_currently_playing) { + logger_->info("play_seq_id: {}. playing audio file ended successfully (transfered all frames to pcm and it is empty).", play_seq_id_); + return; } - // we transfer frames to alsa buffers, and then call snd_pcm_wait and block until buffer - // has more space for next frames. - // this parameter is the amount of min availible frames in the buffer that should trigger - // alsa to notify us for writing more frames. - if( (err = snd_pcm_sw_params_set_avail_min(alsa_playback_handle_, sw_params, AVAIL_MIN)) < 0) { - err_desc << "cannot set minimum available count (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); - } + CheckSongStartTime(); - // how many frames should be in the buffer before alsa start to play it. - // we set to 0 -> means start playing immediately - if( (err = snd_pcm_sw_params_set_start_threshold(alsa_playback_handle_, sw_params, 0U)) < 0) { - err_desc << "cannot set start mode (" << snd_strerror(err) << ")"; - throw std::runtime_error(err_desc.str()); - } + alsa_wait_timer_.expires_from_now(boost::posix_time::millisec(5)); + alsa_wait_timer_.async_wait(std::bind(&AlsaPlaybackService::PcmDrainLoop, this, std::placeholders::_1)); + } - if( (err = snd_pcm_sw_params(alsa_playback_handle_, sw_params)) < 0) { - err_desc << "cannot set software parameters (" << snd_strerror(err) << ")"; + void AlsaPlaybackService::PcmDrop() + { + int err; + if( (err = snd_pcm_drop(alsa_playback_handle_)) < 0 ) { + std::stringstream err_desc; + err_desc << "snd_pcm_drop failed (" << snd_strerror(err) << ")"; throw std::runtime_error(err_desc.str()); - } + } + } - snd_pcm_sw_params_free(sw_params); - sw_params = nullptr; + void AlsaPlaybackService::CheckSongStartTime() { + int err; + snd_pcm_sframes_t delay = 0; + int64_t pos_in_frames = 0; - if( (err = snd_pcm_prepare(alsa_playback_handle_)) < 0) { - err_desc << "cannot prepare audio interface for use (" << snd_strerror(err) << ")"; + if( (err = snd_pcm_delay(alsa_playback_handle_, &delay)) < 0) { + std::stringstream err_desc; + err_desc << "cannot query current offset in buffer (" << snd_strerror(err) << ")"; throw std::runtime_error(err_desc.str()); - } - - } + } - const char *AlsaFramesTransfer::SampleTypeToString(SampleType sample_type) { - switch(sample_type) { - case SampleTypeSigned: return "signed integer"; - case SampleTypeUnsigned: return "unsigned integer"; - case SampleTypeFloat: return "float"; + // this is a magic number test to remove end of file wrong reporting + if(delay < 4096) { + return; } - std::stringstream err_desc; - err_desc << "sample type not supported. value is " << (int)sample_type; - throw std::runtime_error(err_desc.str()); - } - - -} + pos_in_frames = curr_position_frames_ - delay; + int64_t ms_since_audio_file_start = ((pos_in_frames * (int64_t)1000) / (int64_t)frame_rate_); + struct timeval tv; + gettimeofday(&tv, NULL); + // convert sec to ms and usec to ms + uint64_t curr_time_ms_since_epoch = (uint64_t)(tv.tv_sec) * 1000 + (uint64_t)(tv.tv_usec) / 1000; + uint64_t audio_file_start_time_ms_since_epoch = (int64_t)curr_time_ms_since_epoch - ms_since_audio_file_start; + int64_t diff_from_prev = audio_file_start_time_ms_since_epoch - audio_start_time_ms_since_epoch_; + // there might be small jittering, we don't want to update the value often. + if(diff_from_prev <= 1 && diff_from_prev >= -1) + return; + player_events_callback_->NewSongStatus(file_id_, play_seq_id_, audio_file_start_time_ms_since_epoch, 1.0); + std::stringstream msg_stream; + msg_stream << "play_seq_id: " << play_seq_id_ << ". "; + msg_stream << "calculated a new audio file start time: " << audio_file_start_time_ms_since_epoch << " (ms since epoch). "; + if(audio_start_time_ms_since_epoch_ > 0) { + msg_stream << "this is a change since last calculation of " << diff_from_prev << " ms. "; + } + msg_stream << "pcm delay in frames as reported by alsa: " << delay << " and position in file is " << + ms_since_audio_file_start << " ms. "; + logger_->info(msg_stream.str()); + audio_start_time_ms_since_epoch_ = audio_file_start_time_ms_since_epoch; + } + bool AlsaPlaybackService::IsAlsaStatePlaying() + { + int status = snd_pcm_state(alsa_playback_handle_); + // the code had SND_PCM_STATE_PREPARED as well. + // it is removed, to resolve issue of song start playing after end of file. + // the drain function would not finish since the status is 'SND_PCM_STATE_PREPARED' + // because no frames were sent to alsa. + return (status == SND_PCM_STATE_RUNNING); // || (status == SND_PCM_STATE_PREPARED); + } + void AlsaPlaybackServiceFactory::Initialize( + std::shared_ptr logger, + PlayerEventsIfc *player_events_callback, + const std::string &audio_device + ) + { + logger_ = logger; + player_events_callback_ = player_events_callback; + audio_device_ = audio_device; + } + + IAlsaPlaybackService* AlsaPlaybackServiceFactory::CreateAlsaPlaybackService( + const std::string &full_file_name, + const std::string &file_id, + uint32_t play_seq_id + ) + { + return new AlsaPlaybackService( + logger_->clone("alsa_playback_service"), // TODO - use file name or id + player_events_callback_, + full_file_name, + file_id, + audio_device_, + play_seq_id + ); + } + +} \ No newline at end of file diff --git a/src/services/alsa_service.h b/src/services/alsa_service.h new file mode 100644 index 0000000..b2b4297 --- /dev/null +++ b/src/services/alsa_service.h @@ -0,0 +1,55 @@ +#ifndef WAVPLAYERALSA_ALSA_SERVICE_H__ +#define WAVPLAYERALSA_ALSA_SERVICE_H__ + +#include "spdlog/spdlog.h" + +#include "player_events_ifc.h" + +namespace wavplayeralsa +{ + + class IAlsaPlaybackService + { + + public: + virtual ~IAlsaPlaybackService() { } + + public: + virtual const std::string GetFileId() const = 0; + virtual void Play(int32_t offset_in_ms) = 0; + virtual bool Stop() = 0; + + }; + + class AlsaPlaybackServiceFactory + { + + public: + void Initialize( + std::shared_ptr logger, + PlayerEventsIfc *player_events_callback, + const std::string &audio_device + ); + + public: + + IAlsaPlaybackService *CreateAlsaPlaybackService( + const std::string &full_file_name, + const std::string &file_id, + uint32_t play_seq_id + ); + + + private: + std::shared_ptr logger_; + + private: + PlayerEventsIfc *player_events_callback_; + std::string audio_device_; + + }; + +} + + +#endif // WAVPLAYERALSA_ALSA_SERVICE_H__ \ No newline at end of file diff --git a/src/wavplayeralsa.cpp b/src/wavplayeralsa.cpp index 02ff342..4eb092b 100644 --- a/src/wavplayeralsa.cpp +++ b/src/wavplayeralsa.cpp @@ -11,15 +11,16 @@ #include "cxxopts/cxxopts.hpp" #include "spdlog/spdlog.h" +#include "spdlog/async.h" #include "spdlog/sinks/basic_file_sink.h" #include "spdlog/sinks/stdout_color_sinks.h" #include "web_sockets_api.h" #include "http_api.h" #include "mqtt_api.h" -#include "alsa_frames_transfer.h" #include "audio_files_manager.h" #include "current_song_controller.h" +#include "services/alsa_service.h" /* @@ -33,7 +34,11 @@ class WavPlayerAlsa { web_sockets_api_(), io_service_work_(io_service_), mqtt_api_(io_service_), - current_song_controller_(io_service_, &mqtt_api_, &web_sockets_api_, &alsa_frames_transfer_) + current_song_controller_( + io_service_, + &mqtt_api_, + &web_sockets_api_, + &alsa_playback_service_factory_) { } @@ -96,12 +101,16 @@ class WavPlayerAlsa { } // create an initialize all the required loggers. - // we do not want to work without loggers. + // we do not want to run without loggers. // if the function is unable to create a logger, it will print to stderr, and terminate the application void CreateLoggers(const char *command_name) { try { + // default thread pool settings can be modified *before* creating the async logger: + // spdlog::init_thread_pool(8192, 1); // queue with 8k items and 1 backing thread. + spdlog::init_thread_pool(8192, 1); + // create two logger sinks std::vector sinks; sinks.push_back(createLoggerConsole()); @@ -122,7 +131,7 @@ class WavPlayerAlsa { http_api_logger_ = root_logger_->clone("http_api"); ws_api_logger_ = root_logger_->clone("ws_api"); mqtt_api_logger_ = root_logger_->clone("mqtt_api"); - alsa_frames_transfer_logger_ = root_logger_->clone("alsa_frames_transfer"); + alsa_playback_service_factory_logger = root_logger_->clone("alsa_playback_service_factory"); } catch(const std::exception &e) { std::cerr << "Unable to create loggers. error is: " << e.what() << std::endl; @@ -142,12 +151,21 @@ class WavPlayerAlsa { // can throw exception void InitializeComponents() { try { - current_song_controller_.Initialize(uuid_, wav_dir_); - alsa_frames_transfer_.Initialize(alsa_frames_transfer_logger_, ¤t_song_controller_, audio_device_); audio_files_manager.Initialize(wav_dir_); web_sockets_api_.Initialize(ws_api_logger_, &io_service_, ws_listen_port_); http_api_.Initialize(http_api_logger_, uuid_, &io_service_, ¤t_song_controller_, &audio_files_manager, http_listen_port_); + // controllers + current_song_controller_.Initialize(uuid_, wav_dir_); + + // services + + alsa_playback_service_factory_.Initialize( + alsa_playback_service_factory_logger, + ¤t_song_controller_, + audio_device_ + ); + if(UseMqtt()) { mqtt_api_.Initialize(mqtt_api_logger_, mqtt_host_, mqtt_port_); } @@ -245,7 +263,7 @@ class WavPlayerAlsa { std::shared_ptr http_api_logger_; std::shared_ptr mqtt_api_logger_; std::shared_ptr ws_api_logger_; - std::shared_ptr alsa_frames_transfer_logger_; + std::shared_ptr alsa_playback_service_factory_logger; private: std::string uuid_; @@ -256,7 +274,7 @@ class WavPlayerAlsa { wavplayeralsa::HttpApi http_api_; wavplayeralsa::MqttApi mqtt_api_; wavplayeralsa::AudioFilesManager audio_files_manager; - wavplayeralsa::AlsaFramesTransfer alsa_frames_transfer_; + wavplayeralsa::AlsaPlaybackServiceFactory alsa_playback_service_factory_; wavplayeralsa::CurrentSongController current_song_controller_;