1069 lines
35 KiB
C++
1069 lines
35 KiB
C++
|
#include "asio.h"
|
||
|
|
||
|
#include <mutex>
|
||
|
#include <system_error>
|
||
|
#include <utility>
|
||
|
|
||
|
#include <audioclient.h>
|
||
|
|
||
|
#include "external/asio/asiolist.h"
|
||
|
#include "hooks/audio/audio.h"
|
||
|
#include "hooks/audio/audio_private.h"
|
||
|
#include "hooks/audio/util.h"
|
||
|
#include "hooks/audio/backends/wasapi/defs.h"
|
||
|
#include "util/flags_helper.h"
|
||
|
#include "util/logging.h"
|
||
|
|
||
|
// std::max
|
||
|
#ifdef max
|
||
|
#undef max
|
||
|
#endif
|
||
|
|
||
|
// std::min
|
||
|
#ifdef min
|
||
|
#undef min
|
||
|
#endif
|
||
|
|
||
|
constexpr double REFTIMES_PER_SEC = 10000000.;
|
||
|
|
||
|
AsioBackend *ASIO_BACKEND = nullptr;
|
||
|
|
||
|
static std::string asio_error_str(AsioError error) {
|
||
|
switch (error) {
|
||
|
ENUM_VARIANT(ASE_OK);
|
||
|
ENUM_VARIANT(ASE_Success);
|
||
|
ENUM_VARIANT(ASE_NotPresent);
|
||
|
ENUM_VARIANT(ASE_HWMalfunction);
|
||
|
ENUM_VARIANT(ASE_InvalidParameter);
|
||
|
ENUM_VARIANT(ASE_InvalidMode);
|
||
|
ENUM_VARIANT(ASE_SPNotAdvancing);
|
||
|
ENUM_VARIANT(ASE_NoClock);
|
||
|
ENUM_VARIANT(ASE_NoMemory);
|
||
|
default:
|
||
|
return fmt::to_string(static_cast<unsigned>(error));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static std::string asio_sample_type_str(AsioSampleType type) {
|
||
|
switch (type) {
|
||
|
ENUM_VARIANT(ASIOSTInt16MSB);
|
||
|
ENUM_VARIANT(ASIOSTInt24MSB);
|
||
|
ENUM_VARIANT(ASIOSTInt32MSB);
|
||
|
ENUM_VARIANT(ASIOSTFloat32MSB);
|
||
|
ENUM_VARIANT(ASIOSTFloat64MSB);
|
||
|
ENUM_VARIANT(ASIOSTInt32MSB16);
|
||
|
ENUM_VARIANT(ASIOSTInt32MSB18);
|
||
|
ENUM_VARIANT(ASIOSTInt32MSB20);
|
||
|
ENUM_VARIANT(ASIOSTInt32MSB24);
|
||
|
ENUM_VARIANT(ASIOSTInt16LSB);
|
||
|
ENUM_VARIANT(ASIOSTInt24LSB);
|
||
|
ENUM_VARIANT(ASIOSTInt32LSB);
|
||
|
ENUM_VARIANT(ASIOSTFloat32LSB);
|
||
|
ENUM_VARIANT(ASIOSTFloat64LSB);
|
||
|
ENUM_VARIANT(ASIOSTInt32LSB16);
|
||
|
ENUM_VARIANT(ASIOSTInt32LSB18);
|
||
|
ENUM_VARIANT(ASIOSTInt32LSB20);
|
||
|
ENUM_VARIANT(ASIOSTInt32LSB24);
|
||
|
ENUM_VARIANT(ASIOSTDSDInt8LSB1);
|
||
|
ENUM_VARIANT(ASIOSTDSDInt8MSB1);
|
||
|
ENUM_VARIANT(ASIOSTDSDInt8NER8);
|
||
|
default:
|
||
|
return fmt::to_string(type);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static SampleType convert_windows_format(const WAVEFORMATEXTENSIBLE &format_ex) {
|
||
|
const auto &format = format_ex.Format;
|
||
|
|
||
|
bool pcm_format = false;
|
||
|
bool float_format = false;
|
||
|
|
||
|
if (format.wFormatTag == WAVE_FORMAT_PCM) {
|
||
|
pcm_format = true;
|
||
|
} else if (format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
|
||
|
float_format = true;
|
||
|
} else if (format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
||
|
if (format_ex.SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM) {
|
||
|
pcm_format = true;
|
||
|
} else if (format_ex.SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
|
||
|
float_format = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (pcm_format) {
|
||
|
switch (format.wBitsPerSample) {
|
||
|
case 16:
|
||
|
return SampleType::SINT_16;
|
||
|
case 24:
|
||
|
return SampleType::SINT_24;
|
||
|
case 32:
|
||
|
return SampleType::SINT_32;
|
||
|
default:
|
||
|
return SampleType::UNSUPPORTED;
|
||
|
}
|
||
|
} else if (float_format) {
|
||
|
switch (format.wBitsPerSample) {
|
||
|
case 32:
|
||
|
return SampleType::FLOAT_32;
|
||
|
case 64:
|
||
|
return SampleType::FLOAT_64;
|
||
|
default:
|
||
|
return SampleType::UNSUPPORTED;
|
||
|
}
|
||
|
} else {
|
||
|
return SampleType::UNSUPPORTED;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static SampleType convert_asio_sample_type(AsioSampleType type) {
|
||
|
switch (type) {
|
||
|
case ASIOSTInt16LSB:
|
||
|
return SampleType::SINT_16;
|
||
|
case ASIOSTInt24LSB:
|
||
|
return SampleType::SINT_24;
|
||
|
case ASIOSTInt32LSB:
|
||
|
return SampleType::SINT_32;
|
||
|
case ASIOSTFloat32LSB:
|
||
|
return SampleType::FLOAT_32;
|
||
|
case ASIOSTFloat64LSB:
|
||
|
return SampleType::FLOAT_64;
|
||
|
default:
|
||
|
return SampleType::UNSUPPORTED;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
AsioBackend::AsioBackend() {
|
||
|
this->asio_thread = std::thread([this]() {
|
||
|
std::unique_lock<std::mutex> lock_handle(this->asio_thread_state_lock);
|
||
|
|
||
|
log_info("audio::asio", "initializing ASIO thread");
|
||
|
|
||
|
if (this->load_driver() &&
|
||
|
this->update_driver_info() &&
|
||
|
this->set_initial_format(this->last_checked_format))
|
||
|
{
|
||
|
this->asio_thread_state = AsioThreadState::Running;
|
||
|
} else {
|
||
|
this->asio_thread_state = AsioThreadState::Failed;
|
||
|
}
|
||
|
|
||
|
// Notify condition variable waiters of thread state update
|
||
|
lock_handle.unlock();
|
||
|
this->asio_thread_state_cv.notify_all();
|
||
|
|
||
|
if (this->asio_thread_state.load() != AsioThreadState::Running) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
log_info("audio::asio", "ASIO thread entering main loop");
|
||
|
|
||
|
this->asio_thread_initialized = true;
|
||
|
|
||
|
while (this->asio_thread_state.load() == AsioThreadState::Running) {
|
||
|
struct AsioThreadMessage msg;
|
||
|
this->asio_msg_queue_func.wait_dequeue(msg);
|
||
|
|
||
|
auto result = msg.fn();
|
||
|
|
||
|
if (msg.result_needed) {
|
||
|
this->asio_msg_queue_result.enqueue(result);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this->asio_thread_initialized = false;
|
||
|
});
|
||
|
|
||
|
std::unique_lock<std::mutex> lock_handle(this->asio_thread_state_lock);
|
||
|
this->asio_thread_state_cv.wait(lock_handle, [this]() {
|
||
|
return this->asio_thread_state.load() != AsioThreadState::Closed;
|
||
|
});
|
||
|
|
||
|
if (this->asio_thread_state.load() == AsioThreadState::Failed) {
|
||
|
log_fatal("audio::asio", "failed to initialize ASIO thread");
|
||
|
}
|
||
|
}
|
||
|
AsioBackend::~AsioBackend() {
|
||
|
log_info("audio::asio", "shutting down ASIO backend");
|
||
|
|
||
|
if (this->asio_thread_initialized) {
|
||
|
this->run_on_asio_thread([this]() {
|
||
|
this->unload_driver();
|
||
|
|
||
|
return ASE_OK;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// shut down ASIO handler thread
|
||
|
this->set_thread_state(AsioThreadState::ShuttingDown);
|
||
|
|
||
|
// enqueue function to break event loop
|
||
|
if (this->asio_thread_initialized) {
|
||
|
this->run_on_asio_thread([]() {
|
||
|
return ASE_OK;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
this->asio_thread.join();
|
||
|
}
|
||
|
|
||
|
void AsioBackend::set_thread_state(AsioThreadState state) {
|
||
|
std::unique_lock<std::mutex> lock_handle(this->asio_thread_state_lock);
|
||
|
|
||
|
this->asio_thread_state = state;
|
||
|
|
||
|
// Notify condition variable waiters of thread state update
|
||
|
lock_handle.unlock();
|
||
|
this->asio_thread_state_cv.notify_all();
|
||
|
}
|
||
|
bool AsioBackend::load_driver() {
|
||
|
AsioDriverList asio_driver_list;
|
||
|
|
||
|
for (const auto &driver : asio_driver_list.driver_list) {
|
||
|
log_info("audio::asio", "Driver {}", driver.id);
|
||
|
log_info("audio::asio", "... Name : {}", driver.name);
|
||
|
log_info("audio::asio", "... Path : {}", driver.dll_path);
|
||
|
}
|
||
|
|
||
|
const auto driver_id = hooks::audio::ASIO_DRIVER_ID;
|
||
|
|
||
|
IAsio *driver = nullptr;
|
||
|
auto ret = asio_driver_list.open_driver(driver_id, reinterpret_cast<void **>(&driver));
|
||
|
if (ret != 0) {
|
||
|
log_warning("audio::asio", "failed to open driver: {}", FMT_HRESULT(ret));
|
||
|
} else if (driver) {
|
||
|
this->driver_info_.sys_ref = GetDesktopWindow();
|
||
|
if (!driver->init(this->driver_info_.sys_ref)) {
|
||
|
driver->get_error_message(this->driver_info_.error_message);
|
||
|
driver = nullptr;
|
||
|
|
||
|
log_warning("audio::asio", "failed to initialize driver: {}", this->driver_info_.error_message);
|
||
|
} else {
|
||
|
this->asio_driver = driver;
|
||
|
this->asio_driver->AddRef();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return this->asio_driver != nullptr;
|
||
|
}
|
||
|
bool AsioBackend::update_driver_info() {
|
||
|
memset(&this->driver_info_.name, 0, sizeof(this->driver_info_.name));
|
||
|
this->driver_info_.asio_version = 0;
|
||
|
this->driver_info_.driver_version = 0;
|
||
|
|
||
|
memset(&this->asio_info_, 0, sizeof(this->asio_info_));
|
||
|
|
||
|
if (!this->asio_driver) {
|
||
|
log_warning("audio::asio", "attempted to update driver info when no driver is loaded");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
this->driver_info_.asio_version = 2;
|
||
|
|
||
|
// `get_driver_name` and `get_driver_version` are not supposed to fail according to the
|
||
|
// function definitions and the documentation
|
||
|
this->asio_driver->get_driver_name(this->driver_info_.name);
|
||
|
this->driver_info_.driver_version = this->asio_driver->get_driver_version();
|
||
|
|
||
|
// the rest of the functions can fail however
|
||
|
AsioError result;
|
||
|
|
||
|
result = this->asio_driver->get_channels(&this->asio_info_.inputs, &this->asio_info_.outputs);
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to get channels: {}", asio_error_str(result));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
result = this->asio_driver->get_buffer_size(
|
||
|
&this->asio_info_.buffer_min_size,
|
||
|
&this->asio_info_.buffer_max_size,
|
||
|
&this->asio_info_.buffer_preferred_size,
|
||
|
&this->asio_info_.buffer_granularity);
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to get buffer sizes: {}", asio_error_str(result));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
for (long i = 0; i < this->asio_info_.outputs; i++) {
|
||
|
auto &channel_info = this->asio_channel_info_.emplace_back();
|
||
|
channel_info.channel = i;
|
||
|
channel_info.is_input = AsioFalse;
|
||
|
|
||
|
result = this->asio_driver->get_channel_info(&channel_info);
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to get channel {} info: {}", i, asio_error_str(result));
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
log_info("audio::asio", "Device Info:");
|
||
|
log_info("audio::asio", "... Name : {}", this->driver_info_.name);
|
||
|
log_info("audio::asio", "... Version : {}", this->driver_info_.driver_version);
|
||
|
log_info("audio::asio", "... Inputs : {} channels", this->asio_info_.inputs);
|
||
|
log_info("audio::asio", "... Outputs : {} channels", this->asio_info_.outputs);
|
||
|
log_info("audio::asio", "... Buffer Minimum : {} samples", this->asio_info_.buffer_min_size);
|
||
|
log_info("audio::asio", "... Buffer Maximum : {} samples", this->asio_info_.buffer_max_size);
|
||
|
log_info("audio::asio", "... Buffer Preferred : {} samples", this->asio_info_.buffer_preferred_size);
|
||
|
log_info("audio::asio", "... Buffer Granularity : {} samples", this->asio_info_.buffer_granularity);
|
||
|
|
||
|
log_info("audio::asio", "Channel Info:");
|
||
|
for (const auto &channel_info : this->asio_channel_info_) {
|
||
|
log_info("audio::asio", "... Channel {}: {} (group: {}, type: {})",
|
||
|
channel_info.channel,
|
||
|
channel_info.name,
|
||
|
channel_info.channel_group,
|
||
|
asio_sample_type_str(channel_info.type));
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
bool AsioBackend::update_latency() {
|
||
|
auto result = this->asio_driver->get_latencies(
|
||
|
&this->asio_info_.input_latency,
|
||
|
&this->asio_info_.output_latency);
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to get latency: {}", asio_error_str(result));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
bool AsioBackend::set_initial_format(WAVEFORMATEXTENSIBLE &target) {
|
||
|
AsioSampleRate sample_rate = 0.0;
|
||
|
|
||
|
// get current sample rate
|
||
|
auto result = this->asio_driver->get_sample_rate(&sample_rate);
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to get current sample rate: {}", asio_error_str(result));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// set initial format state
|
||
|
target.Format.wFormatTag = WAVE_FORMAT_PCM;
|
||
|
target.Format.nChannels = static_cast<WORD>(this->asio_info_.outputs);
|
||
|
target.Format.nSamplesPerSec = static_cast<WORD>(sample_rate);
|
||
|
target.Format.nBlockAlign = target.Format.nChannels * (target.Format.wBitsPerSample / 8);
|
||
|
target.Format.nAvgBytesPerSec = target.Format.nSamplesPerSec * target.Format.nBlockAlign;
|
||
|
target.Format.cbSize = 0;
|
||
|
|
||
|
// set initial sub-format
|
||
|
if (!this->asio_channel_info_.empty()) {
|
||
|
this->asio_sample_type = convert_asio_sample_type(this->asio_channel_info_[0].type);
|
||
|
|
||
|
// mark this as the extensible struct
|
||
|
target.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
||
|
target.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
|
||
|
target.dwChannelMask = SPEAKER_ALL;
|
||
|
|
||
|
switch (this->asio_sample_type) {
|
||
|
case SampleType::SINT_16:
|
||
|
target.Format.wBitsPerSample = 16;
|
||
|
target.Samples.wValidBitsPerSample = 16;
|
||
|
target.SubFormat = GUID_KSDATAFORMAT_SUBTYPE_PCM;
|
||
|
break;
|
||
|
case SampleType::SINT_24:
|
||
|
target.Format.wBitsPerSample = 24;
|
||
|
target.Samples.wValidBitsPerSample = 24;
|
||
|
target.SubFormat = GUID_KSDATAFORMAT_SUBTYPE_PCM;
|
||
|
break;
|
||
|
case SampleType::SINT_32:
|
||
|
target.Format.wBitsPerSample = 32;
|
||
|
target.Samples.wValidBitsPerSample = 32;
|
||
|
target.SubFormat = GUID_KSDATAFORMAT_SUBTYPE_PCM;
|
||
|
break;
|
||
|
case SampleType::FLOAT_32:
|
||
|
target.Format.wBitsPerSample = 32;
|
||
|
target.Samples.wValidBitsPerSample = 32;
|
||
|
target.SubFormat = GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
||
|
break;
|
||
|
case SampleType::FLOAT_64:
|
||
|
target.Format.wBitsPerSample = 64;
|
||
|
target.Samples.wValidBitsPerSample = 64;
|
||
|
target.SubFormat = GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
||
|
break;
|
||
|
default:
|
||
|
target.Format.wFormatTag = 0;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
bool AsioBackend::init() {
|
||
|
if (ASIO_BACKEND) {
|
||
|
log_warning("audio::asio", "ASIO callbacks already initialized");
|
||
|
return false;
|
||
|
}
|
||
|
ASIO_BACKEND = this;
|
||
|
|
||
|
// create buffer objects
|
||
|
for (long i = 0; i < this->format_.Format.nChannels; i++) {
|
||
|
auto &buffer_info = this->asio_buffers.emplace_back();
|
||
|
buffer_info.is_input = AsioFalse;
|
||
|
buffer_info.channel_num = i;
|
||
|
buffer_info.buffers[0] = nullptr;
|
||
|
buffer_info.buffers[1] = nullptr;
|
||
|
}
|
||
|
|
||
|
// setup ASIO callbacks
|
||
|
this->asio_callbacks.buffer_switch = AsioBackend::buffer_switch;
|
||
|
this->asio_callbacks.sample_rate_did_change = AsioBackend::sample_rate_did_change;
|
||
|
this->asio_callbacks.asio_message = AsioBackend::asio_message;
|
||
|
|
||
|
// create buffers
|
||
|
auto result = this->asio_driver->create_buffers(
|
||
|
this->asio_buffers.data(),
|
||
|
this->format_.Format.nChannels,
|
||
|
this->asio_info_.buffer_preferred_size,
|
||
|
&this->asio_callbacks);
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to create buffers: {}", asio_error_str(result));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
auto sample_size = sample_type_size(this->asio_sample_type);
|
||
|
auto num_samples = static_cast<size_t>(this->asio_info_.buffer_preferred_size);
|
||
|
auto buffer_size = num_samples * sample_size;
|
||
|
|
||
|
for (const auto &buffer_info : this->asio_buffers) {
|
||
|
for (size_t i = 0; i < _countof(buffer_info.buffers); i++) {
|
||
|
log_misc("audio::asio", "channel[{}].buffers[{}] = {}",
|
||
|
buffer_info.channel_num,
|
||
|
i,
|
||
|
fmt::ptr(buffer_info.buffers[i]));
|
||
|
|
||
|
// initialize buffer contents to avoid garbage from being played on start
|
||
|
memset(buffer_info.buffers[i], 0, buffer_size);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// FlexASIO throws an error if `update_latencies` is called before `create_buffers`
|
||
|
if (!this->update_latency()) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
bool AsioBackend::unload_driver() {
|
||
|
AsioError result;
|
||
|
|
||
|
if (!this->asio_driver) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// stop audio
|
||
|
result = this->asio_driver->stop();
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to stop audio: {}", asio_error_str(result));
|
||
|
}
|
||
|
|
||
|
// clear buffer state
|
||
|
this->asio_buffers.clear();
|
||
|
|
||
|
// dispose buffers
|
||
|
result = this->asio_driver->dispose_buffers();
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to dispose buffers: {}", asio_error_str(result));
|
||
|
}
|
||
|
|
||
|
// unload driver
|
||
|
this->asio_driver->Release();
|
||
|
this->asio_driver = nullptr;
|
||
|
|
||
|
// reset global state
|
||
|
if (ASIO_BACKEND == this) {
|
||
|
ASIO_BACKEND = nullptr;
|
||
|
}
|
||
|
|
||
|
// reset local state
|
||
|
this->asio_channel_info_.clear();
|
||
|
memset(&this->asio_callbacks, 0, sizeof(this->asio_callbacks));
|
||
|
memset(&this->driver_info_, 0, sizeof(this->driver_info_));
|
||
|
memset(&this->asio_info_, 0, sizeof(this->asio_info_));
|
||
|
this->asio_sample_type = SampleType::UNSUPPORTED;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
void AsioBackend::reset() {
|
||
|
AsioError result;
|
||
|
|
||
|
if (!this->unload_driver()) {
|
||
|
log_warning("audio::asio", "failed to unload driver");
|
||
|
}
|
||
|
|
||
|
if (!this->load_driver() ||
|
||
|
!this->update_driver_info() ||
|
||
|
!this->set_initial_format(this->last_checked_format))
|
||
|
{
|
||
|
log_warning("audio::asio", "failed to initialize driver");
|
||
|
this->set_thread_state(AsioThreadState::Failed);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
auto sample_rate = this->format_.Format.nSamplesPerSec;
|
||
|
|
||
|
result = this->asio_driver->set_sample_rate(static_cast<double>(sample_rate));
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to set sample rate: {}", asio_error_str(result));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// init buffers
|
||
|
if (FAILED(this->init())) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// start processing if started before reset
|
||
|
if (this->started.load()) {
|
||
|
result = this->asio_driver->start();
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to start processing: {}", asio_error_str(result));
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
AsioError AsioBackend::run_on_asio_thread(AsioFunction fn, bool result_needed) {
|
||
|
AsioError result = ASE_NotPresent;
|
||
|
|
||
|
struct AsioThreadMessage msg {
|
||
|
.fn = std::move(fn),
|
||
|
.result_needed = result_needed,
|
||
|
};
|
||
|
this->asio_msg_queue_func.enqueue(msg);
|
||
|
|
||
|
if (result_needed) {
|
||
|
this->asio_msg_queue_result.wait_dequeue(result);
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
void AsioBackend::open_control_panel() {
|
||
|
if (!this->asio_thread_initialized) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
auto result = this->run_on_asio_thread([this]() {
|
||
|
return this->asio_driver->control_panel();
|
||
|
});
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to open control panel: {}", asio_error_str(result));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AsioBackend::buffer_switch(long double_buffer_index, AsioBool) {
|
||
|
auto self = ASIO_BACKEND;
|
||
|
|
||
|
auto sample_size = sample_type_size(self->asio_sample_type);
|
||
|
auto channels = static_cast<size_t>(self->format_.Format.nChannels);
|
||
|
auto num_samples = static_cast<size_t>(self->asio_info_.buffer_preferred_size);
|
||
|
auto frame_size = channels * sample_size;
|
||
|
auto buffer_len = num_samples * frame_size;
|
||
|
size_t written = 0;
|
||
|
|
||
|
struct BufferEntry *entry;
|
||
|
while ((entry = self->queue.peek()) != nullptr) {
|
||
|
auto write_len = std::min(entry->length - entry->read, buffer_len - written);
|
||
|
|
||
|
if (write_len == 0) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
auto frames_to_write = write_len / frame_size;
|
||
|
|
||
|
for (size_t i = 0; i < frames_to_write; i++) {
|
||
|
for (size_t j = 0; j < channels; j++) {
|
||
|
auto buffer = reinterpret_cast<uint8_t *>(self->asio_buffers[j].buffers[double_buffer_index]);
|
||
|
memcpy(
|
||
|
&buffer[i * sample_size],
|
||
|
&entry->buffer[entry->read + (i * channels + j) * sample_size],
|
||
|
sample_size);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
if (frames_to_write * frame_size != write_len) {
|
||
|
log_warning("audio::asio", "dropped some frames!");
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
written += write_len;
|
||
|
|
||
|
if (entry->read + write_len >= entry->length) {
|
||
|
CoTaskMemFree(entry->buffer);
|
||
|
self->queue.pop();
|
||
|
} else {
|
||
|
entry->read += write_len;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// not all drivers support this method, ignore error
|
||
|
self->asio_driver->output_ready();
|
||
|
|
||
|
if (self->relay_handle.has_value()) {
|
||
|
|
||
|
// trigger game audio callback
|
||
|
if (!SetEvent(self->relay_handle.value())) {
|
||
|
DWORD last_error = GetLastError();
|
||
|
|
||
|
log_warning("audio::asio", "AsioBackend::buffer_switch: SetEvent failed: {} ({})",
|
||
|
last_error,
|
||
|
std::system_category().message(last_error));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (written > 0) {
|
||
|
self->queued_frames.fetch_sub(static_cast<uint32_t>(written / frame_size));
|
||
|
self->queued_bytes.fetch_sub(written);
|
||
|
}
|
||
|
}
|
||
|
void AsioBackend::sample_rate_did_change(AsioSampleRate sample_rate) {
|
||
|
auto self = ASIO_BACKEND;
|
||
|
|
||
|
log_warning("audio::asio", "sample rate change to {} detected, not supported, resetting",
|
||
|
sample_rate);
|
||
|
|
||
|
self->run_on_asio_thread([self]() {
|
||
|
self->reset();
|
||
|
|
||
|
return ASE_OK;
|
||
|
}, false);
|
||
|
}
|
||
|
long AsioBackend::asio_message(long selector, long value, void *message, double *opt) {
|
||
|
auto self = ASIO_BACKEND;
|
||
|
|
||
|
switch (selector) {
|
||
|
case kAsioSelectorSupported:
|
||
|
switch (value) {
|
||
|
case kAsioEngineVersion:
|
||
|
case kAsioResetRequest:
|
||
|
case kAsioResyncRequest:
|
||
|
case kAsioLatenciesChanged:
|
||
|
case kAsioSupportsTimeInfo:
|
||
|
case kAsioOverload:
|
||
|
return 1L;
|
||
|
default:
|
||
|
return 0L;
|
||
|
}
|
||
|
case kAsioEngineVersion:
|
||
|
return 2L;
|
||
|
case kAsioResetRequest:
|
||
|
log_misc("audio::asio", "reset request");
|
||
|
|
||
|
self->run_on_asio_thread([self]() {
|
||
|
self->reset();
|
||
|
|
||
|
return ASE_OK;
|
||
|
}, false);
|
||
|
|
||
|
return 1L;
|
||
|
case kAsioResyncRequest:
|
||
|
log_misc("audio::asio", "resyncing");
|
||
|
return 1L;
|
||
|
case kAsioLatenciesChanged:
|
||
|
log_misc("audio::asio", "latency changed");
|
||
|
|
||
|
self->run_on_asio_thread([self]() {
|
||
|
self->update_latency();
|
||
|
|
||
|
return ASE_OK;
|
||
|
}, false);
|
||
|
|
||
|
return 0L;
|
||
|
case kAsioSupportsTimeInfo:
|
||
|
// TODO(felix): support timecode buffer switch
|
||
|
return 0L;
|
||
|
case kAsioOverload:
|
||
|
log_misc("audio::asio", "overload detected");
|
||
|
return 1L;
|
||
|
default:
|
||
|
return 0L;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool AsioBackend::is_supported_subformat(const WAVEFORMATEXTENSIBLE &format_ex) noexcept {
|
||
|
bool pcm_format = false;
|
||
|
bool float_format = false;
|
||
|
|
||
|
const auto &format = format_ex.Format;
|
||
|
|
||
|
if (format.wFormatTag == WAVE_FORMAT_PCM) {
|
||
|
pcm_format = true;
|
||
|
} else if (format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
|
||
|
float_format = true;
|
||
|
} else if (format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
||
|
if (format_ex.SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM) {
|
||
|
pcm_format = true;
|
||
|
} else if (format_ex.SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
|
||
|
float_format = true;
|
||
|
}
|
||
|
} else {
|
||
|
log_warning("audio::asio", "unsupported format tag");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (pcm_format) {
|
||
|
switch (format.wBitsPerSample) {
|
||
|
case 16:
|
||
|
case 24:
|
||
|
case 32:
|
||
|
break;
|
||
|
default:
|
||
|
log_warning("audio::asio", "unknown PCM sample size: {}", format.wBitsPerSample);
|
||
|
return false;
|
||
|
}
|
||
|
} else if (float_format) {
|
||
|
switch (format.wBitsPerSample) {
|
||
|
case 32:
|
||
|
case 64:
|
||
|
break;
|
||
|
default:
|
||
|
log_warning("audio::asio", "unknown float sample size: {}", format.wBitsPerSample);
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
REFERENCE_TIME AsioBackend::compute_ref_time() const {
|
||
|
auto sample_rate = this->last_checked_format.Format.nSamplesPerSec;
|
||
|
auto buffer_frames = this->asio_info_.buffer_preferred_size;
|
||
|
|
||
|
return static_cast<REFERENCE_TIME>(ceil(REFTIMES_PER_SEC * buffer_frames / sample_rate));
|
||
|
}
|
||
|
REFERENCE_TIME AsioBackend::compute_latency_ref_time() const {
|
||
|
auto sample_rate = this->last_checked_format.Format.nSamplesPerSec;
|
||
|
auto buffer_frames = this->asio_info_.output_latency;
|
||
|
|
||
|
return static_cast<REFERENCE_TIME>(ceil(REFTIMES_PER_SEC * buffer_frames / sample_rate));
|
||
|
}
|
||
|
|
||
|
const WAVEFORMATEXTENSIBLE &AsioBackend::format() const noexcept {
|
||
|
return this->format_;
|
||
|
}
|
||
|
|
||
|
HRESULT AsioBackend::on_initialize(
|
||
|
AUDCLNT_SHAREMODE *ShareMode,
|
||
|
DWORD *StreamFlags,
|
||
|
REFERENCE_TIME *hnsBufferDuration,
|
||
|
REFERENCE_TIME *hnsPeriodicity,
|
||
|
const WAVEFORMATEX *pFormat,
|
||
|
LPCGUID AudioSessionGuid) noexcept
|
||
|
{
|
||
|
AsioError result;
|
||
|
|
||
|
copy_wave_format(&this->format_, pFormat);
|
||
|
memcpy(&this->last_checked_format, &this->format_, sizeof(this->format_));
|
||
|
|
||
|
if (!this->asio_thread_initialized) {
|
||
|
log_warning("audio::asio", "{}: ASIO thread not initialized", "AsioBackend::on_initialize");
|
||
|
return AUDCLNT_E_DEVICE_INVALIDATED;
|
||
|
}
|
||
|
|
||
|
if (ASIO_BACKEND) {
|
||
|
log_warning("audio::asio", "ASIO backend already initialized");
|
||
|
return AUDCLNT_E_ALREADY_INITIALIZED;
|
||
|
}
|
||
|
|
||
|
auto sample_rate = this->format_.Format.nSamplesPerSec;
|
||
|
|
||
|
result = this->run_on_asio_thread([this, sample_rate]() {
|
||
|
return this->asio_driver->set_sample_rate(static_cast<double>(sample_rate));
|
||
|
});
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to set sample rate: {}", asio_error_str(result));
|
||
|
return AUDCLNT_E_UNSUPPORTED_FORMAT;
|
||
|
}
|
||
|
|
||
|
auto ref_time = this->compute_ref_time();
|
||
|
log_info("audio::asio", "AsioBackend::on_intialize: sample rate = {}, reference time = {}",
|
||
|
sample_rate,
|
||
|
ref_time);
|
||
|
|
||
|
// warn if this is being used on shared mode without event callback
|
||
|
if (*ShareMode == AUDCLNT_SHAREMODE_SHARED &&
|
||
|
(*StreamFlags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != AUDCLNT_STREAMFLAGS_EVENTCALLBACK)
|
||
|
{
|
||
|
log_warning("audio::asio", "shared mode without event callback is not supported, sound will be garbled!");
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
// change to shared mode in case the audio dummy mode is not being used
|
||
|
if (*ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
||
|
*ShareMode = AUDCLNT_SHAREMODE_SHARED;
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
*hnsBufferDuration = ref_time;
|
||
|
*hnsPeriodicity = ref_time;
|
||
|
|
||
|
if (!is_supported_subformat(this->format_)) {
|
||
|
return AUDCLNT_E_UNSUPPORTED_FORMAT;
|
||
|
}
|
||
|
|
||
|
result = this->run_on_asio_thread([this]() {
|
||
|
return this->init() ? ASE_OK : ASE_NotPresent;
|
||
|
});
|
||
|
if (result != ASE_OK) {
|
||
|
return AUDCLNT_E_DEVICE_INVALIDATED;
|
||
|
}
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_get_buffer_size(uint32_t *buffer_frames) noexcept {
|
||
|
*buffer_frames = static_cast<uint32_t>(this->asio_info_.buffer_preferred_size);
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_get_stream_latency(REFERENCE_TIME *latency) noexcept {
|
||
|
if (!this->asio_thread_initialized) {
|
||
|
log_warning("audio::asio", "{}: ASIO thread not initialized", "AsioBackend::on_get_stream_latency");
|
||
|
return AUDCLNT_E_NOT_INITIALIZED;
|
||
|
}
|
||
|
|
||
|
auto result = this->run_on_asio_thread([this]() {
|
||
|
return this->update_latency() ? ASE_OK : ASE_NotPresent;
|
||
|
});
|
||
|
if (result != ASE_OK) {
|
||
|
return AUDCLNT_E_DEVICE_INVALIDATED;
|
||
|
}
|
||
|
|
||
|
auto latency_ref_time = this->compute_latency_ref_time();
|
||
|
log_misc("audio::asio", "output latency = {}, reference time = {}",
|
||
|
this->asio_info_.output_latency,
|
||
|
latency_ref_time);
|
||
|
|
||
|
*latency = latency_ref_time;
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) noexcept {
|
||
|
padding_frames = this->queued_frames.load();
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_is_format_supported(
|
||
|
AUDCLNT_SHAREMODE *ShareMode,
|
||
|
const WAVEFORMATEX *pFormat,
|
||
|
WAVEFORMATEX **ppClosestMatch) noexcept
|
||
|
{
|
||
|
if (!this->asio_thread_initialized) {
|
||
|
log_warning("audio::asio", "{}: ASIO thread not initialized", "AsioBackend::on_is_format_supported");
|
||
|
return AUDCLNT_E_NOT_INITIALIZED;
|
||
|
}
|
||
|
|
||
|
copy_wave_format(&this->last_checked_format, pFormat);
|
||
|
|
||
|
auto num_channels = this->last_checked_format.Format.nChannels;
|
||
|
auto sample_rate = this->last_checked_format.Format.nSamplesPerSec;
|
||
|
|
||
|
log_misc("audio::asio", "AsioBackend::on_is_format_supported: checking format {} channels, {} Hz, {}-bit",
|
||
|
num_channels,
|
||
|
sample_rate,
|
||
|
this->last_checked_format.Format.wBitsPerSample);
|
||
|
|
||
|
// check channel count
|
||
|
if (num_channels > this->asio_info_.outputs) {
|
||
|
log_warning("audio::asio", "channel count larger than number of device channels");
|
||
|
return AUDCLNT_E_UNSUPPORTED_FORMAT;
|
||
|
}
|
||
|
|
||
|
// check sub-format
|
||
|
if (!this->is_supported_subformat(this->last_checked_format)) {
|
||
|
return AUDCLNT_E_UNSUPPORTED_FORMAT;
|
||
|
}
|
||
|
|
||
|
// check sample rate
|
||
|
auto result = this->run_on_asio_thread([this, sample_rate]() {
|
||
|
return this->asio_driver->can_sample_rate(static_cast<double>(sample_rate));
|
||
|
});
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "unsupported sample rate: {}", sample_rate);
|
||
|
return AUDCLNT_E_UNSUPPORTED_FORMAT;
|
||
|
}
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept {
|
||
|
if (!this->asio_thread_initialized) {
|
||
|
log_warning("audio::asio", "{}: ASIO thread not initialized", "AsioBackend::on_get_mix_format");
|
||
|
return AUDCLNT_E_NOT_INITIALIZED;
|
||
|
}
|
||
|
|
||
|
auto format = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(CoTaskMemAlloc(sizeof(WAVEFORMATEXTENSIBLE)));
|
||
|
|
||
|
if (!format) {
|
||
|
DWORD last_error = GetLastError();
|
||
|
|
||
|
log_warning("audio::asio", "failed to allocate memory for mix format: {} ({})",
|
||
|
last_error,
|
||
|
std::system_category().message(last_error));
|
||
|
|
||
|
return AUDCLNT_E_BUFFER_ERROR;
|
||
|
}
|
||
|
|
||
|
// build format based on ASIO driver parameters
|
||
|
auto result = this->run_on_asio_thread([this, format]() {
|
||
|
return this->set_initial_format(*format) ? ASE_OK : ASE_NotPresent;
|
||
|
});
|
||
|
if (result != ASE_OK) {
|
||
|
CoTaskMemFree(format);
|
||
|
return AUDCLNT_E_DEVICE_INVALIDATED;
|
||
|
}
|
||
|
|
||
|
*pp_device_format = reinterpret_cast<WAVEFORMATEX *>(format);
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_get_device_period(
|
||
|
REFERENCE_TIME *default_device_period,
|
||
|
REFERENCE_TIME *minimum_device_period) noexcept
|
||
|
{
|
||
|
auto ref_time = this->compute_ref_time();
|
||
|
|
||
|
log_info("audio::asio", "AsioBackend::on_get_device_period: sample rate = {}, reference time = {}",
|
||
|
this->last_checked_format.Format.nSamplesPerSec,
|
||
|
ref_time);
|
||
|
|
||
|
if (default_device_period) {
|
||
|
*default_device_period = ref_time;
|
||
|
}
|
||
|
if (minimum_device_period) {
|
||
|
*minimum_device_period = ref_time;
|
||
|
}
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_start() noexcept {
|
||
|
log_misc("audio::asio", "AsioBackend::on_start");
|
||
|
|
||
|
if (!this->asio_thread_initialized) {
|
||
|
log_warning("audio::asio", "{}: ASIO thread not initialized", "AsioBackend::on_start");
|
||
|
return AUDCLNT_E_NOT_INITIALIZED;
|
||
|
}
|
||
|
|
||
|
// FlexASIO creates the WASAPI context upon start, prevent recurive initialization
|
||
|
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK);
|
||
|
|
||
|
auto result = this->run_on_asio_thread([this]() {
|
||
|
return this->asio_driver->start();
|
||
|
});
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to start processing: {}", asio_error_str(result));
|
||
|
return AUDCLNT_E_DEVICE_INVALIDATED;
|
||
|
}
|
||
|
|
||
|
this->started = true;
|
||
|
|
||
|
// wait until all queued frames are dequeued
|
||
|
while (this->queued_frames > 0) {
|
||
|
std::this_thread::yield();
|
||
|
}
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_stop() noexcept {
|
||
|
log_misc("audio::asio", "AsioBackend::on_stop");
|
||
|
|
||
|
if (!this->asio_thread_initialized) {
|
||
|
log_warning("audio::asio", "{}: ASIO thread not initialized", "AsioBackend::on_stop");
|
||
|
return AUDCLNT_E_NOT_INITIALIZED;
|
||
|
}
|
||
|
|
||
|
if (hooks::audio::ASIO_FORCE_UNLOAD_ON_STOP) {
|
||
|
log_misc("audio::asio", "AsioBackend::on_stop - using ASIO_FORCE_UNLOAD_ON_STOP workaround");
|
||
|
}
|
||
|
|
||
|
auto result = this->run_on_asio_thread([this]() {
|
||
|
if (hooks::audio::ASIO_FORCE_UNLOAD_ON_STOP) {
|
||
|
this->unload_driver();
|
||
|
return (AsioError)ASE_OK;
|
||
|
}
|
||
|
return this->asio_driver->stop();
|
||
|
});
|
||
|
if (result != ASE_OK) {
|
||
|
log_warning("audio::asio", "failed to stop processing: {}", asio_error_str(result));
|
||
|
return AUDCLNT_E_DEVICE_INVALIDATED;
|
||
|
}
|
||
|
|
||
|
this->started = false;
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_set_event_handle(HANDLE *event_handle) noexcept {
|
||
|
this->relay_handle = *event_handle;
|
||
|
|
||
|
// give WASAPI a dummy handle
|
||
|
*event_handle = CreateEvent(nullptr, true, false, nullptr);
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **pp_data) noexcept {
|
||
|
const size_t buffer_size = this->format_.Format.nBlockAlign * num_frames_requested;
|
||
|
|
||
|
// account for larger conversion buffer size
|
||
|
const auto channels = static_cast<size_t>(this->format_.Format.nChannels);
|
||
|
const auto sample_type = this->asio_sample_type;
|
||
|
const auto converted_size = required_buffer_size(num_frames_requested, channels, sample_type);
|
||
|
|
||
|
const size_t max_size = std::max(buffer_size, converted_size);
|
||
|
|
||
|
// allocate temporary sound buffer
|
||
|
this->active_sound_buffer = reinterpret_cast<BYTE *>(CoTaskMemAlloc(max_size));
|
||
|
|
||
|
// check for allocation error
|
||
|
if (!this->active_sound_buffer) {
|
||
|
DWORD last_error = GetLastError();
|
||
|
|
||
|
log_warning("audio::asio", "failed to allocate sound buffer: {} ({})",
|
||
|
last_error,
|
||
|
std::system_category().message(last_error));
|
||
|
|
||
|
return AUDCLNT_E_BUFFER_ERROR;
|
||
|
}
|
||
|
|
||
|
// hand the buffer to the callee
|
||
|
*pp_data = this->active_sound_buffer;
|
||
|
|
||
|
return S_OK;
|
||
|
}
|
||
|
HRESULT AsioBackend::on_release_buffer(uint32_t num_frames_written, DWORD flags) noexcept {
|
||
|
const size_t length = this->format_.Format.nBlockAlign * num_frames_written;
|
||
|
|
||
|
if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == AUDCLNT_BUFFERFLAGS_SILENT) {
|
||
|
memset(this->active_sound_buffer, 0, length);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
if (this->last_sound_buffer.size() < length) {
|
||
|
this->last_sound_buffer.resize(length);
|
||
|
}
|
||
|
|
||
|
memcpy(this->last_sound_buffer.data(), this->active_sound_buffer, length);
|
||
|
*/
|
||
|
|
||
|
// compute the buffer size after conversion
|
||
|
const auto channels = this->format_.Format.nChannels;
|
||
|
const auto sample_type = this->asio_sample_type;
|
||
|
const size_t conversion_size = required_buffer_size(num_frames_written, channels, sample_type);
|
||
|
|
||
|
// audio channel deinterleaving and subformat conversion
|
||
|
convert_sample_type(
|
||
|
channels,
|
||
|
reinterpret_cast<uint8_t *>(this->active_sound_buffer),
|
||
|
length,
|
||
|
this->conversion_sound_buffer,
|
||
|
convert_windows_format(this->format_),
|
||
|
sample_type);
|
||
|
|
||
|
// enqueue the buffer for playback
|
||
|
struct BufferEntry entry {
|
||
|
.buffer = this->active_sound_buffer,
|
||
|
.length = conversion_size,
|
||
|
.read = 0,
|
||
|
};
|
||
|
this->queue.enqueue(entry);
|
||
|
this->queued_frames.fetch_add(num_frames_written);
|
||
|
this->queued_bytes.fetch_add(conversion_size);
|
||
|
|
||
|
return S_OK;
|
||
|
}
|