spicetools/hooks/audio/implementations/asio.cpp

1069 lines
35 KiB
C++
Raw Normal View History

2024-08-28 15:10:34 +00:00
#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;
}