diff --git a/modules/renderer/audio/sounds.ts b/modules/renderer/audio/sounds.ts index 0d2b3e0..994a7f8 100644 --- a/modules/renderer/audio/sounds.ts +++ b/modules/renderer/audio/sounds.ts @@ -1,31 +1,31 @@ -window["require_setup"](module); -// -/// - -import {audio as naudio} from "teaclient_connection"; -import * as paths from "path"; - -namespace audio.sounds { - export async function play_sound(file: sound.SoundFile) : Promise { - await new Promise((resolve, reject) => { - let pathname = paths.dirname(location.pathname); - if(pathname[0] === '/' && pathname[2] === ':') //e.g.: /C:/test... - pathname = pathname.substr(1); - const path = paths.join(pathname, file.path); - - console.log(path); - naudio.sounds.playback_sound({ - callback: (result, message) => { - if(result == naudio.sounds.PlaybackResult.SUCCEEDED) - resolve(); - else - reject(naudio.sounds.PlaybackResult[result].toLowerCase() + ": " + message); - }, - file: path, - volume: file.volume - }); - }); - } -} - +window["require_setup"](module); +// +/// + +import {audio as naudio} from "teaclient_connection"; +import * as paths from "path"; + +namespace audio.sounds { + export async function play_sound(file: sound.SoundFile) : Promise { + await new Promise((resolve, reject) => { + let pathname = paths.dirname(location.pathname); + if(pathname[0] === '/' && pathname[2] === ':') //e.g.: /C:/test... + pathname = pathname.substr(1); + const path = paths.join(pathname, file.path); + + console.log(path); + naudio.sounds.playback_sound({ + callback: (result, message) => { + if(result == naudio.sounds.PlaybackResult.SUCCEEDED) + resolve(); + else + reject(naudio.sounds.PlaybackResult[result].toLowerCase() + ": " + message); + }, + file: path, + volume: file.volume + }); + }); + } +} + Object.assign(window["audio"] || (window["audio"] = {} as any), audio); \ No newline at end of file diff --git a/native/serverconnection/src/audio/file/wav.cpp b/native/serverconnection/src/audio/file/wav.cpp index 4c5b5db..4db294c 100644 --- a/native/serverconnection/src/audio/file/wav.cpp +++ b/native/serverconnection/src/audio/file/wav.cpp @@ -1,180 +1,180 @@ -// -// Created by WolverinDEV on 18/03/2020. -// - -#include "./wav.h" -#include "../../logger.h" - -using namespace tc::audio::file; - -struct WAVFileHeader { - /* RIFF Chunk Descriptor */ - uint8_t RIFF[4]; // RIFF Header Magic header - uint32_t ChunkSize; // RIFF Chunk Size - uint8_t WAVE[4]; // WAVE Header - - /* "fmt" sub-chunk */ - uint8_t fmt[4]; // FMT header - uint32_t Subchunk1Size; // Size of the fmt chunk - uint16_t AudioFormat; // Audio format 1=PCM,6=mulaw,7=alaw, 257=IBM Mu-Law, 258=IBM A-Law, 259=ADPCM - uint16_t NumOfChan; // Number of channels 1=Mono 2=Sterio - uint32_t SamplesPerSec; // Sampling Frequency in Hz - uint32_t bytesPerSec; // bytes per second - uint16_t blockAlign; // 2=16-bit mono, 4=16-bit stereo - uint16_t bitsPerSample; // Number of bits per sample -}; -static_assert(sizeof(WAVFileHeader) == 0x24); - -struct WAFFileChunk { - uint8_t id[4]; - uint32_t size; -}; -static_assert(sizeof(WAFFileChunk) == 0x08); - -WAVReader::WAVReader(std::string file) : file_path_{std::move(file)} {} -WAVReader::~WAVReader() { - this->close_file(); -} - -FileOpenResult WAVReader::open_file(std::string& error) { - this->is_.open(this->file_path_, std::ifstream::in | std::ifstream::binary); - if(!this->is_) { - error = tr("failed to open file"); - return FileOpenResult::OPEN_RESULT_ERROR; - } - - WAVFileHeader header{}; - if(!this->is_.read((char*) &header, sizeof(header))) { - error = tr("failed to read wav header"); - return FileOpenResult::OPEN_RESULT_ERROR; - } - - if(memcmp(header.RIFF, "RIFF", 4) != 0) { - error = tr("invalid RIFF header"); - return FileOpenResult::OPEN_RESULT_ERROR; - } - - if(memcmp(header.WAVE, "WAVE", 4) != 0) { - error = tr("invalid WAVE header"); - return FileOpenResult::OPEN_RESULT_ERROR; - } - - if(memcmp(header.fmt, "fmt ", 4) != 0) { - error = tr("invalid WAVE header"); - return FileOpenResult::OPEN_RESULT_ERROR; - } - - if(header.AudioFormat != 1) { - error = tr("Only PCM has been supported. WAV file does not contains PCM data."); - return FileOpenResult::OPEN_RESULT_FORMAT_UNSUPPORTED; - } - - if(header.bytesPerSec != (header.NumOfChan * header.SamplesPerSec * header.bitsPerSample) / 8) { - error = tr("inconsistent WAV header"); - return FileOpenResult::OPEN_RESULT_INVALID_FORMAT; - } - - if(header.bitsPerSample != 8 && header.bitsPerSample != 16 && header.bitsPerSample != 24) { - error = tr("unsupported bitrate"); - return FileOpenResult::OPEN_RESULT_FORMAT_UNSUPPORTED; - } - - if(header.NumOfChan != 2 && header.NumOfChan != 1) { - error = tr("unsupported channel count"); - return FileOpenResult::OPEN_RESULT_FORMAT_UNSUPPORTED; - } - - WAFFileChunk chunk{}; - while(true) { - if(!this->is_.read((char*) &chunk, sizeof(chunk))) { - error = tr("failed to read chunks until data chunk"); - return FileOpenResult::OPEN_RESULT_ERROR; - } - - if(memcmp(chunk.id, "data ", 4) == 0) - break; - - this->is_.seekg(chunk.size, std::ifstream::cur); - } - - this->current_sample_offset_ = 0; - this->bytes_per_sample = header.bitsPerSample / 8; - this->total_samples_ = chunk.size / this->bytes_per_sample / header.NumOfChan; - this->sample_rate_ = header.SamplesPerSec; - this->channels_ = header.NumOfChan; - return FileOpenResult::OPEN_RESULT_SUCCESS; -} - -void WAVReader::close_file() { - this->total_samples_ = 0; - this->bytes_per_sample = 0; - this->sample_rate_ = 0; - this->channels_ = 0; - this->is_.close(); -} - -float _8bit_float_convert(const uint8_t* buffer) { - int16_t value = buffer[0] & 0xFFU; - return (float) (value - 127) * (1.0f / 127.0f); -} - -float _16bit_float_convert(const uint8_t* buffer) { - int16_t value = *reinterpret_cast(buffer); - return (float) value / 32767.f; -} - -float _24bit_float_convert(const uint8_t* buffer) { -#if 0 - int32_t value = (*reinterpret_cast(buffer) & 0xFFFFFFU) << 8; - return (float) (value - 1073741824) / 1073741824.f; //2147483648 / 2 -#endif -#if 1 - int32_t value = ((uint32_t) buffer[2] << 16U) | ((uint32_t) buffer[1] << 8U) | ((uint32_t) buffer[0] << 0U); - if (value & 0x800000) // if the 24th bit is set, this is a negative number in 24-bit world - value = value | ~0xFFFFFF; // so make sure sign is extended to the 32 bit float - auto result = (float) value / (float) 8388608.f; //8388608 - return result; -#endif -} - -static std::array pcm_to_float_converters{ - _8bit_float_convert, - _16bit_float_convert, - _24bit_float_convert -}; - -ReadResult WAVReader::read(void *buffer, size_t* samples) { - auto fbuffer = (float*) buffer; - - const auto max_sample_count = this->total_samples_ - this->current_sample_offset_; - const auto max_samples = std::min(*samples, max_sample_count); - if(max_samples == 0) { - if(max_sample_count == 0) return ReadResult::READ_RESULT_EOF; - - return ReadResult::READ_RESULT_SUCCESS; - } - - constexpr size_t sbuffer_size{1536}; /* must be dividable by 24, 165 and 8 bit! As well by two channels so 6, 4 and 2 byte to avoid to mess up one frame */ - uint8_t sbuffer[sbuffer_size]; - - size_t samples_read{0}; - auto fconverter = pcm_to_float_converters[(this->bytes_per_sample - 1) & 0x3U]; - while(samples_read < max_samples) { - const auto block_byte_length{std::min(sbuffer_size, (max_samples - samples_read) * this->bytes_per_sample * this->channels_)}; - if(!this->is_.read((char*) sbuffer, block_byte_length)) - return ReadResult::READ_RESULT_UNRECOVERABLE_ERROR; - - uint8_t* sbufferptr = sbuffer; - uint8_t* sbuferendptr = sbuffer + block_byte_length; - while(sbufferptr != sbuferendptr) { - *fbuffer++ = fconverter(sbufferptr); - sbufferptr += this->bytes_per_sample; - } - - samples_read += block_byte_length / (this->bytes_per_sample * this->channels_); - } - - *samples = samples_read; - this->current_sample_offset_ += samples_read; - return ReadResult::READ_RESULT_SUCCESS; +// +// Created by WolverinDEV on 18/03/2020. +// + +#include "./wav.h" +#include "../../logger.h" + +using namespace tc::audio::file; + +struct WAVFileHeader { + /* RIFF Chunk Descriptor */ + uint8_t RIFF[4]; // RIFF Header Magic header + uint32_t ChunkSize; // RIFF Chunk Size + uint8_t WAVE[4]; // WAVE Header + + /* "fmt" sub-chunk */ + uint8_t fmt[4]; // FMT header + uint32_t Subchunk1Size; // Size of the fmt chunk + uint16_t AudioFormat; // Audio format 1=PCM,6=mulaw,7=alaw, 257=IBM Mu-Law, 258=IBM A-Law, 259=ADPCM + uint16_t NumOfChan; // Number of channels 1=Mono 2=Sterio + uint32_t SamplesPerSec; // Sampling Frequency in Hz + uint32_t bytesPerSec; // bytes per second + uint16_t blockAlign; // 2=16-bit mono, 4=16-bit stereo + uint16_t bitsPerSample; // Number of bits per sample +}; +static_assert(sizeof(WAVFileHeader) == 0x24); + +struct WAFFileChunk { + uint8_t id[4]; + uint32_t size; +}; +static_assert(sizeof(WAFFileChunk) == 0x08); + +WAVReader::WAVReader(std::string file) : file_path_{std::move(file)} {} +WAVReader::~WAVReader() { + this->close_file(); +} + +FileOpenResult WAVReader::open_file(std::string& error) { + this->is_.open(this->file_path_, std::ifstream::in | std::ifstream::binary); + if(!this->is_) { + error = tr("failed to open file"); + return FileOpenResult::OPEN_RESULT_ERROR; + } + + WAVFileHeader header{}; + if(!this->is_.read((char*) &header, sizeof(header))) { + error = tr("failed to read wav header"); + return FileOpenResult::OPEN_RESULT_ERROR; + } + + if(memcmp(header.RIFF, "RIFF", 4) != 0) { + error = tr("invalid RIFF header"); + return FileOpenResult::OPEN_RESULT_ERROR; + } + + if(memcmp(header.WAVE, "WAVE", 4) != 0) { + error = tr("invalid WAVE header"); + return FileOpenResult::OPEN_RESULT_ERROR; + } + + if(memcmp(header.fmt, "fmt ", 4) != 0) { + error = tr("invalid WAVE header"); + return FileOpenResult::OPEN_RESULT_ERROR; + } + + if(header.AudioFormat != 1) { + error = tr("Only PCM has been supported. WAV file does not contains PCM data."); + return FileOpenResult::OPEN_RESULT_FORMAT_UNSUPPORTED; + } + + if(header.bytesPerSec != (header.NumOfChan * header.SamplesPerSec * header.bitsPerSample) / 8) { + error = tr("inconsistent WAV header"); + return FileOpenResult::OPEN_RESULT_INVALID_FORMAT; + } + + if(header.bitsPerSample != 8 && header.bitsPerSample != 16 && header.bitsPerSample != 24) { + error = tr("unsupported bitrate"); + return FileOpenResult::OPEN_RESULT_FORMAT_UNSUPPORTED; + } + + if(header.NumOfChan != 2 && header.NumOfChan != 1) { + error = tr("unsupported channel count"); + return FileOpenResult::OPEN_RESULT_FORMAT_UNSUPPORTED; + } + + WAFFileChunk chunk{}; + while(true) { + if(!this->is_.read((char*) &chunk, sizeof(chunk))) { + error = tr("failed to read chunks until data chunk"); + return FileOpenResult::OPEN_RESULT_ERROR; + } + + if(memcmp(chunk.id, "data ", 4) == 0) + break; + + this->is_.seekg(chunk.size, std::ifstream::cur); + } + + this->current_sample_offset_ = 0; + this->bytes_per_sample = header.bitsPerSample / 8; + this->total_samples_ = chunk.size / this->bytes_per_sample / header.NumOfChan; + this->sample_rate_ = header.SamplesPerSec; + this->channels_ = header.NumOfChan; + return FileOpenResult::OPEN_RESULT_SUCCESS; +} + +void WAVReader::close_file() { + this->total_samples_ = 0; + this->bytes_per_sample = 0; + this->sample_rate_ = 0; + this->channels_ = 0; + this->is_.close(); +} + +float _8bit_float_convert(const uint8_t* buffer) { + int16_t value = buffer[0] & 0xFFU; + return (float) (value - 127) * (1.0f / 127.0f); +} + +float _16bit_float_convert(const uint8_t* buffer) { + int16_t value = *reinterpret_cast(buffer); + return (float) value / 32767.f; +} + +float _24bit_float_convert(const uint8_t* buffer) { +#if 0 + int32_t value = (*reinterpret_cast(buffer) & 0xFFFFFFU) << 8; + return (float) (value - 1073741824) / 1073741824.f; //2147483648 / 2 +#endif +#if 1 + int32_t value = ((uint32_t) buffer[2] << 16U) | ((uint32_t) buffer[1] << 8U) | ((uint32_t) buffer[0] << 0U); + if (value & 0x800000) // if the 24th bit is set, this is a negative number in 24-bit world + value = value | ~0xFFFFFF; // so make sure sign is extended to the 32 bit float + auto result = (float) value / (float) 8388608.f; //8388608 + return result; +#endif +} + +static std::array pcm_to_float_converters{ + _8bit_float_convert, + _16bit_float_convert, + _24bit_float_convert +}; + +ReadResult WAVReader::read(void *buffer, size_t* samples) { + auto fbuffer = (float*) buffer; + + const auto max_sample_count = this->total_samples_ - this->current_sample_offset_; + const auto max_samples = std::min(*samples, max_sample_count); + if(max_samples == 0) { + if(max_sample_count == 0) return ReadResult::READ_RESULT_EOF; + + return ReadResult::READ_RESULT_SUCCESS; + } + + constexpr size_t sbuffer_size{1536}; /* must be dividable by 24, 165 and 8 bit! As well by two channels so 6, 4 and 2 byte to avoid to mess up one frame */ + uint8_t sbuffer[sbuffer_size]; + + size_t samples_read{0}; + auto fconverter = pcm_to_float_converters[(this->bytes_per_sample - 1) & 0x3U]; + while(samples_read < max_samples) { + const auto block_byte_length{std::min(sbuffer_size, (max_samples - samples_read) * this->bytes_per_sample * this->channels_)}; + if(!this->is_.read((char*) sbuffer, block_byte_length)) + return ReadResult::READ_RESULT_UNRECOVERABLE_ERROR; + + uint8_t* sbufferptr = sbuffer; + uint8_t* sbuferendptr = sbuffer + block_byte_length; + while(sbufferptr != sbuferendptr) { + *fbuffer++ = fconverter(sbufferptr); + sbufferptr += this->bytes_per_sample; + } + + samples_read += block_byte_length / (this->bytes_per_sample * this->channels_); + } + + *samples = samples_read; + this->current_sample_offset_ += samples_read; + return ReadResult::READ_RESULT_SUCCESS; } \ No newline at end of file diff --git a/native/serverconnection/src/audio/file/wav.h b/native/serverconnection/src/audio/file/wav.h index 88543d6..2fbdac7 100644 --- a/native/serverconnection/src/audio/file/wav.h +++ b/native/serverconnection/src/audio/file/wav.h @@ -1,51 +1,51 @@ -#pragma once - -#include -#include - -namespace tc::audio::file { - enum FileOpenResult { - OPEN_RESULT_SUCCESS, - OPEN_RESULT_INVALID_FORMAT, - OPEN_RESULT_FORMAT_UNSUPPORTED, - OPEN_RESULT_ERROR - }; - - enum ReadResult { - READ_RESULT_SUCCESS, - READ_RESULT_EOF, - READ_RESULT_UNRECOVERABLE_ERROR - }; - - class WAVReader { - public: - explicit WAVReader(std::string /* path */); - ~WAVReader(); - - [[nodiscard]] inline const std::string& file_path() const { return this->file_path_; } - - [[nodiscard]] FileOpenResult open_file(std::string& /* error */); - void close_file(); - - [[nodiscard]] inline size_t channels() const { return this->channels_; } - [[nodiscard]] inline size_t sample_rate() const { return this->sample_rate_; } - [[nodiscard]] inline size_t total_samples() const { return this->total_samples_; } - [[nodiscard]] inline size_t current_sample_offset() const { return this->current_sample_offset_; } - - /** - * Audio data in interleaved floats reachting from [-1;1]. - * Must contains at least channels * sizeof(float) * sample_count bytes - */ - [[nodiscard]] ReadResult read(void* /* target buffer */, size_t* /* sample count */); - private: - const std::string file_path_; - - std::ifstream is_{}; - - size_t channels_{0}; - size_t sample_rate_{0}; - size_t total_samples_{0}; - size_t current_sample_offset_{0}; - uint8_t bytes_per_sample{0}; /* 1, 2 or 3 (8, 16, 24 bit) */ - }; +#pragma once + +#include +#include + +namespace tc::audio::file { + enum FileOpenResult { + OPEN_RESULT_SUCCESS, + OPEN_RESULT_INVALID_FORMAT, + OPEN_RESULT_FORMAT_UNSUPPORTED, + OPEN_RESULT_ERROR + }; + + enum ReadResult { + READ_RESULT_SUCCESS, + READ_RESULT_EOF, + READ_RESULT_UNRECOVERABLE_ERROR + }; + + class WAVReader { + public: + explicit WAVReader(std::string /* path */); + ~WAVReader(); + + [[nodiscard]] inline const std::string& file_path() const { return this->file_path_; } + + [[nodiscard]] FileOpenResult open_file(std::string& /* error */); + void close_file(); + + [[nodiscard]] inline size_t channels() const { return this->channels_; } + [[nodiscard]] inline size_t sample_rate() const { return this->sample_rate_; } + [[nodiscard]] inline size_t total_samples() const { return this->total_samples_; } + [[nodiscard]] inline size_t current_sample_offset() const { return this->current_sample_offset_; } + + /** + * Audio data in interleaved floats reachting from [-1;1]. + * Must contains at least channels * sizeof(float) * sample_count bytes + */ + [[nodiscard]] ReadResult read(void* /* target buffer */, size_t* /* sample count */); + private: + const std::string file_path_; + + std::ifstream is_{}; + + size_t channels_{0}; + size_t sample_rate_{0}; + size_t total_samples_{0}; + size_t current_sample_offset_{0}; + uint8_t bytes_per_sample{0}; /* 1, 2 or 3 (8, 16, 24 bit) */ + }; } \ No newline at end of file diff --git a/native/serverconnection/src/audio/sounds/SoundPlayer.cpp b/native/serverconnection/src/audio/sounds/SoundPlayer.cpp index b8f13ae..feb5a6b 100644 --- a/native/serverconnection/src/audio/sounds/SoundPlayer.cpp +++ b/native/serverconnection/src/audio/sounds/SoundPlayer.cpp @@ -1,309 +1,309 @@ -// -// 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" - -#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) { - if(this->output_source && global_audio_output) - global_audio_output->delete_source(this->output_source); - 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; - } - - 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->current_latency(); - } - - 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->max_buffered_samples = max_buffer; - this->output_source->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->current_latency(); - 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(); - } - }; - - 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 = 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(info[0].As()->Value()); +// +// 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" + +#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) { + if(this->output_source && global_audio_output) + global_audio_output->delete_source(this->output_source); + 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; + } + + 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->current_latency(); + } + + 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->max_buffered_samples = max_buffer; + this->output_source->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->current_latency(); + 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(); + } + }; + + 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 = 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(info[0].As()->Value()); } \ No newline at end of file diff --git a/native/serverconnection/src/audio/sounds/SoundPlayer.h b/native/serverconnection/src/audio/sounds/SoundPlayer.h index b01dbe6..6042bf0 100644 --- a/native/serverconnection/src/audio/sounds/SoundPlayer.h +++ b/native/serverconnection/src/audio/sounds/SoundPlayer.h @@ -1,31 +1,31 @@ -#pragma once - -#include -#include -#include - -namespace tc::audio::sounds { - typedef uintptr_t sound_playback_id; - - enum struct PlaybackResult { - SUCCEEDED, - CANCELED, - SOUND_NOT_INITIALIZED, - FILE_OPEN_ERROR, - PLAYBACK_ERROR /* has the extra error set */ - }; - - typedef std::function callback_status_t; - struct PlaybackSettings { - std::string file{}; - double volume{1.f}; - /* ATTENTION: This callback may be called within the audio loop! */ - callback_status_t callback{}; - }; - - extern sound_playback_id playback_sound(const PlaybackSettings& /* settings */); - extern void cancel_playback(const sound_playback_id&); - - extern NAN_METHOD(playback_sound_js); - extern NAN_METHOD(cancel_playback_js); +#pragma once + +#include +#include +#include + +namespace tc::audio::sounds { + typedef uintptr_t sound_playback_id; + + enum struct PlaybackResult { + SUCCEEDED, + CANCELED, + SOUND_NOT_INITIALIZED, + FILE_OPEN_ERROR, + PLAYBACK_ERROR /* has the extra error set */ + }; + + typedef std::function callback_status_t; + struct PlaybackSettings { + std::string file{}; + double volume{1.f}; + /* ATTENTION: This callback may be called within the audio loop! */ + callback_status_t callback{}; + }; + + extern sound_playback_id playback_sound(const PlaybackSettings& /* settings */); + extern void cancel_playback(const sound_playback_id&); + + extern NAN_METHOD(playback_sound_js); + extern NAN_METHOD(cancel_playback_js); } \ No newline at end of file