Added native audio sounds
This commit is contained in:
parent
d918a822e2
commit
c3513df310
@ -1,31 +1,31 @@
|
||||
window["require_setup"](module);
|
||||
// <reference types="../imports/import_shared.d.ts" />
|
||||
/// <reference types="../../modules/renderer/imports/imports_shared.d.ts" />
|
||||
|
||||
import {audio as naudio} from "teaclient_connection";
|
||||
import * as paths from "path";
|
||||
|
||||
namespace audio.sounds {
|
||||
export async function play_sound(file: sound.SoundFile) : Promise<void> {
|
||||
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);
|
||||
// <reference types="../imports/import_shared.d.ts" />
|
||||
/// <reference types="../../modules/renderer/imports/imports_shared.d.ts" />
|
||||
|
||||
import {audio as naudio} from "teaclient_connection";
|
||||
import * as paths from "path";
|
||||
|
||||
namespace audio.sounds {
|
||||
export async function play_sound(file: sound.SoundFile) : Promise<void> {
|
||||
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);
|
@ -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<const int16_t*>(buffer);
|
||||
return (float) value / 32767.f;
|
||||
}
|
||||
|
||||
float _24bit_float_convert(const uint8_t* buffer) {
|
||||
#if 0
|
||||
int32_t value = (*reinterpret_cast<const uint32_t*>(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<float(*)(const uint8_t*), 3> 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<const int16_t*>(buffer);
|
||||
return (float) value / 32767.f;
|
||||
}
|
||||
|
||||
float _24bit_float_convert(const uint8_t* buffer) {
|
||||
#if 0
|
||||
int32_t value = (*reinterpret_cast<const uint32_t*>(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<float(*)(const uint8_t*), 3> 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;
|
||||
}
|
@ -1,51 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
|
||||
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 <string>
|
||||
#include <fstream>
|
||||
|
||||
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) */
|
||||
};
|
||||
}
|
@ -1,309 +1,309 @@
|
||||
//
|
||||
// Created by WolverinDEV on 18/03/2020.
|
||||
//
|
||||
|
||||
#include <cassert>
|
||||
#include <NanGet.h>
|
||||
#include <NanEventCallback.h>
|
||||
#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<FilePlayer> {
|
||||
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::WAVReader> file_handle{nullptr};
|
||||
std::unique_ptr<AudioResampler> resampler{nullptr};
|
||||
std::shared_ptr<audio::AudioOutputSource> 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<file::WAVReader>(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<AudioResampler>(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<std::shared_ptr<FilePlayer>> 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<FilePlayer>(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<v8::Object>();
|
||||
auto file = Nan::GetLocal<v8::String>(data, "file");
|
||||
auto volume = Nan::GetLocal<v8::Number>(data, "volume", Nan::New<v8::Number>(1.f));
|
||||
v8::Local<v8::Function> callback = Nan::GetLocal<v8::Function>(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<v8::Function> 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<v8::Function>();
|
||||
cb.Reset();
|
||||
|
||||
v8::Local<v8::Value> arguments[2];
|
||||
arguments[0] = Nan::New<v8::Number>((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<v8::Number>()->Value());
|
||||
//
|
||||
// Created by WolverinDEV on 18/03/2020.
|
||||
//
|
||||
|
||||
#include <cassert>
|
||||
#include <NanGet.h>
|
||||
#include <NanEventCallback.h>
|
||||
#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<FilePlayer> {
|
||||
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::WAVReader> file_handle{nullptr};
|
||||
std::unique_ptr<AudioResampler> resampler{nullptr};
|
||||
std::shared_ptr<audio::AudioOutputSource> 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<file::WAVReader>(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<AudioResampler>(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<std::shared_ptr<FilePlayer>> 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<FilePlayer>(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<v8::Object>();
|
||||
auto file = Nan::GetLocal<v8::String>(data, "file");
|
||||
auto volume = Nan::GetLocal<v8::Number>(data, "volume", Nan::New<v8::Number>(1.f));
|
||||
v8::Local<v8::Function> callback = Nan::GetLocal<v8::Function>(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<v8::Function> 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<v8::Function>();
|
||||
cb.Reset();
|
||||
|
||||
v8::Local<v8::Value> arguments[2];
|
||||
arguments[0] = Nan::New<v8::Number>((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<v8::Number>()->Value());
|
||||
}
|
@ -1,31 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <nan.h>
|
||||
|
||||
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<void(PlaybackResult, const std::string&)> 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 <functional>
|
||||
#include <string_view>
|
||||
#include <nan.h>
|
||||
|
||||
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<void(PlaybackResult, const std::string&)> 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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user