// // Created by WolverinDEV on 18/03/2020. // #include #include #include #include "./SoundPlayer.h" #include "../AudioOutput.h" #include "../file/wav.h" #include "../../EventLoop.h" #include "../../logger.h" #include "../AudioEventLoop.h" #include "../AudioResampler.h" #include "../AudioMerger.h" #include "../AudioGain.h" #ifdef max #undef max #endif using namespace tc::audio; extern tc::audio::AudioOutput* global_audio_output; namespace tc::audio::sounds { class FilePlayer : public event::EventEntry, public std::enable_shared_from_this { public: explicit FilePlayer(PlaybackSettings settings) : settings_{std::move(settings)} { log_trace(category::memory, tr("Allocated FilePlayer instance at {}"), (void*) this); } ~FilePlayer() { this->finalize(true); log_trace(category::memory, tr("Deleted FilePlayer instance at {}"), (void*) this); } [[nodiscard]] inline const PlaybackSettings& settings() const { return this->settings_; } [[nodiscard]] inline bool is_finished() const { return this->state_ == PLAYER_STATE_UNSET; } /* should not be blocking! */ bool play() { if(this->state_ != PLAYER_STATE_UNSET) return false; this->state_ = PLAYER_STATE_INITIALIZE; audio::decode_event_loop->schedule(this->shared_from_this()); return true; } /* should not be blocking! */ void cancel() { this->state_ = PLAYER_STATE_CANCELED; audio::decode_event_loop->schedule(this->shared_from_this()); } private: constexpr static auto kBufferChunkTimespan{0.2}; const PlaybackSettings settings_; std::unique_ptr file_handle{nullptr}; std::unique_ptr resampler{nullptr}; std::shared_ptr output_source; void* cache_buffer{nullptr}; enum { PLAYER_STATE_INITIALIZE, PLAYER_STATE_PLAYING, PLAYER_STATE_AWAIT_FINISH, PLAYER_STATE_FINISHED, PLAYER_STATE_CANCELED, PLAYER_STATE_UNSET } state_{PLAYER_STATE_UNSET}; void finalize(bool is_destructor_call) { this->output_source = nullptr; if(this->file_handle) this->file_handle = nullptr; if(auto buffer{std::exchange(this->cache_buffer, nullptr)}; buffer) ::free(buffer); if(!is_destructor_call) audio::decode_event_loop->cancel(this->shared_from_this()); this->state_ = PLAYER_STATE_UNSET; } void event_execute(const std::chrono::system_clock::time_point &) override { if(this->state_ == PLAYER_STATE_INITIALIZE) { this->file_handle = std::make_unique(this->settings_.file); std::string error{}; if(auto err{this->file_handle->open_file(error)}; err != file::OPEN_RESULT_SUCCESS) { if(auto callback{this->settings_.callback}; callback) { callback(PlaybackResult::FILE_OPEN_ERROR, error); } this->finalize(false); return; } if(!global_audio_output) { if(auto callback{this->settings_.callback}; callback) callback(PlaybackResult::SOUND_NOT_INITIALIZED, ""); this->finalize(false); return; } this->initialize_playback(); auto max_samples = (size_t) std::max(this->output_source->sample_rate, this->file_handle->sample_rate()) * kBufferChunkTimespan * 8 * std::max(this->file_handle->channels(), this->output_source->channel_count); this->cache_buffer = ::malloc((size_t) (max_samples * sizeof(float))); if(!this->cache_buffer) { if(auto callback{this->settings_.callback}; callback) callback(PlaybackResult::PLAYBACK_ERROR, "failed to allocate cached buffer"); this->finalize(false); return; } this->state_ = PLAYER_STATE_PLAYING; } if(this->state_ == PLAYER_STATE_PLAYING) { if(!this->could_enqueue_next_buffer()) return; auto samples_to_read = (size_t) (this->file_handle->sample_rate() * kBufferChunkTimespan); auto errc = this->file_handle->read(this->cache_buffer, &samples_to_read); switch (errc) { case file::READ_RESULT_SUCCESS: break; case file::READ_RESULT_EOF: this->state_ = PLAYER_STATE_AWAIT_FINISH; return; case file::READ_RESULT_UNRECOVERABLE_ERROR: if(auto callback{this->settings_.callback}; callback) callback(PlaybackResult::PLAYBACK_ERROR, "read resulted in an unrecoverable error"); this->finalize(false); return; } if(!merge::merge_channels_interleaved(this->cache_buffer, this->output_source->channel_count, this->cache_buffer, this->file_handle->channels(), samples_to_read)) { log_warn(category::audio, tr("failed to merge channels for replaying a sound")); return; } auto resampled_samples = this->resampler->process(this->cache_buffer, this->cache_buffer, samples_to_read); if(resampled_samples <= 0) { log_warn(category::audio, tr("failed to resample file audio buffer ({})"), resampled_samples); return; } audio::apply_gain(this->cache_buffer, this->output_source->channel_count, resampled_samples, this->settings_.volume); this->output_source->enqueue_samples(this->cache_buffer, resampled_samples); if(this->could_enqueue_next_buffer()) audio::decode_event_loop->schedule(this->shared_from_this()); } else if(this->state_ == PLAYER_STATE_FINISHED || this->state_ == PLAYER_STATE_CANCELED) { this->finalize(false); if(auto callback{this->settings_.callback}; callback) callback(this->state_ == PLAYER_STATE_CANCELED ? PlaybackResult::CANCELED : PlaybackResult::SUCCEEDED, ""); this->state_ = PLAYER_STATE_UNSET; return; } auto filled_samples = this->output_source->currently_buffered_samples(); } void initialize_playback() { assert(this->file_handle); assert(global_audio_output); const auto max_buffer = (size_t) ceil(global_audio_output->sample_rate() * kBufferChunkTimespan * 3); this->output_source = global_audio_output->create_source(max_buffer); this->output_source->overflow_strategy = audio::overflow_strategy::ignore; this->output_source->set_max_buffered_samples(max_buffer); this->output_source->set_min_buffered_samples((size_t) floor(this->output_source->sample_rate * 0.04)); auto weak_this = this->weak_from_this(); this->output_source->on_underflow = [weak_this](size_t sample_count){ auto self = weak_this.lock(); if(!self) { return false; } if(self->state_ == PLAYER_STATE_PLAYING) { log_warn(category::audio, tr("Having an audio underflow while playing a sound.")); } else if(self->state_ == PLAYER_STATE_AWAIT_FINISH) { self->state_ = PLAYER_STATE_FINISHED; } audio::decode_event_loop->schedule(self); return false; }; this->output_source->on_read = [weak_this] { auto self = weak_this.lock(); if(!self) { return; } if(self->could_enqueue_next_buffer() && self->state_ == PLAYER_STATE_PLAYING) { audio::decode_event_loop->schedule(self); } }; this->output_source->on_overflow = [weak_this](size_t count) { log_warn(category::audio, tr("Having an audio overflow while playing a sound.")); }; this->resampler = std::make_unique(this->file_handle->sample_rate(), this->output_source->sample_rate, this->output_source->channel_count); } [[nodiscard]] inline size_t cache_buffer_sample_size() const { return (size_t) (this->output_source->sample_rate * kBufferChunkTimespan); } [[nodiscard]] inline bool could_enqueue_next_buffer() const { if(!this->output_source) { return false; } const auto current_size = this->output_source->currently_buffered_samples(); const auto max_size = this->output_source->max_buffered_samples(); if(current_size > max_size) return false; const auto size_left = max_size - current_size; return size_left >= this->cache_buffer_sample_size() * 1.5; /* ensure we've a bit more space */ } }; std::mutex file_player_mutex{}; std::deque> file_players{}; sound_playback_id playback_sound(const PlaybackSettings& settings) { if(!audio::initialized()) { settings.callback(PlaybackResult::SOUND_NOT_INITIALIZED, ""); return 0; } std::unique_lock fplock{file_player_mutex}; file_players.erase(std::remove_if(file_players.begin(), file_players.end(), [](const auto& player) { return player->is_finished(); }), file_players.end()); auto player = std::make_shared(settings); file_players.push_back(player); if(!player->play()) { if(auto callback{settings.callback}; callback) { callback(PlaybackResult::PLAYBACK_ERROR, "failed to start playback."); } return 0; } fplock.unlock(); return (sound_playback_id) &*player; } void cancel_playback(const sound_playback_id& id) { std::unique_lock fplock{file_player_mutex}; auto player_it = std::find_if(file_players.begin(), file_players.end(), [&](const auto& player) { return (sound_playback_id) &*player == id; }); if(player_it == file_players.end()) { return; } auto player = *player_it; file_players.erase(player_it); fplock.unlock(); player->cancel(); } } NAN_METHOD(tc::audio::sounds::playback_sound_js) { if(info.Length() != 1 || !info[0]->IsObject()) { Nan::ThrowError("invalid arguments"); return; } auto data = info[0].As(); auto file = Nan::GetLocal(data, "file"); auto volume = Nan::GetLocal(data, "volume", Nan::New(1.f)); v8::Local callback = Nan::GetLocal(data, "callback"); if(file.IsEmpty() || !file->IsString()) { Nan::ThrowError("missing file path"); return; } if(volume.IsEmpty() || !volume->IsNumber()) { Nan::ThrowError("invalid volume"); return; } PlaybackSettings settings{}; settings.file = *Nan::Utf8String(file); settings.volume = (float) volume->Value(); if(!callback.IsEmpty()) { if(!callback->IsFunction()) { Nan::ThrowError("invalid callback function"); return; } Nan::Global cb{callback}; auto async_callback = Nan::async_callback([cb = std::move(cb)](PlaybackResult result, std::string error) mutable { Nan::HandleScope scope{}; auto callback = cb.Get(Nan::GetCurrentContext()->GetIsolate()).As(); cb.Reset(); v8::Local arguments[2]; arguments[0] = Nan::New((int) result); arguments[1] = Nan::LocalStringUTF8(error); (void) callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 2, arguments); }).option_destroyed_execute(true); settings.callback = [async_callback](PlaybackResult result, const std::string& error) mutable { async_callback.call_cpy(result, error); }; } info.GetReturnValue().Set((uint32_t) playback_sound(settings)); } NAN_METHOD(tc::audio::sounds::cancel_playback_js) { if(info.Length() != 1 || !info[0]->IsNumber()) { Nan::ThrowError("invalid arguments"); return; } cancel_playback((sound_playback_id) info[0].As()->Value()); }