#include "asio.h" #include #include #include #include #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(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 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 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 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(&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(this->asio_info_.outputs); target.Format.nSamplesPerSec = static_cast(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(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(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(self->format_.Format.nChannels); auto num_samples = static_cast(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(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(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(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(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(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(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 &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(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(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(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(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(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(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; }