├── kdASIOVersion.txt ├── src ├── kdasioconfig │ ├── .gitignore │ ├── mainicon.rc │ ├── mainicon.ico │ ├── images │ │ ├── BG_01.png │ │ ├── Frame.png │ │ ├── Fader BTN.png │ │ ├── INFO BTN ON.png │ │ ├── Koord Logo.png │ │ ├── CONFIG BTN ON.png │ │ ├── INFO BTN OFF.png │ │ ├── SHARED ON BTN.png │ │ ├── CONFIG BTN OFF.png │ │ ├── EXCLUSIVE ON BTN.png │ │ ├── KoordASIO Logo.png │ │ ├── RoundedArrow4x.png │ │ ├── SHARED OFF BTN.png │ │ ├── mark-github-24.png │ │ └── EXCLUSIVE OFF BTN.png │ ├── README.md │ ├── koordipics.qrc │ ├── CMakeLists.txt │ ├── main.cpp │ ├── kdasioconfig.h │ └── kdasioconfig.cpp ├── flexasio │ ├── FlexASIO │ │ ├── flexasio.rc.h │ │ ├── flexasio.rc │ │ ├── control_panel.h │ │ ├── log.h │ │ ├── cflexasio.h │ │ ├── flexasio.idl │ │ ├── dll.def │ │ ├── flexasio.rgs │ │ ├── portaudio.h │ │ ├── comdll.cpp │ │ ├── flexasio.manifest │ │ ├── log.cpp │ │ ├── portaudio.cpp │ │ ├── CMakeLists.txt │ │ ├── config.h │ │ ├── control_panel.cpp │ │ ├── cflexasio.cpp │ │ ├── flexasio.h │ │ └── config.cpp │ ├── FlexASIOUtil │ │ ├── shell.h │ │ ├── windows_error.h │ │ ├── windows_string.h │ │ ├── windows_registry.h │ │ ├── windows_com.h │ │ ├── windows_registry.cpp │ │ ├── windows_com.cpp │ │ ├── shell.cpp │ │ ├── windows_error.cpp │ │ ├── CMakeLists.txt │ │ ├── windows_string.cpp │ │ ├── portaudio.h │ │ └── portaudio.cpp │ ├── FlexASIOTest │ │ ├── CMakeLists.txt │ │ └── main.cpp │ ├── CMakeModules │ │ └── Findtinytoml.cmake │ ├── PortAudioDevices │ │ ├── CMakeLists.txt │ │ └── list.cpp │ ├── versioninfo.rc │ └── CMakeLists.txt ├── portaudio_version_stamp.in.h ├── portaudio_version_stamp.cmake ├── check_git_submodule.cmake ├── installer.cmake ├── portaudio.cmake ├── installer.in.iss ├── README.md └── CMakeLists.txt ├── ASIO.jpg ├── .gitignore ├── windows ├── koordasio.bmp ├── koordasio-small.bmp ├── kdinstaller.iss └── deploy_windows.ps1 ├── .gitmodules ├── LICENSE.txt ├── README.md └── .github ├── autobuild ├── get_build_vars.py └── windows.ps1 └── workflows ├── continuous-integration.yml_WIP └── autobuild.yml /kdASIOVersion.txt: -------------------------------------------------------------------------------- 1 | VERSION = 2.1.1 -------------------------------------------------------------------------------- /src/kdasioconfig/.gitignore: -------------------------------------------------------------------------------- 1 | CMakeLists.txt.user* -------------------------------------------------------------------------------- /ASIO.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/ASIO.jpg -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/flexasio.rc.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define IDR_FLEXASIO 100 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2017 files 2 | .vs/ 3 | CMakeSettings.json 4 | out/ 5 | build-* -------------------------------------------------------------------------------- /src/kdasioconfig/mainicon.rc: -------------------------------------------------------------------------------- 1 | IDI_MAINICON ICON DISCARDABLE "mainicon.ico" 2 | -------------------------------------------------------------------------------- /windows/koordasio.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/windows/koordasio.bmp -------------------------------------------------------------------------------- /src/kdasioconfig/mainicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/mainicon.ico -------------------------------------------------------------------------------- /windows/koordasio-small.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/windows/koordasio-small.bmp -------------------------------------------------------------------------------- /src/kdasioconfig/images/BG_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/BG_01.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/Frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/Frame.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/Fader BTN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/Fader BTN.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/INFO BTN ON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/INFO BTN ON.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/Koord Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/Koord Logo.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/CONFIG BTN ON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/CONFIG BTN ON.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/INFO BTN OFF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/INFO BTN OFF.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/SHARED ON BTN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/SHARED ON BTN.png -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/flexasio.rc: -------------------------------------------------------------------------------- 1 | #include "flexasio.rc.h" 2 | 3 | IDR_FLEXASIO REGISTRY "flexasio.rgs" 4 | 5 | 1 TYPELIB "FlexASIO.tlb" 6 | -------------------------------------------------------------------------------- /src/kdasioconfig/images/CONFIG BTN OFF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/CONFIG BTN OFF.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/EXCLUSIVE ON BTN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/EXCLUSIVE ON BTN.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/KoordASIO Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/KoordASIO Logo.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/RoundedArrow4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/RoundedArrow4x.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/SHARED OFF BTN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/SHARED OFF BTN.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/mark-github-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/mark-github-24.png -------------------------------------------------------------------------------- /src/kdasioconfig/images/EXCLUSIVE OFF BTN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kormix-io/KoordASIO/HEAD/src/kdasioconfig/images/EXCLUSIVE OFF BTN.png -------------------------------------------------------------------------------- /src/portaudio_version_stamp.in.h: -------------------------------------------------------------------------------- 1 | // AUTOGENERATED BY portaudio_version_stamp.cmake 2 | 3 | #define PA_GIT_REVISION @PA_GIT_REVISION@-FlexASIO 4 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/shell.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace flexasio { 7 | std::wstring GetUserDirectory(); 8 | } -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/control_panel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace flexasio { 6 | 7 | void OpenControlPanel(HWND windowHandle); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/windows_error.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace flexasio { 8 | 9 | std::string GetWindowsErrorString(DWORD error); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/kdasioconfig/README.md: -------------------------------------------------------------------------------- 1 | ## KoordASIOConfig 2 | 3 | Basic Qt GUI configurator for KoordASIO driver. 4 | 5 | Hardcodes to WASAPI backend. 6 | 7 | Allows setting of: 8 | 9 | - Input / Output devices 10 | - Exclusive / Shared mode 11 | - Buffer size (samples) -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/windows_string.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace flexasio { 7 | 8 | std::string ConvertToUTF8(std::wstring_view); 9 | std::wstring ConvertFromUTF8(std::string_view); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/windows_registry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace flexasio { 8 | 9 | struct HKEYDeleter final { 10 | void operator()(HKEY hkey) const; 11 | }; 12 | using UniqueHKEY = std::unique_ptr, HKEYDeleter>; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace flexasio { 6 | 7 | // In performance-critical code paths, use IsLoggingEnabled() to avoid wasting time formatting a log message that will go nowhere. 8 | bool IsLoggingEnabled(); 9 | ::dechamps_cpplog::Logger Log(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/cflexasio.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct IASIO; 4 | 5 | // Used by FlexASIOTest to instantiate FlexASIO directly, instead of going through the ASIO Host SDK and COM. 6 | // In production, standard COM factory mechanisms are used to instantiate FlexASIO, not these functions. 7 | IASIO * CreateFlexASIO(); 8 | void ReleaseFlexASIO(IASIO *); 9 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/flexasio.idl: -------------------------------------------------------------------------------- 1 | [uuid(42DCBA0A-BA58-4BA7-918F-D34A121F9E8D)] 2 | library LFlexASIO 3 | { 4 | [object, uuid(40FAA6D2-9235-47C7-BBF0-472BE9AD1ECD)] 5 | interface IFlexASIO : IUnknown 6 | { 7 | }; 8 | 9 | [uuid(EE8F66BC-9646-4487-ACB9-0F1C7D68D1FA)] 10 | coclass CFlexASIO 11 | { 12 | [default] interface IFlexASIO; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/windows_com.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace flexasio { 6 | 7 | class COMInitializer final { 8 | public: 9 | COMInitializer(DWORD coInit); 10 | ~COMInitializer(); 11 | 12 | COMInitializer(const COMInitializer&) = delete; 13 | COMInitializer& operator=(const COMInitializer&) = delete; 14 | }; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/portaudio_version_stamp.cmake: -------------------------------------------------------------------------------- 1 | if (Git_FOUND) 2 | execute_process( 3 | COMMAND "${GIT_EXECUTABLE}" -C "${PORTAUDIO_LIST_DIR}" rev-parse HEAD 4 | OUTPUT_VARIABLE PA_GIT_REVISION OUTPUT_STRIP_TRAILING_WHITESPACE 5 | ) 6 | else() 7 | set(PA_GIT_REVISION "unknown") 8 | endif() 9 | configure_file("${CMAKE_CURRENT_LIST_DIR}/portaudio_version_stamp.in.h" "${OUTPUT_HEADER_FILE}" @ONLY) 10 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOTest/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(FlexASIOTest main.cpp ../versioninfo.rc) 2 | target_compile_definitions(FlexASIOTest PRIVATE PROJECT_DESCRIPTION="FlexASIO Self-test program") 3 | target_link_libraries(FlexASIOTest 4 | PRIVATE ASIOTest::ASIOTest 5 | PRIVATE KoordASIO 6 | PRIVATE dechamps_CMakeUtils_version_stamp 7 | ) 8 | 9 | install(TARGETS FlexASIOTest RUNTIME DESTINATION bin) 10 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOTest/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "..\FlexASIO\cflexasio.h" 4 | 5 | #include 6 | 7 | int main(int argc, char** argv) { 8 | auto* const asioDriver = CreateFlexASIO(); 9 | if (asioDriver == nullptr) abort(); 10 | 11 | const auto result = ::ASIOTest_RunTest(asioDriver, argc, argv); 12 | 13 | ReleaseFlexASIO(asioDriver); 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /src/flexasio/CMakeModules/Findtinytoml.cmake: -------------------------------------------------------------------------------- 1 | find_path(tinytoml_INCLUDE_DIR NAMES toml/toml.h) 2 | mark_as_advanced(tinytoml_INCLUDE_DIR) 3 | 4 | include(FindPackageHandleStandardArgs) 5 | find_package_handle_standard_args(tinytoml REQUIRED_VARS tinytoml_INCLUDE_DIR) 6 | 7 | if(tinytoml_FOUND AND NOT TARGET tinytoml) 8 | add_library(tinytoml INTERFACE) 9 | target_include_directories(tinytoml INTERFACE "${tinytoml_INCLUDE_DIR}") 10 | endif() 11 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/windows_registry.cpp: -------------------------------------------------------------------------------- 1 | #include "windows_registry.h" 2 | 3 | #include "windows_error.h" 4 | 5 | #include 6 | 7 | namespace flexasio { 8 | 9 | void HKEYDeleter::operator()(HKEY hkey) const { 10 | const auto regCloseKeyError = ::RegCloseKey(hkey); 11 | if (regCloseKeyError != ERROR_SUCCESS) 12 | throw std::runtime_error("Unable to close registry key: " + GetWindowsErrorString(regCloseKeyError)); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/flexasio/PortAudioDevices/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(PortAudioDevices list.cpp ../versioninfo.rc) 2 | target_compile_definitions(PortAudioDevices PRIVATE PROJECT_DESCRIPTION="PortAudio device list application") 3 | target_link_libraries(PortAudioDevices 4 | PRIVATE dechamps_CMakeUtils_version_stamp 5 | PRIVATE FlexASIOUtil_portaudio 6 | PRIVATE dechamps_cpputil::string 7 | PRIVATE PortAudio::PortAudio 8 | ) 9 | install(TARGETS PortAudioDevices RUNTIME DESTINATION bin) 10 | -------------------------------------------------------------------------------- /src/check_git_submodule.cmake: -------------------------------------------------------------------------------- 1 | function(check_git_submodule DIR) 2 | file(GLOB DIR_FILES "${DIR}/*") 3 | if (DIR_FILES STREQUAL "") 4 | message(WARNING "It looks like the '${DIR}' directory is empty. Did you forget to update git submodules?") 5 | find_package(Git MODULE) 6 | if (Git_FOUND) 7 | message(STATUS "Updating the '${DIR}' git submodule to fix.") 8 | execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${CMAKE_CURRENT_LIST_DIR}" submodule update --init -- "${DIR}") 9 | endif() 10 | endif() 11 | endfunction() 12 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/windows_com.cpp: -------------------------------------------------------------------------------- 1 | #include "windows_com.h" 2 | 3 | #include "windows_error.h" 4 | 5 | #include 6 | 7 | namespace flexasio { 8 | 9 | COMInitializer::COMInitializer(DWORD coInit) { 10 | const auto coInitializeResult = CoInitializeEx(NULL, coInit); 11 | if (FAILED(coInitializeResult)) 12 | throw std::runtime_error("Failed to initialize COM: " + GetWindowsErrorString(coInitializeResult)); 13 | } 14 | 15 | COMInitializer::~COMInitializer() { 16 | CoUninitialize(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/dll.def: -------------------------------------------------------------------------------- 1 | ; Module-definition file for the FlexASIO DLL 2 | ; Our DLL is a COM class factory, so we only need to export the functions required by COM. 3 | ; We can't use declspec(dllexport) for those because the naming convention doesn't match (leading "@") 4 | ; We also a couple of functions for users that want to instantiate FlexASIO directly (e.g. FlexASIOTest). 5 | 6 | LIBRARY 7 | 8 | EXPORTS 9 | DllCanUnloadNow PRIVATE 10 | DllGetClassObject PRIVATE 11 | DllRegisterServer PRIVATE 12 | DllUnregisterServer PRIVATE 13 | CreateFlexASIO 14 | ReleaseFlexASIO 15 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/shell.cpp: -------------------------------------------------------------------------------- 1 | #include "shell.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace flexasio { 7 | 8 | std::wstring GetUserDirectory() { 9 | PWSTR userDirectory = nullptr; 10 | const auto getKnownFolderPathHResult = ::SHGetKnownFolderPath(FOLDERID_Profile, 0, NULL, &userDirectory); 11 | if (getKnownFolderPathHResult != S_OK) 12 | throw std::system_error(getKnownFolderPathHResult, std::system_category(), "SHGetKnownFolderPath() failed"); 13 | const std::wstring userDirectoryString(userDirectory); 14 | ::CoTaskMemFree(userDirectory); 15 | return userDirectoryString; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/flexasio.rgs: -------------------------------------------------------------------------------- 1 | HKCR 2 | { 3 | ForceRemove koordasio.KoordASIO.1 = s 'KoordASIO' 4 | { 5 | CLSID = s '{EE8F66BC-9646-4487-ACB9-0F1C7D68D1FA}' 6 | } 7 | 8 | NoRemove CLSID 9 | { 10 | ForceRemove {EE8F66BC-9646-4487-ACB9-0F1C7D68D1FA} = s 'KoordASIO' 11 | { 12 | ProgID = s 'koordasio.KoordASIO.1' 13 | InprocServer32 = s '%MODULE%' 14 | { 15 | val ThreadingModel = s 'Both' 16 | } 17 | } 18 | } 19 | } 20 | 21 | HKLM 22 | { 23 | NoRemove SOFTWARE 24 | { 25 | NoRemove ASIO 26 | { 27 | ForceRemove KoordASIO 28 | { 29 | val CLSID = s '{EE8F66BC-9646-4487-ACB9-0F1C7D68D1FA}' 30 | val Description = s 'KoordASIO' 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/portaudio.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace flexasio { 8 | 9 | struct StreamDeleter { 10 | void operator()(PaStream*) throw(); 11 | }; 12 | using Stream = std::unique_ptr; 13 | Stream OpenStream(const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData); 14 | 15 | struct StreamStopper { 16 | void operator()(PaStream*) throw(); 17 | }; 18 | using ActiveStream = std::unique_ptr; 19 | ActiveStream StartStream(PaStream*); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/installer.cmake: -------------------------------------------------------------------------------- 1 | include(check_git_submodule.cmake) 2 | check_git_submodule(dechamps_CMakeUtils) 3 | 4 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/dechamps_CMakeUtils") 5 | find_package(InnoSetup MODULE REQUIRED) 6 | 7 | find_package(Git MODULE REQUIRED) 8 | set(DECHAMPS_CMAKEUTILS_GIT_DIR "${CMAKE_CURRENT_LIST_DIR}/flexasio") 9 | include(version/version) 10 | string(REGEX REPLACE "^flexasio-" "" FLEXASIO_VERSION "${DECHAMPS_CMAKEUTILS_GIT_DESCRIPTION_DIRTY}") 11 | 12 | configure_file("${CMAKE_CURRENT_LIST_DIR}/installer.in.iss" "${CMAKE_CURRENT_LIST_DIR}/out/installer.iss" @ONLY) 13 | include(execute_process_or_die) 14 | execute_process_or_die( 15 | COMMAND "${InnoSetup_iscc_EXECUTABLE}" out/installer.iss /Oout/installer /FFlexASIO-${FLEXASIO_VERSION} 16 | ) 17 | -------------------------------------------------------------------------------- /src/flexasio/versioninfo.rc: -------------------------------------------------------------------------------- 1 | #include // Required, otherwise VERSIONINFO doesn't work 2 | 3 | #include 4 | 5 | VS_VERSION_INFO VERSIONINFO 6 | { 7 | BLOCK "StringFileInfo" 8 | { 9 | BLOCK "04090000" // US. English, ASCII 10 | { 11 | VALUE "FileDescription", PROJECT_DESCRIPTION " (" BUILD_CONFIGURATION " " BUILD_PLATFORM ")" 12 | VALUE "LegalCopyright", "MIT License, Copyright (c) 2018 Etienne Dechamps " 13 | VALUE "ProductName", "FlexASIO https://github.com/dechamps/FlexASIO" 14 | VALUE "ProductVersion", DECHAMPS_CMAKEUTILS_GIT_DESCRIPTION_DIRTY " built on " DECHAMPS_CMAKEUTILS_BUILD_TIME 15 | } 16 | } 17 | BLOCK "VarFileInfo" 18 | { 19 | VALUE "Translation", 0x0409, 0x0000 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/comdll.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // This implements DLL entry points so that the automagic ATL module can do its thing. Nothing to see here, move along. 5 | 6 | class COMDLL : public CAtlDllModuleT { }; 7 | static COMDLL comdll; 8 | 9 | extern "C" BOOL WINAPI DllMain(HINSTANCE, DWORD dwReason, LPVOID lpReserved) { return comdll.DllMain(dwReason, lpReserved); } 10 | __control_entrypoint(DllExport) STDAPI DllCanUnloadNow(void) { return comdll.DllCanUnloadNow(); } 11 | _Check_return_ STDAPI DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, __deref_out LPVOID* ppv) { return comdll.DllGetClassObject(rclsid, riid, ppv); } 12 | STDAPI DllRegisterServer(void) { return comdll.DllRegisterServer(); } 13 | STDAPI DllUnregisterServer(void) { return comdll.DllUnregisterServer(); } 14 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/windows_error.cpp: -------------------------------------------------------------------------------- 1 | #include "windows_error.h" 2 | 3 | #include 4 | 5 | namespace flexasio { 6 | 7 | std::string GetWindowsErrorString(DWORD error) { 8 | std::string message(4096, 0); 9 | auto messageSize = ::FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, error, 0, message.data(), DWORD(message.size()), NULL); 10 | if (messageSize <= 0 || messageSize >= message.size()) { 11 | message = "failed to format error message - result " + std::to_string(messageSize) + ", error " + std::to_string(GetLastError()) + ")"; 12 | } 13 | else { 14 | for (; messageSize > 0 && isspace(static_cast(message[messageSize - 1])); --messageSize); 15 | message.resize(messageSize); 16 | } 17 | 18 | return "Windows error code " + std::to_string(error) + " \"" + message + "\""; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/kdasioconfig/koordipics.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/BG_01.png 4 | images/CONFIG BTN OFF.png 5 | images/CONFIG BTN ON.png 6 | images/EXCLUSIVE OFF BTN.png 7 | images/EXCLUSIVE ON BTN.png 8 | images/Fader BTN.png 9 | images/Frame.png 10 | images/INFO BTN OFF.png 11 | images/INFO BTN ON.png 12 | images/Koord Logo.png 13 | images/KoordASIO Logo.png 14 | images/mark-github-24.png 15 | images/SHARED OFF BTN.png 16 | images/SHARED ON BTN.png 17 | images/RoundedArrow4x.png 18 | mainicon.ico 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(FlexASIOUtil_portaudio STATIC portaudio.cpp) 2 | target_link_libraries(FlexASIOUtil_portaudio 3 | PUBLIC PortAudio::PortAudio 4 | PRIVATE dechamps_cpputil::string 5 | ) 6 | 7 | add_library(FlexASIOUtil_shell STATIC shell.cpp) 8 | 9 | add_library(FlexASIOUtil_windows_com STATIC windows_com.cpp) 10 | target_link_libraries(FlexASIOUtil_windows_com 11 | PRIVATE FlexASIOUtil_windows_error 12 | ) 13 | 14 | add_library(FlexASIOUtil_windows_error STATIC windows_error.cpp) 15 | 16 | add_library(FlexASIOUtil_windows_registry STATIC windows_registry.cpp) 17 | target_link_libraries(FlexASIOUtil_windows_registry 18 | PRIVATE FlexASIOUtil_windows_error 19 | ) 20 | 21 | add_library(FlexASIOUtil_windows_string STATIC windows_string.cpp) 22 | target_link_libraries(FlexASIOUtil_windows_string 23 | PRIVATE FlexASIOUtil_windows_error 24 | ) 25 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/flexasio.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/tinytoml"] 2 | path = src/tinytoml 3 | url = https://github.com/mayah/tinytoml.git 4 | [submodule "src/portaudio"] 5 | path = src/portaudio 6 | url = https://github.com/PortAudio/portaudio.git 7 | [submodule "src/cxxopts"] 8 | path = src/cxxopts 9 | url = https://github.com/jarro2783/cxxopts.git 10 | [submodule "src/libsndfile"] 11 | path = src/libsndfile 12 | url = https://github.com/erikd/libsndfile.git 13 | [submodule "src/dechamps_cpputil"] 14 | path = src/dechamps_cpputil 15 | url = https://github.com/dechamps/cpputil.git 16 | [submodule "src/dechamps_cpplog"] 17 | path = src/dechamps_cpplog 18 | url = https://github.com/dechamps/cpplog.git 19 | [submodule "src/dechamps_ASIOUtil"] 20 | path = src/dechamps_ASIOUtil 21 | url = https://github.com/dechamps/ASIOUtil.git 22 | [submodule "src/ASIOTest"] 23 | path = src/ASIOTest 24 | url = https://github.com/dechamps/ASIOTest.git 25 | [submodule "src/dechamps_CMakeUtils"] 26 | path = src/dechamps_CMakeUtils 27 | url = https://github.com/dechamps/CMakeUtils.git 28 | [submodule "src/kdasioconfig/singleapplication"] 29 | path = src/kdasioconfig/singleapplication 30 | url = https://github.com/itay-grudev/SingleApplication.git 31 | -------------------------------------------------------------------------------- /src/portaudio.cmake: -------------------------------------------------------------------------------- 1 | # These CMake commands will be injected into the PortAudio CMake build. 2 | 3 | set(FLEXASIO_LIST_DIR "${CMAKE_CURRENT_LIST_DIR}") 4 | 5 | # Due to what looks like scoping issues, there are some things that can't be 6 | # done directly from here, such as adding dependencies to source files. 7 | # We inject ourselves into the CMake add_library() function (which does run 8 | # into the proper scope) to work around that. 9 | function(ADD_LIBRARY) 10 | _ADD_LIBRARY(${ARGV}) 11 | if("${FLEXASIO_INJECTED}") 12 | return() 13 | endif() 14 | set(FLEXASIO_INJECTED 1 PARENT_SCOPE) 15 | message(STATUS "Running FlexASIO portaudio.cmake injection") 16 | 17 | find_package(Git) 18 | set(FLEXASIO_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/flexasio_version_stamp.h") 19 | add_custom_target(flexasio_version_stamp_gen 20 | COMMAND "${CMAKE_COMMAND}" 21 | -DPORTAUDIO_LIST_DIR="${CMAKE_CURRENT_LIST_DIR}" 22 | -DOUTPUT_HEADER_FILE="${FLEXASIO_VERSION_FILE}" 23 | -DGit_FOUND="${Git_FOUND}" 24 | -DGIT_EXECUTABLE="${GIT_EXECUTABLE}" 25 | -P "${FLEXASIO_LIST_DIR}/portaudio_version_stamp.cmake" 26 | ) 27 | set_property(SOURCE src/common/pa_front.c APPEND PROPERTY OBJECT_DEPENDS flexasio_version_stamp_gen) 28 | set_property(SOURCE src/common/pa_front.c APPEND PROPERTY COMPILE_OPTIONS "/FI${FLEXASIO_VERSION_FILE}") 29 | endfunction() 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | NOTE: The license below only applies to the source code of FlexASIO itself. the 2 | ASIO trademark and ASIO SDK are subject to specific license terms; see 3 | "Steinberg ASIO Licensing Agreement" in the ASIO SDK. 4 | 5 | ASIO is a trademark and software of Steinberg Media Technologies GmbH. 6 | 7 | --- 8 | 9 | Copyright (c) 2018 Etienne Dechamps 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. -------------------------------------------------------------------------------- /src/kdasioconfig/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(kdasioconfig VERSION 0.1 LANGUAGES CXX) 4 | 5 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 6 | 7 | set(CMAKE_AUTOUIC ON) 8 | set(CMAKE_AUTOMOC ON) 9 | set(CMAKE_AUTORCC ON) 10 | 11 | set(CMAKE_CXX_STANDARD 11) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | 14 | enable_language("RC") 15 | 16 | find_package(QT NAMES Qt6 COMPONENTS Widgets REQUIRED) 17 | find_package(Qt6 COMPONENTS Widgets REQUIRED) 18 | find_package(Qt6 COMPONENTS Multimedia REQUIRED) 19 | 20 | set(PROJECT_SOURCES 21 | main.cpp 22 | kdasioconfig.cpp 23 | kdasioconfig.h 24 | kdasioconfigbase.ui 25 | ) 26 | 27 | set(app_icon_resource_windows "${CMAKE_CURRENT_SOURCE_DIR}/mainicon.rc") 28 | 29 | qt_add_executable(kdasioconfig 30 | WIN32 31 | MANUAL_FINALIZATION 32 | ${PROJECT_SOURCES} 33 | koordipics.qrc 34 | ${app_icon_resource_windows} 35 | ) 36 | 37 | set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") 38 | add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/singleapplication") 39 | 40 | target_link_libraries(kdasioconfig PRIVATE SingleApplication::SingleApplication) 41 | 42 | target_link_libraries(kdasioconfig PRIVATE Qt6::Widgets) 43 | target_link_libraries(kdasioconfig PRIVATE Qt6::Multimedia) 44 | 45 | set_target_properties(kdasioconfig PROPERTIES 46 | OUTPUT_NAME "KoordASIOControl" 47 | WIN32_EXECUTABLE TRUE 48 | ) 49 | 50 | qt_finalize_executable(kdasioconfig) 51 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/windows_string.cpp: -------------------------------------------------------------------------------- 1 | #include "windows_string.h" 2 | 3 | #include "windows_error.h" 4 | 5 | #include 6 | 7 | #include 8 | 9 | namespace flexasio { 10 | 11 | std::string ConvertToUTF8(std::wstring_view input) { 12 | if (input.size() == 0) return {}; 13 | 14 | const auto size = ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, input.data(), static_cast(input.size()), NULL, 0, NULL, NULL); 15 | if (size <= 0) throw std::runtime_error("Unable to get size for string conversion to UTF-8: " + GetWindowsErrorString(::GetLastError())); 16 | 17 | std::string result(size, 0); 18 | if (::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, input.data(), int(input.size()), result.data(), int(result.size()), NULL, NULL) != int(result.size())) 19 | throw std::runtime_error("Unable to convert string to UTF-8: " + GetWindowsErrorString(::GetLastError())); 20 | return result; 21 | } 22 | 23 | std::wstring ConvertFromUTF8(std::string_view input) { 24 | if (input.size() == 0) return {}; 25 | 26 | const auto size = ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, input.data(), int(input.size()), NULL, 0); 27 | if (size <= 0) throw std::runtime_error("Unable to get size for string conversion from UTF-8: " + GetWindowsErrorString(::GetLastError())); 28 | 29 | std::wstring result(size, 0); 30 | if (::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, input.data(), int(input.size()), result.data(), int(result.size())) != int(result.size())) 31 | throw std::runtime_error("Unable to convert string from UTF-8: " + GetWindowsErrorString(::GetLastError())); 32 | return result; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/installer.in.iss: -------------------------------------------------------------------------------- 1 | ; Note: this Inno Setup installer script is meant to run as part of 2 | ; installer.cmake. It will not work on its own. 3 | ; 4 | ; Inno Setup 6 or later is required for this script to work. 5 | 6 | [Setup] 7 | AppID=FlexASIO 8 | AppName=FlexASIO 9 | AppVerName=FlexASIO @FLEXASIO_VERSION@ 10 | AppVersion=@FLEXASIO_VERSION@ 11 | AppPublisher=Etienne Dechamps 12 | AppPublisherURL=https://github.com/dechamps/FlexASIO 13 | AppSupportURL=https://github.com/dechamps/FlexASIO/issues 14 | AppUpdatesURL=https://github.com/dechamps/FlexASIO/releases 15 | AppReadmeFile=https://github.com/dechamps/FlexASIO/blob/@DECHAMPS_CMAKEUTILS_GIT_DESCRIPTION@/README.md 16 | AppContact=etienne@edechamps.fr 17 | WizardStyle=modern 18 | 19 | DefaultDirName={autopf}\FlexASIO 20 | AppendDefaultDirName=no 21 | ArchitecturesInstallIn64BitMode=x64 22 | 23 | [Files] 24 | Source:"install\x64-Release\bin\FlexASIO.dll"; DestDir: "{app}\x64"; Flags: ignoreversion regserver 64bit; Check: Is64BitInstallMode 25 | Source:"install\x64-Release\bin\*"; DestDir: "{app}\x64"; Flags: ignoreversion 64bit; Check: Is64BitInstallMode 26 | Source:"install\x86-Release\bin\FlexASIO.dll"; DestDir: "{app}\x86"; Flags: ignoreversion regserver 27 | Source:"install\x86-Release\bin\*"; DestDir: "{app}\x86"; Flags: ignoreversion 28 | Source:"..\..\*.txt"; DestDir:"{app}"; Flags: ignoreversion 29 | Source:"..\..\*.md"; DestDir:"{app}"; Flags: ignoreversion 30 | Source:"..\..\*.jpg"; DestDir:"{app}"; Flags: ignoreversion 31 | 32 | [Run] 33 | Filename:"https://github.com/dechamps/FlexASIO/blob/@DECHAMPS_CMAKEUTILS_GIT_DESCRIPTION@/README.md"; Description:"Open README"; Flags: postinstall shellexec nowait skipifsilent 34 | -------------------------------------------------------------------------------- /src/flexasio/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | project(FlexASIO DESCRIPTION "FlexASIO Universal ASIO Driver") 3 | 4 | if(CMAKE_SIZEOF_VOID_P EQUAL 4) 5 | set(FLEXASIO_PLATFORM x86) 6 | elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) 7 | set(FLEXASIO_PLATFORM x64) 8 | else() 9 | set(FLEXASIO_PLATFORM unknown) 10 | endif() 11 | 12 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMakeModules") 13 | find_package(tinytoml MODULE REQUIRED) 14 | find_package(PortAudio CONFIG REQUIRED) 15 | find_package(dechamps_cpplog CONFIG REQUIRED) 16 | find_package(dechamps_cpputil CONFIG REQUIRED) 17 | find_package(dechamps_ASIOUtil CONFIG REQUIRED) 18 | find_package(ASIOTest CONFIG REQUIRED) 19 | 20 | set(CMAKE_CXX_STANDARD 17) 21 | add_compile_options( 22 | /external:anglebrackets /WX /W4 /external:W0 /permissive- /analyze /analyze:external- 23 | 24 | # Suppress warnings about shadowing declarations. 25 | # 26 | # In most cases, this happens when a lambda is used to initialize some 27 | # variable, and the lambda declares a local variable with the same name as the 28 | # variable it's tasked with initializing. In such cases the shadowing is 29 | # actually desirable, because it prevents one from accidentally using the (not 30 | # yet initialized) outer variable instead of the (valid) local variable within 31 | # the lambda. 32 | /wd4458 /wd4456 33 | ) 34 | add_definitions( 35 | -DBUILD_CONFIGURATION="$" 36 | -DBUILD_PLATFORM="${FLEXASIO_PLATFORM}" 37 | ) 38 | 39 | add_subdirectory(../dechamps_CMakeUtils/version version EXCLUDE_FROM_ALL) 40 | 41 | add_subdirectory(FlexASIOUtil EXCLUDE_FROM_ALL) 42 | add_subdirectory(FlexASIO) 43 | add_subdirectory(FlexASIOTest) 44 | add_subdirectory(PortAudioDevices) 45 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/log.cpp: -------------------------------------------------------------------------------- 1 | #include "log.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "../FlexASIOUtil/shell.h" 9 | 10 | namespace flexasio { 11 | 12 | namespace { 13 | 14 | class FlexASIOLogSink final : public ::dechamps_cpplog::LogSink { 15 | public: 16 | static std::unique_ptr Open() { 17 | std::filesystem::path path; 18 | try { 19 | path = GetUserDirectory(); 20 | } 21 | catch (...) { 22 | return nullptr; 23 | } 24 | // path.append("KoordASIO-builtin.log"); 25 | path.append("KoordASIO.log"); 26 | 27 | if (!std::filesystem::exists(path)) return nullptr; 28 | 29 | return std::make_unique(path); 30 | } 31 | 32 | static FlexASIOLogSink* Get() { 33 | static const auto output = Open(); 34 | return output.get(); 35 | } 36 | 37 | FlexASIOLogSink(const std::filesystem::path& path) : file_sink(path) { 38 | ::dechamps_cpplog::Logger(this) << "FlexASIO " << BUILD_CONFIGURATION << " " << BUILD_PLATFORM << " " << ::dechamps_CMakeUtils_gitDescriptionDirty << " built on " << ::dechamps_CMakeUtils_buildTime; 39 | } 40 | 41 | void Write(const std::string_view str) override { return preamble_sink.Write(str); } 42 | 43 | private: 44 | ::dechamps_cpplog::FileLogSink file_sink; 45 | ::dechamps_cpplog::ThreadSafeLogSink thread_safe_sink{ file_sink }; 46 | ::dechamps_cpplog::PreambleLogSink preamble_sink{ thread_safe_sink }; 47 | }; 48 | 49 | } 50 | 51 | bool IsLoggingEnabled() { return FlexASIOLogSink::Get() != nullptr; } 52 | ::dechamps_cpplog::Logger Log() { return ::dechamps_cpplog::Logger(FlexASIOLogSink::Get()); } 53 | 54 | } -------------------------------------------------------------------------------- /windows/kdinstaller.iss: -------------------------------------------------------------------------------- 1 | ; Inno Setup 6 or later is required for this script to work. 2 | 3 | [Setup] 4 | AppID=KoordASIO 5 | AppName=KoordASIO 6 | AppVerName=KoordASIO 7 | AppVersion={#ApplicationVersion} 8 | VersionInfoVersion={#ApplicationVersion} 9 | AppPublisher=Koord.Live 10 | AppPublisherURL=https://github.com/koord-live/KoordASIO 11 | AppSupportURL=https://github.com/koord-live/KoordASIO/issues 12 | AppUpdatesURL=https://github.com/koord-live/KoordASIO/releases 13 | AppContact=contact@koord.live 14 | WizardStyle=modern 15 | DefaultGroupName=KoordASIO 16 | DefaultDirName={autopf}\KoordASIO 17 | AppendDefaultDirName=no 18 | ArchitecturesInstallIn64BitMode=x64 19 | ; disk space isn't calculated accurately - set here to 29Mb x 1024 x 1024 bytes 20 | ExtraDiskSpaceRequired=30408704 21 | 22 | ; for 100% dpi setting should be 164x314 - https://jrsoftware.org/ishelp/ 23 | WizardImageFile=windows\koordasio.bmp 24 | ; for 100% dpi setting should be 55x55 25 | WizardSmallImageFile=windows\koordasio-small.bmp 26 | 27 | [Files] 28 | Source:"deploy\x86_64\KoordASIO.dll"; DestDir: "{app}"; Flags: ignoreversion regserver 64bit; Check: Is64BitInstallMode 29 | ; install everything else in deploy dir, including portaudio.dll, KoordASIOControl.exe and all Qt dll deps 30 | Source:"deploy\x86_64\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs 64bit; Check: Is64BitInstallMode 31 | 32 | [Icons] 33 | Name: "{group}\KoordASIO Control"; Filename: "{app}\KoordASIOControl.exe"; WorkingDir: "{app}" 34 | 35 | [Run] 36 | ; make sure we have SOME working default configuration after installation 37 | Filename: "{app}\KoordASIOControl.exe"; Parameters: "-defaults"; Description: "Set KoordASIO defaults"; Flags: nowait 38 | ; also allow user to configure immediately after installation 39 | Filename: "{app}\KoordASIOControl.exe"; Description: "Run KoordASIO Control"; Flags: postinstall nowait skipifsilent 40 | 41 | ; install reg key to locate KoordASIOControl at runtime 42 | [Registry] 43 | Root: HKLM64; Subkey: "Software\Koord"; Flags: uninsdeletekeyifempty 44 | Root: HKLM64; Subkey: "Software\Koord\KoordASIO"; Flags: uninsdeletekey 45 | Root: HKLM64; Subkey: "Software\Koord\KoordASIO\Install"; ValueType: string; ValueName: "InstallPath"; ValueData: "{app}" 46 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # FlexASIO Developer Information 2 | 3 | [![.github/workflows/continuous-integration.yml](https://github.com/dechamps/FlexASIO/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/dechamps/FlexASIO/actions/workflows/continuous-integration.yml) 4 | 5 | See `LICENSE.txt` for licensing information. In particular, do note that 6 | specific license terms apply to the ASIO trademark and ASIO SDK. 7 | 8 | ## Building 9 | 10 | FlexASIO is designed to be built using CMake within the Microsoft Visual C++ 11 | 2019/2022 toolchain native CMake support. 12 | 13 | FlexASIO uses a CMake "superbuild" system (in `/src`) to automatically build the 14 | dependencies (most notably [PortAudio][]) before building FlexASIO itself. These 15 | dependencies are pulled in as git submodules. 16 | 17 | It is strongly recommended to use the superbuild system. Providing dependencies 18 | manually is quite tedious because FlexASIO uses a highly modular structure that 19 | relies on many small subprojects. 20 | 21 | Note that the ASIOUtil build system will download the [ASIO SDK][] for you 22 | automatically at configure time. 23 | 24 | ## Packaging 25 | 26 | The following command will generate the installer package for you: 27 | 28 | ``` 29 | cmake -P installer.cmake 30 | ``` 31 | 32 | Note that for this command to work: 33 | 34 | - You need to have [Inno Setup][] installed. 35 | - You need to have built FlexASIO in the `x64-Release` and `x86-Release` 36 | Visual Studio configurations first. 37 | 38 | **Note:** instead of running `installer.cmake` manually, it is often preferable 39 | to let the FlexASIO GitHub Actions workflow build the installer, as that 40 | guarantees a clean build. 41 | 42 | ## Troubleshooting 43 | 44 | ### VC runtime DLLs are not included in the installation 45 | 46 | See this [Visual Studio 2019 bug][InstallRequiredSystemLibraries]. 47 | 48 | --- 49 | 50 | *ASIO is a trademark and software of Steinberg Media Technologies GmbH* 51 | 52 | [ASIO SDK]: http://www.steinberg.net/en/company/developer.html 53 | [Inno Setup]: http://www.jrsoftware.org/isdl.php 54 | [InstallRequiredSystemLibraries]: https://developercommunity.visualstudio.com/content/problem/618084/cmake-installrequiredsystemlibraries-broken-in-lat.html 55 | [PortAudio]: http://www.portaudio.com/ 56 | [tinytoml]: https://github.com/mayah/tinytoml 57 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/portaudio.cpp: -------------------------------------------------------------------------------- 1 | #include "portaudio.h" 2 | 3 | #include "log.h" 4 | #include "../FlexASIOUtil/portaudio.h" 5 | 6 | namespace flexasio { 7 | 8 | void StreamDeleter::operator()(PaStream* stream) throw() { 9 | Log() << "Closing PortAudio stream " << stream; 10 | const auto error = Pa_CloseStream(stream); 11 | if (error != paNoError) 12 | Log() << "Unable to close PortAudio stream: " << Pa_GetErrorText(error); 13 | } 14 | 15 | Stream OpenStream(const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData) { 16 | Log() << "Opening PortAudio stream with..."; 17 | Log() << "...input parameters: " << (inputParameters == nullptr ? "none" : DescribeStreamParameters(*inputParameters)); 18 | Log() << "...output parameters: " << (outputParameters == nullptr ? "none" : DescribeStreamParameters(*outputParameters)); 19 | Log() << "...sample rate: " << sampleRate << " Hz"; 20 | Log() << "...frames per buffer: " << framesPerBuffer; 21 | Log() << "...stream flags: " << GetStreamFlagsString(streamFlags); 22 | Log() << "...stream callback: " << streamCallback << " (user data " << userData << ")"; 23 | PaStream* stream = nullptr; 24 | const auto error = Pa_OpenStream(&stream, inputParameters, outputParameters, sampleRate, framesPerBuffer, streamFlags, streamCallback, userData); 25 | if (error != paNoError) throw std::runtime_error(std::string("unable to open PortAudio stream: ") + Pa_GetErrorText(error)); 26 | if (stream == nullptr)throw std::runtime_error("Pa_OpenStream() unexpectedly returned null"); 27 | Log() << "PortAudio stream opened: " << stream; 28 | return Stream(stream); 29 | } 30 | 31 | void StreamStopper::operator()(PaStream* stream) throw() { 32 | Log() << "Stopping PortAudio stream " << stream; 33 | const auto error = Pa_StopStream(stream); 34 | if (error != paNoError) 35 | Log() << "Unable to stop PortAudio stream: " << Pa_GetErrorText(error); 36 | } 37 | 38 | ActiveStream StartStream(PaStream* const stream) { 39 | Log() << "Starting PortAudio stream " << stream; 40 | const auto error = Pa_StartStream(stream); 41 | if (error != paNoError) throw std::runtime_error(std::string("unable to start PortAudio stream: ") + Pa_GetErrorText(error)); 42 | Log() << "PortAudio stream started"; 43 | return ActiveStream(stream); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/portaudio.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | namespace flexasio { 14 | 15 | class PortAudioDebugRedirector final { 16 | public: 17 | using Write = std::function; 18 | 19 | explicit PortAudioDebugRedirector(Write write); 20 | ~PortAudioDebugRedirector(); 21 | 22 | private: 23 | static void DebugPrint(const char*); 24 | 25 | static Write write; 26 | }; 27 | 28 | std::string GetHostApiTypeIdString(PaHostApiTypeId hostApiTypeId); 29 | std::string GetSampleFormatString(PaSampleFormat sampleFormat); 30 | std::string GetStreamFlagsString(PaStreamFlags streamFlags); 31 | std::string GetWasapiFlagsString(PaWasapiFlags wasapiFlags); 32 | std::string GetWasapiThreadPriorityString(PaWasapiThreadPriority threadPriority); 33 | std::string GetWasapiStreamCategoryString(PaWasapiStreamCategory streamCategory); 34 | std::string GetWasapiStreamOptionString(PaWasapiStreamOption streamOption); 35 | std::string GetStreamCallbackFlagsString(PaStreamCallbackFlags streamCallbackFlags); 36 | 37 | struct HostApi { 38 | explicit HostApi(PaHostApiIndex index) : index(index), info(GetInfo(index)) {} 39 | 40 | const PaHostApiIndex index; 41 | const PaHostApiInfo& info; 42 | 43 | friend std::ostream& operator<<(std::ostream&, const HostApi&); 44 | 45 | private: 46 | static const PaHostApiInfo& GetInfo(PaHostApiIndex index); 47 | }; 48 | 49 | struct Device { 50 | explicit Device(PaDeviceIndex index) : index(index), info(GetInfo(index)) {} 51 | 52 | const PaDeviceIndex index; 53 | const PaDeviceInfo& info; 54 | 55 | friend std::ostream& operator<<(std::ostream&, const Device&); 56 | 57 | private: 58 | static const PaDeviceInfo& GetInfo(PaDeviceIndex index); 59 | }; 60 | 61 | WAVEFORMATEXTENSIBLE GetWasapiDeviceDefaultFormat(PaDeviceIndex index); 62 | WAVEFORMATEXTENSIBLE GetWasapiDeviceMixFormat(PaDeviceIndex index); 63 | 64 | std::string GetWaveFormatTagString(WORD formatTag); 65 | std::string GetWaveFormatChannelMaskString(DWORD channelMask); 66 | std::string GetWaveSubFormatString(const GUID& subFormat); 67 | std::string DescribeWaveFormat(const WAVEFORMATEXTENSIBLE& waveFormatExtensible); 68 | 69 | std::string DescribeStreamParameters(const PaStreamParameters& parameters); 70 | std::string DescribeStreamInfo(const PaStreamInfo& info); 71 | 72 | std::string DescribeStreamCallbackTimeInfo(const PaStreamCallbackTimeInfo& streamCallbackTimeInfo); 73 | 74 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KoordASIO, a user-friendly universal ASIO driver 2 | 3 | **If you are looking for an installer, see the 4 | [GitHub releases page][releases], or install directly from the [Microsoft App Store][windowstore].** 5 | 6 | ## Description 7 | 8 | KoordASIO is a universal ASIO driver, meaning that it is not tied to 9 | specific audio hardware. 10 | You can use it with any audio hardware that doesn't come with its own drivers, 11 | or where you need features that aren't available with your bundled ASIO drivers. 12 | 13 | ![KoordASIOScreenshot1a](https://user-images.githubusercontent.com/584572/184341896-1544a755-ebed-466f-b61e-e1d82c4530af.png) 14 | 15 | KoordASIO is a clone of the powerful FlexASIO project, but with the addition of an 16 | intuitive Control GUI that gives the user an easy way to use all the power of 17 | FlexASIO without any technical knowledge. FlexASIO itself has a wide array of 18 | options, but KoordASIO focuses on simplicity and low-latency configuration, 19 | giving the user the choice of WASAPI Shared Mode (to mix ASIO audio with other 20 | application audio) and WASAPI Exclusive Mode (locks out non-ASIO audio, ensuring 21 | lowest-latency, bit-perfect operation). 22 | 23 | ## Requirements 24 | 25 | - Windows Vista or later 26 | - Compatible 64-bit ASIO Host Applications 27 | 28 | ## Usage 29 | 30 | After running the [installer][releases], KoordASIO should appear in the ASIO 31 | driver list of any ASIO Host Application (e.g. Ableton, Cubase, Reaper). The Control 32 | GUI (KoordASIOControl.exe) can be launched at any time by clicking on the "ASIO Setup" 33 | button in your host software, or as usual via the Windows launcher. 34 | 35 | The default settings are as follows: 36 | 37 | - WASAPI [Shared Mode][BACKENDS] 38 | - Uses the Windows default recording and playback audio devices 39 | - 32-bit float sample type 40 | - 32-sample buffer size 41 | - Minimum "suggested" latency 42 | 43 | The KoordASIO Control GUI lets you select your Input/Output audio devices (with 44 | a link to the relevant Windows control panel), choose between Shared or 45 | Exclusive mode, and to change the Buffer Size in steps between 32 and 2048 samples. 46 | 47 | ## Troubleshooting 48 | Hopefully KoordASIO should work seamlessly out-of-the-box for you. If you do notice 49 | problems, please create an Issue at [the Issues page][issues] or send an email to 50 | music@koord.live. 51 | 52 | --- 53 | 54 | *ASIO is a trademark and software of Steinberg Media Technologies GmbH* 55 | 56 | [releases]: https://github.com/koord-live/KoordASIO/releases 57 | [issues]: https://github.com/koord-live/KoordASIO/issues 58 | [BACKENDS]: BACKENDS.md 59 | [windowstore]: https://apps.microsoft.com/store/detail/koordasio-universal-driver/XP9CSS6NZBDV21 60 | -------------------------------------------------------------------------------- /.github/autobuild/get_build_vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This script is trigged from the Github Autobuild workflow. 3 | # It analyzes kdASIOVersion.txt and git push details (tag vs. branch, etc.) to decide 4 | # - whether a release should be created, 5 | # - whether it is a pre-release, and 6 | # - what its title should be. 7 | 8 | import os 9 | import re 10 | import subprocess 11 | 12 | REPO_PATH = os.path.join(os.path.dirname(__file__), '..', '..') 13 | 14 | # get the koordasio version from the version file 15 | def get_koordasio_version(): 16 | koordasio_version = "" 17 | with open (REPO_PATH + '/kdASIOVersion.txt','r') as f: 18 | ver_content = f.read() 19 | ver_content = ver_content.replace('\r','') 20 | ver_lines = ver_content.split('\n') 21 | for line in ver_lines: 22 | line = line.strip() 23 | VERSION_LINE_STARTSWITH = 'VERSION = ' 24 | if line.startswith(VERSION_LINE_STARTSWITH): 25 | koordasio_version = line[len(VERSION_LINE_STARTSWITH):] 26 | return koordasio_version 27 | return "UNKNOWN_VERSION" 28 | 29 | 30 | def get_git_hash(): 31 | return subprocess.check_output([ 32 | 'git', 33 | 'describe', 34 | '--match=xxxxxxxxxxxxxxxxxxxx', 35 | '--always', 36 | '--abbrev', 37 | '--dirty' 38 | ]).decode('ascii').strip() 39 | 40 | 41 | def get_build_version(koordasio_version): 42 | if "dev" in koordasio_version: 43 | version = "{}-{}".format(koordasio_version, get_git_hash()) 44 | return 'intermediate', version 45 | 46 | version = koordasio_version 47 | return 'release', version 48 | 49 | 50 | def set_github_variable(varname, varval): 51 | print("{}={}".format(varname, varval)) # console output 52 | outputfile = os.getenv('GITHUB_OUTPUT') 53 | with open(outputfile, "a") as ghout: 54 | ghout.write(f"{varname}={varval}\n") 55 | 56 | 57 | koordasio_version = get_koordasio_version() 58 | set_github_variable("KOORDASIO_VERSION", koordasio_version) 59 | build_type, build_version = get_build_version(koordasio_version) 60 | print(f'building a version of type "{build_type}": {build_version}') 61 | 62 | fullref = os.environ['GITHUB_REF'] 63 | publish_to_release = bool(re.match(r'^refs/tags/r\d+_\d+_\d+\S*$', fullref)) 64 | 65 | # BUILD_VERSION is required for all builds including branch pushes 66 | # and PRs: 67 | set_github_variable("BUILD_VERSION", build_version) 68 | 69 | # PUBLISH_TO_RELEASE is always required as the workflow decides about further 70 | # steps based on this. It will only be true for tag pushes with a tag 71 | # starting with "r". 72 | set_github_variable("PUBLISH_TO_RELEASE", str(publish_to_release).lower()) 73 | 74 | if publish_to_release: 75 | reflist = fullref.split("/", 2) 76 | release_tag = reflist[2] 77 | release_title = f"Release {build_version} ({release_tag})" 78 | is_prerelease = not re.match(r'^r\d+_\d+_\d+$', release_tag) 79 | if not is_prerelease and build_version != release_tag[1:].replace('_', '.'): 80 | raise Exception(f"non-pre-release tag {release_tag} doesn't match kdASIOVersion.txt VERSION = {build_version}") 81 | 82 | # Those variables are only used when a release is created at all: 83 | set_github_variable("IS_PRERELEASE", str(is_prerelease).lower()) 84 | set_github_variable("RELEASE_TITLE", release_title) 85 | set_github_variable("RELEASE_TAG", release_tag) 86 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(CMAKE_SIZEOF_VOID_P EQUAL 4) 2 | set(FLEXASIO_MIDL_ENV_FLAG /env win32) 3 | elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) 4 | set(FLEXASIO_MIDL_ENV_FLAG /env amd64) 5 | else() 6 | set(FLEXASIO_MIDL_ENV_FLAG) 7 | endif() 8 | 9 | add_custom_command( 10 | OUTPUT flexasio_h.h flexasio_i.c flexasio.tlb 11 | COMMAND midl /nologo /header flexasio_h.h ${FLEXASIO_MIDL_ENV_FLAG} "${CMAKE_CURRENT_LIST_DIR}/flexasio.idl" 12 | MAIN_DEPENDENCY flexasio.idl 13 | ) 14 | add_library(FlexASIO_idl INTERFACE) 15 | target_sources(FlexASIO_idl 16 | INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/flexasio_h.h" 17 | INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/flexasio.tlb" 18 | ) 19 | target_include_directories(FlexASIO_idl INTERFACE "${CMAKE_CURRENT_BINARY_DIR}") 20 | 21 | add_library(FlexASIO_cflexasio STATIC EXCLUDE_FROM_ALL cflexasio.cpp) 22 | target_link_libraries(FlexASIO_cflexasio 23 | PRIVATE FlexASIO_flexasio 24 | PRIVATE FlexASIO_idl 25 | PRIVATE FlexASIO_log 26 | PRIVATE FlexASIOUtil_shell 27 | PRIVATE dechamps_cpputil::exception 28 | PRIVATE dechamps_ASIOUtil::asiosdk_iasiodrv 29 | PRIVATE dechamps_ASIOUtil::asio 30 | ) 31 | 32 | add_library(FlexASIO_comdll STATIC EXCLUDE_FROM_ALL comdll.cpp) 33 | target_compile_definitions(FlexASIO_comdll PRIVATE _WINDLL) 34 | 35 | add_library(FlexASIO_config STATIC EXCLUDE_FROM_ALL config.cpp) 36 | target_link_libraries(FlexASIO_config 37 | PRIVATE FlexASIO_log 38 | PRIVATE FlexASIOUtil_shell 39 | PRIVATE dechamps_cpputil::exception 40 | PRIVATE tinytoml 41 | ) 42 | 43 | add_library(FlexASIO_control_panel STATIC EXCLUDE_FROM_ALL control_panel.cpp) 44 | target_link_libraries(FlexASIO_control_panel 45 | PRIVATE FlexASIO_log 46 | PRIVATE FlexASIOUtil_windows_com 47 | PRIVATE FlexASIOUtil_windows_error 48 | PRIVATE FlexASIOUtil_windows_registry 49 | PRIVATE FlexASIOUtil_windows_string 50 | PRIVATE dechamps_CMakeUtils_version 51 | PRIVATE dechamps_cpputil::exception 52 | ) 53 | 54 | add_library(FlexASIO_log STATIC EXCLUDE_FROM_ALL log.cpp) 55 | target_link_libraries(FlexASIO_log 56 | PUBLIC dechamps_cpplog::log 57 | PRIVATE FlexASIOUtil_shell 58 | PRIVATE dechamps_CMakeUtils_version 59 | ) 60 | 61 | add_library(FlexASIO_portaudio STATIC EXCLUDE_FROM_ALL portaudio.cpp) 62 | target_link_libraries(FlexASIO_portaudio 63 | PRIVATE FlexASIOUtil_portaudio 64 | PRIVATE PortAudio::PortAudio 65 | ) 66 | 67 | add_library(FlexASIO_flexasio STATIC EXCLUDE_FROM_ALL flexasio.cpp) 68 | target_link_libraries(FlexASIO_flexasio 69 | PUBLIC dechamps_ASIOUtil::asiosdk_asioh 70 | PUBLIC dechamps_ASIOUtil::asiosdk_asiosys 71 | PUBLIC FlexASIO_config 72 | PUBLIC FlexASIOUtil_portaudio 73 | PRIVATE dechamps_ASIOUtil::asio 74 | PRIVATE FlexASIO_control_panel 75 | PRIVATE FlexASIO_log 76 | PRIVATE dechamps_cpputil::endian 77 | PRIVATE dechamps_cpputil::exception 78 | PRIVATE dechamps_cpputil::string 79 | PRIVATE PortAudio::PortAudio 80 | PRIVATE winmm 81 | ) 82 | 83 | # Note: this is SHARED, not MODULE, otherwise CMake refuses to link that in FlexASIOTest. 84 | add_library(KoordASIO SHARED dll.def flexasio.rc ../versioninfo.rc flexasio.manifest) 85 | target_compile_definitions(KoordASIO PRIVATE PROJECT_DESCRIPTION="KoordASIO ASIO Driver DLL") 86 | target_link_libraries(KoordASIO 87 | PRIVATE FlexASIO_cflexasio 88 | PRIVATE FlexASIO_portaudio 89 | PRIVATE dechamps_CMakeUtils_version_stamp 90 | PRIVATE FlexASIO_comdll 91 | PRIVATE FlexASIO_idl 92 | ) 93 | install(TARGETS KoordASIO RUNTIME DESTINATION bin) 94 | -------------------------------------------------------------------------------- /src/kdasioconfig/main.cpp: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** Copyright (C) 2017 The Qt Company Ltd. 4 | ** Contact: https://www.qt.io/licensing/ 5 | ** 6 | ** This file is part of the examples of the Qt Toolkit. 7 | ** 8 | ** $QT_BEGIN_LICENSE:BSD$ 9 | ** Commercial License Usage 10 | ** Licensees holding valid commercial Qt licenses may use this file in 11 | ** accordance with the commercial license agreement provided with the 12 | ** Software or, alternatively, in accordance with the terms contained in 13 | ** a written agreement between you and The Qt Company. For licensing terms 14 | ** and conditions see https://www.qt.io/terms-conditions. For further 15 | ** information use the contact form at https://www.qt.io/contact-us. 16 | ** 17 | ** BSD License Usage 18 | ** Alternatively, you may use this file under the terms of the BSD license 19 | ** as follows: 20 | ** 21 | ** "Redistribution and use in source and binary forms, with or without 22 | ** modification, are permitted provided that the following conditions are 23 | ** met: 24 | ** * Redistributions of source code must retain the above copyright 25 | ** notice, this list of conditions and the following disclaimer. 26 | ** * Redistributions in binary form must reproduce the above copyright 27 | ** notice, this list of conditions and the following disclaimer in 28 | ** the documentation and/or other materials provided with the 29 | ** distribution. 30 | ** * Neither the name of The Qt Company Ltd nor the names of its 31 | ** contributors may be used to endorse or promote products derived 32 | ** from this software without specific prior written permission. 33 | ** 34 | ** 35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 46 | ** 47 | ** $QT_END_LICENSE$ 48 | ** 49 | ****************************************************************************/ 50 | 51 | #include 52 | #include 53 | 54 | #include "kdasioconfig.h" 55 | 56 | int main(int argc, char **argv) 57 | { 58 | SingleApplication app( argc, argv ); 59 | // QApplication app(argc, argv); 60 | app.setApplicationName("KoordASIO Control"); 61 | 62 | KdASIOConfig audio; 63 | 64 | // For installation: we need to set sensible defaults to make KoordASIO immediately loadable 65 | // -ds - run silent no-gui setDefaultMode - shared 66 | // -de - run silent no-gui setDefaultMode - exclusive 67 | // VERY BASIC ARG-PARSING: 68 | // IF there is one arg AND it is "-defaults" THEN app runs setDefaults() and exits 69 | // qInfo() << "ARGV2" << argv[1]; 70 | if ( !QString("-defaults").compare ( argv[1] ) ) { 71 | audio.setDefaults(); 72 | app.exit(0); 73 | return 0; 74 | } 75 | 76 | // in normal mode - show GUI 77 | audio.show(); 78 | return app.exec(); 79 | } 80 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace flexasio { 15 | 16 | struct Config { 17 | struct DefaultDevice final { 18 | bool operator==(const DefaultDevice&) const { return true; } 19 | }; 20 | struct NoDevice final { 21 | bool operator==(const NoDevice&) const { return true; } 22 | }; 23 | struct DeviceRegex final { 24 | DeviceRegex(std::string string) : string(std::move(string)), regex(this->string) {} 25 | 26 | const std::string& getString() const { return string; } 27 | const std::regex& getRegex() const { return regex; } 28 | 29 | bool operator==(const DeviceRegex& other) const { return string == other.string; } 30 | 31 | private: 32 | std::string string; 33 | std::regex regex; 34 | }; 35 | using Device = std::variant; 36 | 37 | std::optional backend; 38 | std::optional bufferSizeSamples; 39 | 40 | struct Stream { 41 | Device device; 42 | std::optional channels; 43 | std::optional sampleType; 44 | std::optional suggestedLatencySeconds; 45 | bool wasapiExclusiveMode = false; 46 | bool wasapiAutoConvert = true; 47 | bool wasapiExplicitSampleFormat = true; 48 | 49 | bool operator==(const Stream& other) const { 50 | return 51 | device == other.device && 52 | channels == other.channels && 53 | sampleType == other.sampleType && 54 | suggestedLatencySeconds == other.suggestedLatencySeconds && 55 | wasapiExclusiveMode == other.wasapiExclusiveMode && 56 | wasapiAutoConvert == other.wasapiAutoConvert && 57 | wasapiExplicitSampleFormat == other.wasapiExplicitSampleFormat; 58 | } 59 | }; 60 | Stream input; 61 | Stream output; 62 | 63 | bool operator==(const Config& other) const { 64 | return 65 | backend == other.backend && 66 | bufferSizeSamples == other.bufferSizeSamples && 67 | input == other.input && 68 | output == other.output; 69 | } 70 | }; 71 | 72 | class ConfigLoader { 73 | public: 74 | ConfigLoader(); 75 | 76 | const Config& Initial() const { return initialConfig; } 77 | 78 | class Watcher { 79 | public: 80 | Watcher(const ConfigLoader& configLoader, std::function onConfigChange); 81 | ~Watcher() noexcept(false); 82 | 83 | private: 84 | struct HandleCloser { 85 | void operator()(HANDLE handle); 86 | }; 87 | using UniqueHandle = std::unique_ptr, HandleCloser>; 88 | 89 | struct OverlappedWithEvent { 90 | OverlappedWithEvent(); 91 | ~OverlappedWithEvent(); 92 | 93 | OVERLAPPED overlapped = { 0 }; 94 | }; 95 | 96 | void StartWatching(); 97 | void RunThread(); 98 | void OnEvent(); 99 | void Debounce(); 100 | bool FillNotifyInformationBuffer(); 101 | bool FindConfigFileEvents(); 102 | void OnConfigFileEvent(); 103 | 104 | const ConfigLoader& configLoader; 105 | const std::function onConfigChange; 106 | const UniqueHandle stopEvent; 107 | const UniqueHandle directory; 108 | OverlappedWithEvent overlapped; 109 | alignas(DWORD) char fileNotifyInformationBuffer[64 * 1024]; 110 | std::thread thread; 111 | }; 112 | 113 | private: 114 | const std::filesystem::path configDirectory; 115 | const Config initialConfig; 116 | }; 117 | 118 | } -------------------------------------------------------------------------------- /src/kdasioconfig/kdasioconfig.h: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** Copyright (C) 2017 The Qt Company Ltd. 4 | ** Contact: https://www.qt.io/licensing/ 5 | ** 6 | ** This file is part of the examples of the Qt Toolkit. 7 | ** 8 | ** $QT_BEGIN_LICENSE:BSD$ 9 | ** Commercial License Usage 10 | ** Licensees holding valid commercial Qt licenses may use this file in 11 | ** accordance with the commercial license agreement provided with the 12 | ** Software or, alternatively, in accordance with the terms contained in 13 | ** a written agreement between you and The Qt Company. For licensing terms 14 | ** and conditions see https://www.qt.io/terms-conditions. For further 15 | ** information use the contact form at https://www.qt.io/contact-us. 16 | ** 17 | ** BSD License Usage 18 | ** Alternatively, you may use this file under the terms of the BSD license 19 | ** as follows: 20 | ** 21 | ** "Redistribution and use in source and binary forms, with or without 22 | ** modification, are permitted provided that the following conditions are 23 | ** met: 24 | ** * Redistributions of source code must retain the above copyright 25 | ** notice, this list of conditions and the following disclaimer. 26 | ** * Redistributions in binary form must reproduce the above copyright 27 | ** notice, this list of conditions and the following disclaimer in 28 | ** the documentation and/or other materials provided with the 29 | ** distribution. 30 | ** * Neither the name of The Qt Company Ltd nor the names of its 31 | ** contributors may be used to endorse or promote products derived 32 | ** from this software without specific prior written permission. 33 | ** 34 | ** 35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 46 | ** 47 | ** $QT_END_LICENSE$ 48 | ** 49 | ****************************************************************************/ 50 | 51 | #ifndef KDASIOCONFIG_H 52 | #define KDASIOCONFIG_H 53 | 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include "toml.h" 62 | 63 | #include "ui_kdasioconfigbase.h" 64 | 65 | class KdASIOConfigBase : public QMainWindow, public Ui::KdASIOConfigBase 66 | { 67 | public: 68 | KdASIOConfigBase(QWidget *parent = 0); 69 | virtual ~KdASIOConfigBase(); 70 | }; 71 | 72 | class KdASIOConfig : public KdASIOConfigBase 73 | { 74 | Q_OBJECT 75 | 76 | public: 77 | explicit KdASIOConfig(QWidget *parent = nullptr); 78 | 79 | private: 80 | QAudioDevice m_inputDeviceInfo; 81 | QAudioDevice m_outputDeviceInfo; 82 | QAudioDevice::Mode input_mode = QAudioDevice::Input; 83 | QAudioDevice::Mode output_mode = QAudioDevice::Output; 84 | QMediaDevices *m_devices = nullptr; 85 | QAudioFormat m_settings; 86 | int bufferSize; 87 | bool exclusive_mode; 88 | QString outputDeviceName; 89 | QString inputDeviceName; 90 | // QString fullpath = QDir::homePath() + "/.KoordASIO-builtin.toml"; 91 | QString fullpath = QDir::homePath() + "/.KoordASIO.toml"; 92 | QString inputAudioSettPath = "mmsys.cpl,,1"; 93 | QString outputAudioSettPath = "mmsys.cpl"; 94 | QList bufferSizes = { 32, 64, 128, 256, 512, 1024, 2048 }; 95 | QProcess *mmcplProc; 96 | 97 | public slots: 98 | void setDefaults(); 99 | 100 | private slots: 101 | void bufferSizeChanged(int idx); 102 | void bufferSizeDisplayChange(int idx); 103 | // void exclusiveModeChanged(); 104 | void setOperationMode(); 105 | void sharedModeSet(); 106 | void exclusiveModeSet(); 107 | void writeTomlFile(); 108 | void inputDeviceChanged(int idx); 109 | void outputDeviceChanged(int idx); 110 | 111 | void setValuesFromToml(std::ifstream *ifs, toml::ParseResult *pr); 112 | void inputAudioSettClicked(); 113 | void outputAudioSettClicked(); 114 | void koordLiveClicked(); 115 | void githubClicked(); 116 | void versionButtonClicked(); 117 | 118 | void updateInputsList(); 119 | void updateOutputsList(); 120 | }; 121 | 122 | #endif 123 | 124 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # FlexASIO CMake superbuild. 2 | 3 | cmake_minimum_required(VERSION 3.11) 4 | project(flexasio DESCRIPTION "FlexASIO Universal ASIO Driver Superbuild") 5 | include(ExternalProject) 6 | set(INTERNAL_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/install") 7 | set(CMAKE_ARGS 8 | "-DCMAKE_INSTALL_PREFIX=${INTERNAL_INSTALL_PREFIX}" 9 | "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}" 10 | ) 11 | 12 | include(check_git_submodule.cmake) 13 | 14 | check_git_submodule(dechamps_CMakeUtils) 15 | 16 | check_git_submodule(tinytoml) 17 | ExternalProject_Add( 18 | tinytoml 19 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/tinytoml" 20 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 21 | CONFIGURE_COMMAND "" 22 | BUILD_COMMAND "" BUILD_ALWAYS 23 | INSTALL_COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_LIST_DIR}/tinytoml/include" "${INTERNAL_INSTALL_PREFIX}/include" 24 | EXCLUDE_FROM_ALL 25 | ) 26 | 27 | check_git_submodule(cxxopts) 28 | ExternalProject_Add( 29 | cxxopts 30 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/cxxopts" 31 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 32 | CMAKE_ARGS ${CMAKE_ARGS} 33 | BUILD_ALWAYS 34 | EXCLUDE_FROM_ALL 35 | ) 36 | 37 | check_git_submodule(libsndfile) 38 | ExternalProject_Add( 39 | libsndfile 40 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/libsndfile" 41 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 42 | CMAKE_ARGS ${CMAKE_ARGS} -DBUILD_PROGRAMS=OFF -DBUILD_SHARED_LIBS=ON 43 | BUILD_ALWAYS USES_TERMINAL_BUILD TRUE 44 | EXCLUDE_FROM_ALL 45 | ) 46 | 47 | check_git_submodule(portaudio) 48 | ExternalProject_Add( 49 | portaudio 50 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/portaudio" 51 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 52 | BUILD_ALWAYS TRUE USES_TERMINAL_BUILD TRUE 53 | CMAKE_ARGS ${CMAKE_ARGS} 54 | -DPA_ENABLE_DEBUG_OUTPUT=ON 55 | "-DCMAKE_PROJECT_PortAudio_INCLUDE=${CMAKE_CURRENT_LIST_DIR}/portaudio.cmake" 56 | # The CMAKE_INSTALL_INCLUDEDIR built-in was introduced in CMake 3.14. 57 | # Sadly current Visual Studio (16.11.3) ships with CMake 3.13, so we have to work around the missing built-in. 58 | -DCMAKE_INSTALL_INCLUDEDIR=include 59 | EXCLUDE_FROM_ALL 60 | ) 61 | 62 | check_git_submodule(dechamps_cpputil) 63 | ExternalProject_Add( 64 | dechamps_cpputil 65 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/dechamps_cpputil" 66 | BUILD_ALWAYS TRUE USES_TERMINAL_BUILD TRUE 67 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 68 | CMAKE_ARGS ${CMAKE_ARGS} 69 | EXCLUDE_FROM_ALL 70 | ) 71 | 72 | check_git_submodule(dechamps_cpplog) 73 | ExternalProject_Add( 74 | dechamps_cpplog 75 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/dechamps_cpplog" 76 | BUILD_ALWAYS TRUE USES_TERMINAL_BUILD TRUE 77 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 78 | CMAKE_ARGS ${CMAKE_ARGS} 79 | EXCLUDE_FROM_ALL 80 | ) 81 | 82 | check_git_submodule(dechamps_ASIOUtil) 83 | ExternalProject_Add( 84 | dechamps_ASIOUtil 85 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/dechamps_ASIOUtil" 86 | USES_TERMINAL_CONFIGURE TRUE 87 | BUILD_ALWAYS TRUE USES_TERMINAL_BUILD TRUE 88 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 89 | CMAKE_ARGS ${CMAKE_ARGS} 90 | EXCLUDE_FROM_ALL 91 | DEPENDS dechamps_cpputil 92 | ) 93 | 94 | check_git_submodule(ASIOTest) 95 | ExternalProject_Add( 96 | ASIOTest 97 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/ASIOTest" 98 | BUILD_ALWAYS TRUE USES_TERMINAL_BUILD TRUE 99 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 100 | CMAKE_ARGS ${CMAKE_ARGS} 101 | EXCLUDE_FROM_ALL 102 | DEPENDS cxxopts dechamps_cpputil dechamps_cpplog dechamps_ASIOUtil libsndfile 103 | ) 104 | 105 | ExternalProject_Add( 106 | FlexASIO 107 | SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/flexasio" 108 | BUILD_ALWAYS TRUE USES_TERMINAL_BUILD TRUE 109 | INSTALL_DIR "${INTERNAL_INSTALL_PREFIX}" 110 | CMAKE_ARGS ${CMAKE_ARGS} 111 | DEPENDS tinytoml portaudio dechamps_cpputil dechamps_cpplog dechamps_ASIOUtil ASIOTest 112 | ) 113 | 114 | install(DIRECTORY "${INTERNAL_INSTALL_PREFIX}/" DESTINATION "${CMAKE_INSTALL_PREFIX}") 115 | install(SCRIPT dechamps_CMakeUtils/InstallPdbFiles.cmake) 116 | 117 | # Work around https://developercommunity.visualstudio.com/content/problem/618088/cmake-msvc-toolset-version-is-incorrect-in-visual.html 118 | # Note that for InstallRequiredSystemLibraries to work you might also need to work around https://developercommunity.visualstudio.com/content/problem/618084/cmake-installrequiredsystemlibraries-broken-in-lat.html 119 | if (MSVC_VERSION EQUAL 1921 AND MSVC_TOOLSET_VERSION EQUAL 141) 120 | set(MSVC_TOOLSET_VERSION 142) 121 | endif() 122 | # Work around https://developercommunity.visualstudio.com/t/1616850 123 | if (MSVC_VERSION EQUAL 1930 AND MSVC_TOOLSET_VERSION EQUAL 142) 124 | cmake_host_system_information(RESULT VS_DIR QUERY VS_17_DIR) 125 | file(GLOB MSVC_REDIST_LIBRARIES "${VS_DIR}/VC/Redist/MSVC/*/${MSVC_C_ARCHITECTURE_ID}/Microsoft.VC143.CRT/*.dll") 126 | list(APPEND CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS "${MSVC_REDIST_LIBRARIES}") 127 | set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_NO_WARNINGS TRUE) 128 | endif() 129 | include(InstallRequiredSystemLibraries) 130 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/control_panel.cpp: -------------------------------------------------------------------------------- 1 | #pragma warning(disable: 6553) // https://developercommunity.visualstudio.com/t/warning-C6553:-The-annotation-for-functi/1676659 2 | 3 | #include "control_panel.h" 4 | 5 | #include "../FlexASIOUtil/windows_com.h" 6 | #include "../FlexASIOUtil/windows_error.h" 7 | #include "../FlexASIOUtil/windows_registry.h" 8 | #include "../FlexASIOUtil/windows_string.h" 9 | 10 | #include 11 | #include 12 | 13 | #include "log.h" 14 | 15 | #include 16 | 17 | namespace flexasio { 18 | 19 | namespace { 20 | 21 | void Execute(HWND windowHandle, const std::wstring& file) { 22 | Log() << "Initializing COM for shell execution"; 23 | std::optional comInitializer; 24 | try { 25 | // As suggested in https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew#remarks 26 | comInitializer.emplace(COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); 27 | } 28 | catch (const std::exception& exception) { 29 | Log() << "Unable to initialize COM: " << ::dechamps_cpputil::GetNestedExceptionMessage(exception); 30 | } 31 | catch (...) { 32 | Log() << "Unable to initialize COM due to unknown exception"; 33 | } 34 | 35 | Log() << "Executing: " << ConvertToUTF8(file); 36 | if (!::ShellExecuteW(windowHandle, NULL, file.c_str(), NULL, NULL, SW_SHOWNORMAL)) 37 | throw std::runtime_error("Execution failed: " + GetWindowsErrorString(::GetLastError())); 38 | } 39 | 40 | std::wstring GetStringRegistryValue(HKEY registryKey, LPCWSTR valueName) { 41 | std::vector value; 42 | for (;;) { 43 | DWORD valueType = REG_NONE; 44 | DWORD valueSize = static_cast(value.size()); 45 | Log() << "Querying registry value with buffer size " << valueSize; 46 | const auto regQueryValueError = ::RegQueryValueExW(registryKey, valueName, NULL, &valueType, reinterpret_cast(value.data()), &valueSize); 47 | if ((regQueryValueError == ERROR_SUCCESS && value.size() == 0) || regQueryValueError == ERROR_MORE_DATA) { 48 | if (valueSize <= value.size()) throw std::runtime_error("Invalid value size returned from RegQueryValueEx(" + std::to_string(value.size()) + "): " + std::to_string(valueSize)); 49 | value.resize(valueSize); 50 | continue; 51 | } 52 | if (regQueryValueError != ERROR_SUCCESS) throw std::runtime_error("Unable to query string registry value: " + GetWindowsErrorString(regQueryValueError)); 53 | Log() << "Registry value size: " << valueSize; 54 | if (valueType != REG_SZ) throw std::runtime_error("Expected string registry value type, got " + std::to_string(valueType)); 55 | value.resize(valueSize); 56 | break; 57 | } 58 | 59 | const auto char_size = sizeof(std::wstring::value_type); 60 | if (value.size() % char_size != 0) throw std::runtime_error("Invalid value size returned from RegQueryValueEx(): " + std::to_string(value.size())); 61 | std::wstring result(value.size() / char_size, 0); 62 | memcpy(result.data(), value.data(), value.size()); 63 | while (!result.empty() && result.back() == 0) result.pop_back(); 64 | return result; 65 | } 66 | 67 | UniqueHKEY OpenFlexAsioGuiInstallRegistryKey() { 68 | HKEY registryKey; 69 | const auto regOpenKeyError = ::RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Koord\\KoordASIO\\Install", {}, KEY_QUERY_VALUE | KEY_WOW64_64KEY, ®istryKey); 70 | if (regOpenKeyError != ERROR_SUCCESS) throw std::runtime_error("Unable to open KoordASIOControl registry key: " + GetWindowsErrorString(regOpenKeyError)); 71 | return UniqueHKEY(registryKey); 72 | } 73 | 74 | std::wstring GetFlexAsioGuiInstallDirectory() { 75 | Log() << "Attempting to open KoordASIOControl install registry key"; 76 | const auto installRegistryKey = OpenFlexAsioGuiInstallRegistryKey(); 77 | 78 | Log() << "Attempting to query KoordASIOControl install path registry value"; 79 | return GetStringRegistryValue(installRegistryKey.get(), L"InstallPath"); 80 | } 81 | 82 | void OpenFlexAsioGui(HWND windowHandle) { 83 | const auto installDirectory = GetFlexAsioGuiInstallDirectory(); 84 | Log() << "KoordASIOControl install directory: " << ConvertToUTF8(installDirectory); 85 | 86 | Execute(windowHandle, installDirectory + L"\\KoordASIOControl.exe"); 87 | } 88 | 89 | void OpenConfigurationDocs(HWND windowHandle) { 90 | Execute(windowHandle, std::wstring(L"https://github.com/koord-live/KoordASIO/blob/") + ConvertFromUTF8(::dechamps_CMakeUtils_gitDescription) + L"/CONFIGURATION.md"); 91 | } 92 | 93 | } 94 | 95 | void OpenControlPanel(HWND windowHandle) { 96 | Log() << "Attempting to open KoordASIOControl"; 97 | try { 98 | OpenFlexAsioGui(windowHandle); 99 | return; 100 | } 101 | catch (const std::exception& exception) { 102 | Log() << "Unable to open KoordASIOControl: " << ::dechamps_cpputil::GetNestedExceptionMessage(exception); 103 | } 104 | catch (...) { 105 | Log() << "Unable to open KoordASIOControl due to unknown exception"; 106 | } 107 | 108 | Log() << "Attempting to open configuration docs"; 109 | OpenConfigurationDocs(windowHandle); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/flexasio/PortAudioDevices/list.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "../FlexASIOUtil/portaudio.h" 15 | 16 | namespace flexasio { 17 | namespace { 18 | 19 | template 20 | auto ThrowOnPaError(Result result) { 21 | if (result >= 0) return result; 22 | throw std::runtime_error(std::string("PortAudio error ") + Pa_GetErrorText(result)); 23 | } 24 | 25 | void SetUTF8Mode(FILE* file, std::wstring_view label) { 26 | const auto fileno = _fileno(file); 27 | if (fileno < 0) { 28 | std::wcerr << "Warning: cannot get file descriptor for " << label; 29 | return; 30 | } 31 | // We need proper non-ASCII encoding support for printing device names - see https://github.com/dechamps/FlexASIO/issues/73 32 | // However, Unicode support in Windows console applications is an absolute mess. 33 | // _setmode(), _O_U8TEXT, and sticking to wide character I/O seems to produce the "least broken" results. 34 | // One thing that seems to always be broken no matter what is when using "> file.txt" in Powershell, which results in some garbage UTF-8-in-UTF-16 abomination (cmd.exe is fine, though). 35 | if (_setmode(fileno, _O_U8TEXT) < 0) 36 | std::wcerr << "Warning: cannot set " << label << " to UTF-8"; 37 | } 38 | 39 | std::wstring UTF8ToWideString(std::string_view utf8) { 40 | const auto size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), int(utf8.size()), NULL, 0); 41 | if (size == 0) 42 | throw std::runtime_error("Unable to convert UTF-8 string"); 43 | std::wstring result(size, 0); 44 | if (MultiByteToWideChar(CP_UTF8, 0, utf8.data(), int(utf8.size()), result.data(), int(result.size())) == 0) 45 | throw std::runtime_error("Unable to convert UTF-8 string"); 46 | return result; 47 | } 48 | 49 | void PrintDevice(PaDeviceIndex deviceIndex) { 50 | std::wcout << "Device index: " << deviceIndex << std::endl; 51 | 52 | const auto device = Pa_GetDeviceInfo(deviceIndex); 53 | if (device == nullptr) throw std::runtime_error("Pa_GetDeviceInfo() returned NULL"); 54 | 55 | std::wcout << "Device name: \"" << UTF8ToWideString(device->name) << "\"" << std::endl; 56 | std::wcout << "Default sample rate: " << device->defaultSampleRate << std::endl; 57 | std::wcout << "Input: max channel count " << device->maxInputChannels << ", default latency " << device->defaultLowInputLatency << "s (low) " << device->defaultHighInputLatency << "s (high)" << std::endl; 58 | std::wcout << "Output: max channel count " << device->maxOutputChannels << ", default latency " << device->defaultLowOutputLatency << "s (low) " << device->defaultHighOutputLatency << "s (high)" << std::endl; 59 | 60 | if (device->hostApi < 0) throw std::runtime_error("invalid hostApi index"); 61 | const auto hostApi = Pa_GetHostApiInfo(device->hostApi); 62 | if (hostApi == nullptr) throw std::runtime_error("Pa_GetHostApiInfo() returned NULL"); 63 | 64 | std::wcout << "Host API name: " << hostApi->name << std::endl; 65 | std::wcout << "Host API type: " << UTF8ToWideString(GetHostApiTypeIdString(hostApi->type)) << std::endl; 66 | if (deviceIndex == hostApi->defaultInputDevice) { 67 | std::wcout << "DEFAULT INPUT DEVICE for this host API" << std::endl; 68 | } 69 | if (deviceIndex == hostApi->defaultOutputDevice) { 70 | std::wcout << "DEFAULT OUTPUT DEVICE for this host API" << std::endl; 71 | } 72 | 73 | switch (hostApi->type) { 74 | case paWASAPI: 75 | std::wcout << "WASAPI device default format: " << UTF8ToWideString(DescribeWaveFormat(GetWasapiDeviceDefaultFormat(deviceIndex))) << std::endl; 76 | std::wcout << "WASAPI device mix format: " << UTF8ToWideString(DescribeWaveFormat(GetWasapiDeviceMixFormat(deviceIndex))) << std::endl; 77 | } 78 | } 79 | 80 | void ListDevices() { 81 | const PaDeviceIndex deviceCount = Pa_GetDeviceCount(); 82 | 83 | for (PaDeviceIndex deviceIndex = 0; deviceIndex < deviceCount; ++deviceIndex) { 84 | try { 85 | PrintDevice(deviceIndex); 86 | } 87 | catch (const std::exception& exception) { 88 | std::wcerr << "Error while printing device index " << deviceIndex << ": " << exception.what() << std::endl; 89 | } 90 | std::wcout << std::endl; 91 | } 92 | } 93 | 94 | void InitAndListDevices() { 95 | SetUTF8Mode(stderr, L"standard error"); 96 | SetUTF8Mode(stdout, L"standard output"); 97 | 98 | PortAudioDebugRedirector portAudioLogger([](std::string_view str) { std::wcerr << "[PortAudio] " << UTF8ToWideString(str) << std::endl; }); 99 | 100 | try { 101 | ThrowOnPaError(Pa_Initialize()); 102 | } 103 | catch (const std::exception& exception) { 104 | throw std::runtime_error(std::string("failed to initialize PortAudio: ") + exception.what()); 105 | } 106 | 107 | ListDevices(); 108 | 109 | try { 110 | ThrowOnPaError(Pa_Terminate()); 111 | } 112 | catch (const std::exception& exception) { 113 | throw std::runtime_error(std::string("failed to terminate PortAudio: ") + exception.what()); 114 | } 115 | } 116 | 117 | } 118 | } 119 | 120 | int main(int, char**) { 121 | try { 122 | ::flexasio::InitAndListDevices(); 123 | } 124 | catch (const std::exception& exception) { 125 | std::wcerr << "ERROR: " << exception.what() << std::endl; 126 | return EXIT_FAILURE; 127 | } 128 | return EXIT_SUCCESS; 129 | } -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml_WIP: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | build: 4 | runs-on: windows-latest 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | include: 9 | - msvc_config: x64-Release 10 | msvc_arch: amd64 11 | build_type: RelWithDebInfo 12 | # - msvc_config: x86-Release 13 | # msvc_arch: amd64_x86 14 | # build_type: RelWithDebInfo 15 | # - msvc_config: x64-Debug 16 | # msvc_arch: amd64 17 | # build_type: Debug 18 | # - msvc_config: x86-Debug 19 | # msvc_arch: amd64_x86 20 | # build_type: Debug 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | submodules: recursive 25 | # Required for version stamping (`git describe`) to work. 26 | fetch-depth: 0 27 | - uses: ilammy/msvc-dev-cmd@v1 28 | with: 29 | arch: ${{ matrix.msvc_arch }} 30 | - name: install Qt 31 | run: | 32 | pip install aqtinstall==3.0.1 33 | aqt install-qt --outputdir C:\Qt windows desktop 6.4.1 win64_msvc2019_64 --modules qtmultimedia --archives qtbase qttools 34 | aqt install-tool windows desktop --outputdir C:\Qt tools_vcredist qt.tools.vcredist_msvc2019_x64 35 | aqt install-tool windows desktop --outputdir C:\Qt tools_cmake qt.tools.cmake 36 | - name: Set up codesign cert and passwd 37 | shell: powershell 38 | run: | 39 | $B64Cert = $Env:WINDOWS_CODESIGN_CERT 40 | $WindowsOVCert = [Convert]::FromBase64String($B64Cert) 41 | [IO.File]::WriteAllBytes('C:\KoordOVCert.pfx', $WindowsOVCert) 42 | $Env:WINDOWS_CODESIGN_PWD | Out-File 'C:\KoordOVCertPwd' 43 | ## Start actual build 44 | - run: cmake -S src -B src/out/build/${{ matrix.msvc_config }} -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_INSTALL_PREFIX:PATH=${{ github.workspace }}/src/out/install/${{ matrix.msvc_config }} 45 | - run: cmake --build src/out/build/${{ matrix.msvc_config }} 46 | - run: cmake --install src/out/build/${{ matrix.msvc_config }} 47 | 48 | # Run windeployqt 49 | - name: windeployqt 50 | run: windeployqt --release --no-compiler-runtime --dir=$DeployPath\$BuildArch --no-system-d3d-compiler --no-opengl-sw $BuildPath\release\kdasioconfig\KoordASIOControl.exe 51 | 52 | - name: Copy VSdist DLLs ??? 53 | shell: powershell 54 | run: Copy-Item -Path "$VsDistFile64Path\*" -Destination "$DeployPath\$BuildArch" 55 | 56 | - name: Copy Binaries - necessary ?? 57 | shell: powershell 58 | run: | 59 | Move-Item -Path "$BuildPath\$BuildConfig\kdasioconfig\KoordASIOControl.exe" -Destination "$DeployPath\$BuildArch" -Force 60 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\KoordASIO.dll" -Destination "$DeployPath\$BuildArch" -Force 61 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\portaudio.dll" -Destination "$DeployPath\$BuildArch" -Force 62 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\ASIOTest.dll" -Destination "$DeployPath\$BuildArch" -Force 63 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\sndfile.dll" -Destination "$DeployPath\$BuildArch" -Force 64 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\FlexASIOTest.exe" -Destination "$DeployPath\$BuildArch" -Force 65 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\PortAudioDevices.exe" -Destination "$DeployPath\$BuildArch" -Force 66 | Move-Item -Path "$WindowsPath\kdinstaller.iss" -Destination "$RootPath" -Force 67 | 68 | - uses: actions/upload-artifact@v3 69 | with: 70 | name: FlexASIO-${{ matrix.msvc_config }} 71 | path: src/out/install/${{ matrix.msvc_config }}/ 72 | 73 | # https://github.com/actions/virtual-environments/issues/2528 74 | # TODO: Scream only provides an output device. See if we can use a 75 | # different virtual audio driver that provides a virtual input device as 76 | # well. 77 | - name: Install Scream 78 | shell: powershell 79 | run: | 80 | Invoke-WebRequest https://github.com/duncanthrax/scream/releases/download/3.8/Scream3.8.zip -OutFile Scream3.8.zip 81 | Expand-Archive -Path Scream3.8.zip -DestinationPath Scream 82 | Import-Certificate -FilePath Scream\Install\driver\x64\Scream.cat -CertStoreLocation Cert:\LocalMachine\TrustedPublisher 83 | Scream\Install\helpers\devcon-x64.exe install Scream\Install\driver\x64\Scream.inf *Scream 84 | # Starting from windows-2022, the Windows Audio engine doesn't seem to 85 | # be started by default anymore. 86 | - run: net start audiosrv 87 | # Obviously this doesn't do any kind of thorough testing. We just want to 88 | # make sure that the executables are not obviously broken. 89 | - run: src/out/install/${{ matrix.msvc_config }}/bin/PortAudioDevices.exe 90 | - run: src/out/install/${{ matrix.msvc_config }}/bin/FlexASIOTest.exe --verbose 91 | 92 | installer: 93 | runs-on: windows-latest 94 | needs: build 95 | steps: 96 | - uses: actions/checkout@v2 97 | with: 98 | submodules: recursive 99 | # Required for version stamping (`git describe`) to work. 100 | fetch-depth: 0 101 | - uses: actions/download-artifact@v2 102 | with: 103 | name: FlexASIO-x64-Release 104 | path: src/out/install/x64-Release 105 | # - uses: actions/download-artifact@v2 106 | # with: 107 | # name: FlexASIO-x86-Release 108 | # path: src/out/install/x86-Release 109 | - run: cmake -P installer.cmake 110 | working-directory: src 111 | - uses: actions/upload-artifact@v3 112 | with: 113 | name: FlexASIO-installer 114 | path: src/out/installer/* 115 | -------------------------------------------------------------------------------- /.github/autobuild/windows.ps1: -------------------------------------------------------------------------------- 1 | # Steps for generating Windows artifacts via Github Actions 2 | # See README.md in this folder for details. 3 | # See windows/deploy_windows.ps1 for standalone builds. 4 | 5 | param( 6 | [Parameter(Mandatory=$true)] 7 | [string] $Stage = "", 8 | # Allow buildoption to be passed for jackonwindows build, leave empty for standard (ASIO) build: 9 | [string] $BuildOption = "", 10 | # unused, only required during refactoring as long as not all platforms have been updated: 11 | [string] $GithubWorkspace ="" 12 | ) 13 | 14 | # Fail early on all errors 15 | $ErrorActionPreference = "Stop" 16 | 17 | $QtDir = 'C:\Qt' 18 | $ChocoCacheDir = 'C:\ChocoCache' 19 | $Qt64Version = "6.4.2" 20 | $AqtinstallVersion = "3.0.1" 21 | $Msvc64Version = "win64_msvc2019_64" 22 | $JomVersion = "1.1.2" 23 | 24 | $KoordASIOVersion = $Env:koordasio_buildversionstring 25 | if ( $KoordASIOVersion -notmatch '^\d+\.\d+\.\d+.*' ) 26 | { 27 | throw "Environment variable koordasio_buildversionstring has to be set to a valid version string" 28 | } 29 | 30 | Function installQt 31 | { 32 | param( 33 | [string] $QtVersion, 34 | [string] $QtArch 35 | ) 36 | $Args = ( 37 | "--outputdir", "$QtDir", 38 | "windows", 39 | "desktop", 40 | "$QtVersion", 41 | "$QtArch", 42 | "--modules", "qtmultimedia", 43 | "--archives", "qtbase", "qtdeclarative", "qtsvg", "qttools" 44 | ) 45 | aqt install-qt @Args 46 | if ( !$? ) 47 | { 48 | Write-Output "WARNING: Qt installation via first aqt run failed, re-starting with different base URL." 49 | aqt install-qt -b https://mirrors.ocf.berkeley.edu/qt/ @Args 50 | if ( !$? ) 51 | { 52 | throw "Qt installation with args @Args failed with exit code $LastExitCode" 53 | } 54 | } 55 | 56 | # Above should do: 57 | # aqt install --outputdir C:\Qt 5.15.2 windows desktop win64_msvc2019_64 58 | 59 | # add vcredist and cmake - for KoordASIO build 60 | aqt install-tool windows desktop --outputdir C:\Qt tools_vcredist qt.tools.vcredist_msvc2019_x64 61 | aqt install-tool windows desktop --outputdir C:\Qt tools_cmake qt.tools.cmake 62 | 63 | # # add openssl 1.1 64 | # aqt install-tool windows desktop --outputdir C:\Qt tools_openssl_x64 65 | } 66 | 67 | Function ensureQt 68 | { 69 | if ( Test-Path -Path $QtDir ) 70 | { 71 | Write-Output "Using Qt installation from previous run (actions/cache)" 72 | return 73 | } 74 | 75 | Write-Output "Install Qt..." 76 | # Install Qt 77 | # "Preparing metadata (pyproject.toml) did not run successfully." 78 | pip install "aqtinstall==$AqtinstallVersion" 79 | if ( !$? ) 80 | { 81 | throw "pip install aqtinstall failed with exit code $LastExitCode" 82 | } 83 | 84 | Write-Output "Get Qt 64 bit..." 85 | installQt "${Qt64Version}" "${Msvc64Version}" 86 | } 87 | 88 | Function ensureJom 89 | { 90 | choco install --no-progress -y jom --version "${JomVersion}" 91 | } 92 | 93 | Function setupCodeSignCertificate 94 | { 95 | # write Windows OV CodeSign cert to file 96 | Write-Output "Writing CodeSign cert output to file C:\KoordOVCert.pfx ..." 97 | $B64Cert = $Env:WINDOWS_CODESIGN_CERT 98 | $WindowsOVCert = [Convert]::FromBase64String($B64Cert) 99 | [IO.File]::WriteAllBytes('C:\KoordOVCert.pfx', $WindowsOVCert) 100 | ls 'C:\KoordOVCert.pfx' 101 | Write-Output "debug: CodeSign cert :" 102 | cat 'C:\KoordOVCert.pfx' 103 | 104 | # write Windows OV CodeSIgn cert password to file 105 | Write-Output "Writing CodeSign password to C:\KoordOVCertPwd ..." 106 | $Env:WINDOWS_CODESIGN_PWD | Out-File 'C:\KoordOVCertPwd' 107 | # New-Item 'C:\KoordOVCertPwd' 108 | # Set-Content 'C:\KoordOVCertPwd' $Env:WINDOWS_CODESIGN_PWD 109 | ls 'C:\KoordOVCertPwd' 110 | Write-Output "debug: CodeSign password :" 111 | cat 'C:\KoordOVCertPwd' 112 | } 113 | 114 | Function buildAppWithInstaller 115 | { 116 | Write-Output "Build app and create installer..." 117 | $ExtraArgs = @() 118 | if ( $BuildOption -ne "" ) 119 | { 120 | $ExtraArgs += ("-BuildOption", $BuildOption) 121 | } 122 | $ExtraArgs += ("-APP_BUILD_VERSION", $KoordASIOVersion) 123 | $ExtraArgs += ("-QtInstallPath", "C:\Qt\${Qt64Version}" ) 124 | powershell ".\windows\deploy_windows.ps1" @ExtraArgs 125 | if ( !$? ) 126 | { 127 | throw "deploy_windows.ps1 failed with exit code $LastExitCode" 128 | } 129 | } 130 | 131 | Function passExeArtifactToJob 132 | { 133 | $artifact = "KoordASIO_${KoordASIOVersion}.exe" 134 | 135 | Write-Output "Copying artifact to ${artifact}" 136 | # "Output" is name of dir for innosetup output 137 | Move-Item ".\Output\KoordASIO*.exe" ".\deploy\${artifact}" 138 | if ( !$? ) 139 | { 140 | throw "Move-Item failed with exit code $LastExitCode" 141 | } 142 | Write-Output "Setting Github step output name=artifact_1::${artifact}" 143 | Write-Output "artifact_1=${artifact}" >> "$Env:GITHUB_OUTPUT" 144 | } 145 | 146 | # Function passMsixArtifactToJob 147 | # { 148 | # $artifact = "KoordASIO_${KoordASIOVersion}.msix" 149 | 150 | # Write-Output "Copying artifact to ${artifact}" 151 | # # "deploy" is dir of MakeAppx output 152 | 153 | # # make special dir for store upload 154 | # New-Item -Path ".\publish" -ItemType Directory 155 | # # Copy-Item .msix artifact to publish/ dir 156 | # Copy-Item ".\deploy\KoordASIO.msix" ".\publish\${artifact}" 157 | 158 | # Move-Item ".\deploy\KoordASIO.msix" ".\deploy\${artifact}" 159 | # if ( !$? ) 160 | # { 161 | # throw "Move-Item failed with exit code $LastExitCode" 162 | # } 163 | # Write-Output "Setting Github step output name=artifact_2::${artifact}" 164 | # Write-Output "artifact_2=${artifact}" >> "$Env:GITHUB_OUTPUT" 165 | # } 166 | 167 | switch ( $Stage ) 168 | { 169 | "setup" 170 | { 171 | choco config set cacheLocation $ChocoCacheDir 172 | ensureQt 173 | ensureJom 174 | 175 | } 176 | "build" 177 | { 178 | setupCodeSignCertificate 179 | buildAppWithInstaller 180 | } 181 | "get-artifacts" 182 | { 183 | passExeArtifactToJob 184 | # passMsixArtifactToJob 185 | } 186 | default 187 | { 188 | throw "Unknown stage ${Stage}" 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/cflexasio.cpp: -------------------------------------------------------------------------------- 1 | #include "cflexasio.h" 2 | 3 | #include "flexasio.h" 4 | #include "flexasio.rc.h" 5 | #include "flexasio_h.h" 6 | 7 | #include "log.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | // Provide a definition for the ::CFlexASIO class declaration that the MIDL compiler generated. 20 | // The actual implementation is in a derived class in an anonymous namespace, as it should be. 21 | // 22 | // Note: ASIO doesn't use COM properly, and doesn't define a proper interface. 23 | // Instead, it uses the CLSID to create an instance and then blindfully casts it to IASIO, giving the finger to QueryInterface() and to sensible COM design in general. 24 | // Of course, since this is a blind cast, the order of inheritance below becomes critical: if IASIO is not first, the cast is likely to produce a wrong vtable offset, crashing the whole thing. What a nice design. 25 | class CFlexASIO : public IASIO, public IFlexASIO {}; 26 | 27 | namespace flexasio { 28 | namespace { 29 | 30 | class CFlexASIO : 31 | public ::CFlexASIO, 32 | public CComObjectRootEx, 33 | public CComCoClass 34 | { 35 | BEGIN_COM_MAP(CFlexASIO) 36 | COM_INTERFACE_ENTRY(IFlexASIO) 37 | 38 | // To add insult to injury, ASIO mistakes the CLSID for an IID when calling CoCreateInstance(). Yuck. 39 | COM_INTERFACE_ENTRY(::CFlexASIO) 40 | 41 | // IASIO doesn't have an IID (see above), which is why it doesn't appear here. 42 | END_COM_MAP() 43 | 44 | DECLARE_REGISTRY_RESOURCEID(IDR_FLEXASIO) 45 | 46 | public: 47 | CFlexASIO() throw() { Enter("CFlexASIO()", [] {}); } 48 | ~CFlexASIO() throw() { Enter("~CFlexASIO()", [] {}); } 49 | 50 | // IASIO implementation 51 | 52 | ASIOBool init(void* sysHandle) throw() final { 53 | return (Enter("init()", [&] { 54 | if (flexASIO.has_value()) throw ASIOException(ASE_InvalidMode, "init() called more than once"); 55 | flexASIO.emplace(sysHandle); 56 | }) == ASE_OK) ? ASIOTrue : ASIOFalse; 57 | } 58 | void getDriverName(char* name) throw() final { 59 | Enter("getDriverName()", [&] { 60 | strcpy_s(name, 32, "FlexASIO"); 61 | }); 62 | } 63 | long getDriverVersion() throw() final { 64 | Enter("getDriverVersion()", [] {}); 65 | return 0; 66 | } 67 | void getErrorMessage(char* string) throw() final { 68 | Enter("getErrorMessage()", [&] { 69 | std::string_view error(lastError); 70 | constexpr auto maxSize = 123; 71 | if (error.size() > maxSize) error.remove_suffix(error.size() - maxSize); 72 | std::copy(error.begin(), error.end(), string); 73 | string[error.size()] = '\0'; 74 | }); 75 | } 76 | ASIOError getClockSources(ASIOClockSource* clocks, long* numSources) throw() final; 77 | ASIOError setClockSource(long reference) throw() final; 78 | ASIOError getBufferSize(long* minSize, long* maxSize, long* preferredSize, long* granularity) throw() final { 79 | return EnterWithMethod("getBufferSize()", &FlexASIO::GetBufferSize, minSize, maxSize, preferredSize, granularity); 80 | } 81 | 82 | ASIOError getChannels(long* numInputChannels, long* numOutputChannels) throw() final { 83 | return EnterWithMethod("getChannels()", &FlexASIO::GetChannels, numInputChannels, numOutputChannels); 84 | } 85 | ASIOError getChannelInfo(ASIOChannelInfo* info) throw() final { 86 | return EnterWithMethod("getChannelInfo()", &FlexASIO::GetChannelInfo, info); 87 | } 88 | ASIOError canSampleRate(ASIOSampleRate sampleRate) throw() final { 89 | bool result; 90 | const auto error = EnterInitialized("canSampleRate()", [&] { 91 | result = flexASIO->CanSampleRate(sampleRate); 92 | }); 93 | if (error != ASE_OK) return error; 94 | return result ? ASE_OK : ASE_NoClock; 95 | } 96 | ASIOError setSampleRate(ASIOSampleRate sampleRate) throw() final { 97 | return EnterWithMethod("setSampleRate()", &FlexASIO::SetSampleRate, sampleRate); 98 | } 99 | ASIOError getSampleRate(ASIOSampleRate* sampleRate) throw() final { 100 | return EnterWithMethod("getSampleRate()", &FlexASIO::GetSampleRate, sampleRate); 101 | } 102 | 103 | ASIOError createBuffers(ASIOBufferInfo* bufferInfos, long numChannels, long bufferSize, ASIOCallbacks* callbacks) throw() final { 104 | return EnterWithMethod("createBuffers()", &FlexASIO::CreateBuffers, bufferInfos, numChannels, bufferSize, callbacks); 105 | } 106 | ASIOError disposeBuffers() throw() final { 107 | return EnterWithMethod("disposeBuffers()", &FlexASIO::DisposeBuffers); 108 | } 109 | ASIOError getLatencies(long* inputLatency, long* outputLatency) throw() final { 110 | return EnterWithMethod("getLatencies()", &FlexASIO::GetLatencies, inputLatency, outputLatency); 111 | } 112 | 113 | ASIOError start() throw() final { 114 | return EnterWithMethod("start()", &FlexASIO::Start); 115 | } 116 | ASIOError stop() throw() final { 117 | return EnterWithMethod("stop()", &FlexASIO::Stop); 118 | } 119 | ASIOError getSamplePosition(ASIOSamples* sPos, ASIOTimeStamp* tStamp) throw() final { 120 | return EnterWithMethod("getSamplePosition()", &FlexASIO::GetSamplePosition, sPos, tStamp); 121 | } 122 | 123 | ASIOError controlPanel() throw() final { 124 | return EnterWithMethod("controlPanel()", &FlexASIO::ControlPanel); 125 | } 126 | ASIOError future(long selector, void *) throw() final { 127 | return Enter("future()", [&] { 128 | Log() << "Requested future selector: " << ::dechamps_ASIOUtil::GetASIOFutureSelectorString(selector); 129 | throw ASIOException(ASE_InvalidParameter, "future() is not supported"); 130 | }); 131 | } 132 | 133 | ASIOError outputReady() throw() final { 134 | return EnterWithMethod("outputReady()", &FlexASIO::OutputReady); 135 | } 136 | 137 | private: 138 | std::string lastError; 139 | std::optional flexASIO; 140 | 141 | template ASIOError Enter(std::string_view context, Functor functor); 142 | template ASIOError EnterInitialized(std::string_view context, Functor functor); 143 | template ASIOError EnterWithMethod(std::string_view context, Method method, Args&&... args); 144 | }; 145 | 146 | OBJECT_ENTRY_AUTO(__uuidof(::CFlexASIO), CFlexASIO); 147 | 148 | template ASIOError CFlexASIO::Enter(std::string_view context, Functor functor) { 149 | if (IsLoggingEnabled()) Log() << "--- ENTERING CONTEXT: " << context; 150 | ASIOError result; 151 | try { 152 | functor(); 153 | result = ASE_OK; 154 | } 155 | catch (const ASIOException& exception) { 156 | lastError = exception.what(); 157 | result = exception.GetASIOError(); 158 | } 159 | catch (const std::exception& exception) { 160 | lastError = ::dechamps_cpputil::GetNestedExceptionMessage(exception); 161 | result = ASE_HWMalfunction; 162 | } 163 | catch (...) { 164 | lastError = "unknown exception"; 165 | result = ASE_HWMalfunction; 166 | } 167 | if (result == ASE_OK) { 168 | if (IsLoggingEnabled()) Log() << "--- EXITING CONTEXT: " << context << " [OK]"; 169 | } 170 | else { 171 | if (IsLoggingEnabled()) Log() << "--- EXITING CONTEXT: " << context << " (" << ::dechamps_ASIOUtil::GetASIOErrorString(result) << " " << lastError << ")"; 172 | } 173 | return result; 174 | } 175 | 176 | template ASIOError CFlexASIO::EnterInitialized(std::string_view context, Functor functor) { 177 | return Enter(context, [&] { 178 | if (!flexASIO.has_value()) { 179 | throw ASIOException(ASE_InvalidMode, std::string("entered ") + std::string(context) + " but uninitialized state"); 180 | } 181 | functor(); 182 | }); 183 | } 184 | 185 | template ASIOError CFlexASIO::EnterWithMethod(std::string_view context, Method method, Args&&... args) { 186 | return EnterInitialized(context, [&] { return ((*flexASIO).*method)(std::forward(args)...); }); 187 | } 188 | 189 | ASIOError CFlexASIO::getClockSources(ASIOClockSource* clocks, long* numSources) throw() 190 | { 191 | return Enter("getClockSources()", [&] { 192 | if (!clocks || !numSources || *numSources < 1) 193 | throw ASIOException(ASE_InvalidParameter, "invalid parameters to getClockSources()"); 194 | 195 | clocks->index = 0; 196 | clocks->associatedChannel = -1; 197 | clocks->associatedGroup = -1; 198 | clocks->isCurrentSource = ASIOTrue; 199 | strcpy_s(clocks->name, 32, "Internal"); 200 | *numSources = 1; 201 | }); 202 | } 203 | 204 | ASIOError CFlexASIO::setClockSource(long reference) throw() 205 | { 206 | return Enter("setClockSource()", [&] { 207 | Log() << "reference = " << reference; 208 | if (reference != 0) throw ASIOException(ASE_InvalidParameter, "setClockSource() parameter out of bounds"); 209 | }); 210 | } 211 | 212 | } 213 | } 214 | 215 | IASIO* CreateFlexASIO() { 216 | ::CFlexASIO* flexASIO = nullptr; 217 | if (::flexasio::CFlexASIO::CreateInstance(&flexASIO) != S_OK) abort(); 218 | if (flexASIO == nullptr) abort(); 219 | return flexASIO; 220 | } 221 | 222 | void ReleaseFlexASIO(IASIO* const iASIO) { 223 | if (iASIO == nullptr) abort(); 224 | iASIO->Release(); 225 | } 226 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/flexasio.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "config.h" 4 | 5 | #include "portaudio.h" 6 | #include "../FlexASIOUtil/portaudio.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace flexasio { 22 | 23 | class ASIOException : public std::runtime_error { 24 | public: 25 | template ASIOException(ASIOError asioError, Args&&... args) : asioError(asioError), std::runtime_error(std::forward(args)...) {} 26 | ASIOError GetASIOError() const { return asioError; } 27 | 28 | private: 29 | ASIOError asioError; 30 | }; 31 | 32 | class FlexASIO final { 33 | public: 34 | FlexASIO(void* sysHandle); 35 | 36 | void GetBufferSize(long* minSize, long* maxSize, long* preferredSize, long* granularity); 37 | void GetChannels(long* numInputChannels, long* numOutputChannels); 38 | void GetChannelInfo(ASIOChannelInfo* info); 39 | bool CanSampleRate(ASIOSampleRate sampleRate); 40 | void SetSampleRate(ASIOSampleRate requestedSampleRate); 41 | void GetSampleRate(ASIOSampleRate* sampleRateResult); 42 | 43 | void CreateBuffers(ASIOBufferInfo* bufferInfos, long numChannels, long bufferSize, ASIOCallbacks* callbacks); 44 | void DisposeBuffers(); 45 | 46 | void GetLatencies(long* inputLatency, long* outputLatency); 47 | void Start(); 48 | void Stop(); 49 | void GetSamplePosition(ASIOSamples* sPos, ASIOTimeStamp* tStamp); 50 | void OutputReady(); 51 | 52 | void ControlPanel(); 53 | 54 | private: 55 | struct SampleType { 56 | ASIOSampleType asio; 57 | PaSampleFormat pa; 58 | size_t size; 59 | GUID waveSubFormat; 60 | }; 61 | 62 | struct OpenStreamResult { 63 | Stream stream; 64 | bool exclusive; 65 | }; 66 | 67 | class PortAudioHandle { 68 | public: 69 | PortAudioHandle(); 70 | PortAudioHandle(const PortAudioHandle&) = delete; 71 | PortAudioHandle(const PortAudioHandle&&) = delete; 72 | ~PortAudioHandle(); 73 | }; 74 | 75 | class Win32HighResolutionTimer { 76 | public: 77 | Win32HighResolutionTimer(); 78 | Win32HighResolutionTimer(const Win32HighResolutionTimer&) = delete; 79 | Win32HighResolutionTimer(Win32HighResolutionTimer&&) = delete; 80 | ~Win32HighResolutionTimer(); 81 | DWORD GetTimeMilliseconds() const; 82 | }; 83 | 84 | class PreparedState { 85 | public: 86 | PreparedState(FlexASIO& flexASIO, ASIOSampleRate sampleRate, ASIOBufferInfo* asioBufferInfos, long numChannels, long bufferSizeInFrames, ASIOCallbacks* callbacks); 87 | PreparedState(const PreparedState&) = delete; 88 | PreparedState(PreparedState&&) = delete; 89 | 90 | bool IsExclusive() const { return openStreamResult.exclusive; } 91 | 92 | bool IsChannelActive(bool isInput, long channel) const; 93 | 94 | void GetLatencies(long* inputLatency, long* outputLatency); 95 | void Start(); 96 | void Stop(); 97 | 98 | void GetSamplePosition(ASIOSamples* sPos, ASIOTimeStamp* tStamp); 99 | void OutputReady(); 100 | 101 | void RequestReset(); 102 | 103 | private: 104 | struct Buffers 105 | { 106 | Buffers(size_t bufferSetCount, size_t inputChannelCount, size_t outputChannelCount, size_t bufferSizeInFrames, size_t inputSampleSizeInBytes, size_t outputSampleSizeInBytes); 107 | ~Buffers(); 108 | uint8_t* GetInputBuffer(size_t bufferSetIndex, size_t channelIndex) { return buffers.data() + bufferSetIndex * GetBufferSetSizeInBytes() + channelIndex * GetInputBufferSizeInBytes(); } 109 | uint8_t* GetOutputBuffer(size_t bufferSetIndex, size_t channelIndex) { return GetInputBuffer(bufferSetIndex, inputChannelCount) + channelIndex * GetOutputBufferSizeInBytes(); } 110 | size_t GetBufferSetSizeInBytes() const { return buffers.size() / bufferSetCount; } 111 | size_t GetInputBufferSizeInBytes() const { if (buffers.empty()) return 0; return bufferSizeInFrames * inputSampleSizeInBytes; } 112 | size_t GetOutputBufferSizeInBytes() const { if (buffers.empty()) return 0; return bufferSizeInFrames * outputSampleSizeInBytes; } 113 | 114 | const size_t bufferSetCount; 115 | const size_t inputChannelCount; 116 | const size_t outputChannelCount; 117 | const size_t bufferSizeInFrames; 118 | const size_t inputSampleSizeInBytes; 119 | const size_t outputSampleSizeInBytes; 120 | 121 | // This is a giant buffer containing all ASIO buffers. It is organized as follows: 122 | // [ input channel 0 buffer 0 ] [ input channel 1 buffer 0 ] ... [ input channel N buffer 0 ] [ output channel 0 buffer 0 ] [ output channel 1 buffer 0 ] .. [ output channel N buffer 0 ] 123 | // [ input channel 0 buffer 1 ] [ input channel 1 buffer 1 ] ... [ input channel N buffer 1 ] [ output channel 0 buffer 1 ] [ output channel 1 buffer 1 ] .. [ output channel N buffer 1 ] 124 | // The reason why this is a giant blob is to slightly improve performance by (theroretically) improving memory locality. 125 | std::vector buffers; 126 | }; 127 | 128 | class RunningState { 129 | public: 130 | RunningState(PreparedState& preparedState); 131 | 132 | void GetSamplePosition(ASIOSamples* sPos, ASIOTimeStamp* tStamp) const; 133 | void OutputReady(); 134 | 135 | PaStreamCallbackResult StreamCallback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags); 136 | 137 | private: 138 | enum class State { PRIMING, PRIMED, STEADYSTATE }; 139 | 140 | struct SamplePosition { 141 | ASIOSamples samples = { 0 }; 142 | ASIOTimeStamp timestamp = { 0 }; 143 | }; 144 | 145 | class Registration { 146 | public: 147 | Registration(RunningState*& holder, RunningState& runningState) : holder(holder) { 148 | holder = &runningState; 149 | } 150 | ~Registration() { holder = nullptr; } 151 | 152 | private: 153 | RunningState*& holder; 154 | }; 155 | 156 | void Register() { preparedState.runningState = this; } 157 | void Unregister() { preparedState.runningState = nullptr; } 158 | 159 | PreparedState& preparedState; 160 | const bool host_supports_timeinfo; 161 | const bool hostSupportsOutputReady; 162 | State state = hostSupportsOutputReady ? State::PRIMING : State::PRIMED; 163 | // The index of the "unlocked" buffer (or "half-buffer", i.e. 0 or 1) that contains data not currently being processed by the ASIO host. 164 | long driverBufferIndex = state == State::PRIMING ? 1 : 0; 165 | std::atomic samplePosition; 166 | 167 | std::mutex outputReadyMutex; 168 | std::condition_variable outputReadyCondition; 169 | bool outputReady = true; 170 | 171 | Win32HighResolutionTimer win32HighResolutionTimer; 172 | Registration registration{ preparedState.runningState, *this }; 173 | const ActiveStream activeStream; 174 | }; 175 | 176 | static int StreamCallback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) throw(); 177 | 178 | void OnConfigChange(); 179 | 180 | FlexASIO& flexASIO; 181 | const ASIOSampleRate sampleRate; 182 | const ASIOCallbacks callbacks; 183 | 184 | // PortAudio buffer addresses are dynamic and are only valid for the duration of the stream callback. 185 | // In contrast, ASIO buffer addresses are static and are valid for as long as the stream is running. 186 | // Thus we need our own buffer on top of PortAudio's buffers. This doens't add any latency because buffers are copied immediately. 187 | Buffers buffers; 188 | const std::vector bufferInfos; 189 | 190 | const OpenStreamResult openStreamResult; 191 | 192 | // RunningState will set runningState before ownedRunningState has finished constructing. 193 | // This allows PreparedState to properly forward stream callbacks that might fire before RunningState construction is fully complete. 194 | // (See https://github.com/dechamps/FlexASIO/issues/27) 195 | // During steady-state operation, runningState just points to *ownedRunningState. 196 | RunningState* runningState = nullptr; 197 | std::optional ownedRunningState; 198 | ConfigLoader::Watcher configWatcher; 199 | }; 200 | 201 | static const SampleType float32; 202 | static const SampleType int32; 203 | static const SampleType int24; 204 | static const SampleType int16; 205 | static const std::pair sampleTypes[]; 206 | static SampleType ParseSampleType(std::string_view str); 207 | static SampleType WaveFormatToSampleType(const WAVEFORMATEXTENSIBLE& waveFormat); 208 | static SampleType SelectSampleType(PaHostApiTypeId hostApiTypeId, const Device& device, const Config::Stream& streamConfig); 209 | static std::string DescribeSampleType(const SampleType&); 210 | static DWORD SelectChannelMask(PaHostApiTypeId hostApiTypeId, const Device& device, const Config::Stream& streamConfig); 211 | 212 | int GetInputChannelCount() const; 213 | int GetOutputChannelCount() const; 214 | 215 | struct BufferSizes { 216 | long minimum; 217 | long maximum; 218 | long preferred; 219 | long granularity; 220 | }; 221 | BufferSizes ComputeBufferSizes() const; 222 | 223 | long ComputeLatency(long latencyInFrames, bool output, size_t bufferSizeInFrames) const; 224 | long ComputeLatencyFromStream(PaStream* stream, bool output, size_t bufferSizeInFrames) const; 225 | 226 | OpenStreamResult OpenStream(bool inputEnabled, bool outputEnabled, double sampleRate, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData); 227 | 228 | const HWND windowHandle = nullptr; 229 | const ConfigLoader configLoader; 230 | const Config& config = configLoader.Initial(); 231 | 232 | PortAudioDebugRedirector portAudioDebugRedirector; 233 | PortAudioHandle portAudioHandle; 234 | 235 | const HostApi hostApi; 236 | const std::optional inputDevice; 237 | const std::optional outputDevice; 238 | const std::optional inputSampleType; 239 | const std::optional outputSampleType; 240 | const DWORD inputChannelMask; 241 | const DWORD outputChannelMask; 242 | 243 | ASIOSampleRate sampleRate = 0; 244 | bool sampleRateWasAccessed = false; 245 | bool hostSupportsOutputReady = false; 246 | 247 | std::optional preparedState; 248 | }; 249 | 250 | } -------------------------------------------------------------------------------- /.github/workflows/autobuild.yml: -------------------------------------------------------------------------------- 1 | #### Automatically build and upload releases to GitHub #### 2 | 3 | 4 | # see analyse_git_reference.py for implementation of the logic: 5 | # for every push to a branch starting with "autobuild": (can be used during development for tighter supervision of builds) 6 | # - do CodeQl while building for every platform 7 | # - publish the created binaries/packs only as artifacts/appendix of the github-action-run (not as release), and only retain those files for limited period 8 | # for every pull-request to master: 9 | # - do CodeQl while building for every platform 10 | # - publish the created binaries/packs only as artifacts/appendix of the github-action-run (not as release), and only retain those files for limited period 11 | # for every tag that starts with 'r' and has an arbitrary suffix (e.g. beta1, rc1, etc.) 12 | # - do CodeQl while building for every platform 13 | # - publish the created binaries/packs only as artifacts/appendix as a prerelease 14 | # for every tag that starts with 'r' and does not have any suffix: 15 | # - do CodeQl while building for every platform 16 | # - publish the created binaries/packs only as artifacts/appendix as a release 17 | # 18 | 19 | on: 20 | workflow_dispatch: 21 | push: 22 | tags: 23 | - "r*" 24 | branches: 25 | - "autobuild*" # for developers: branches starting with autobuild will be built and evaluated on each push 26 | - "master" 27 | 28 | pull_request: # The branches below must be a subset of the branches in "push" 29 | branches: 30 | - master 31 | 32 | name: Auto-Build 33 | jobs: 34 | create_release: 35 | name: Prepare Auto-Build/Release 36 | runs-on: ubuntu-20.04 37 | outputs: 38 | publish_to_release: ${{ steps.get-build-vars.outputs.PUBLISH_TO_RELEASE }} 39 | version: ${{ steps.get-build-vars.outputs.KOORDASIO_VERSION }} 40 | steps: 41 | # Checkout code 42 | - name: Checkout code 43 | uses: actions/checkout@v2 44 | 45 | # Set variables 46 | # Determine release / pre-release 47 | - name: Get koordasio build info, determine actions & variables 48 | run: ./.github/autobuild/get_build_vars.py 49 | id: get-build-vars 50 | 51 | release_assets: 52 | name: Build assets for ${{ matrix.config.config_name }} 53 | needs: create_release 54 | strategy: 55 | fail-fast: false 56 | matrix: # Think of this like a foreach loop. Basically runs the steps with every combination of the contents of this. More info: https://docs.github.com/en/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 57 | config: 58 | - config_name: Windows 10+ (exe, msix) 59 | target_os: windows 60 | building_on_os: windows-2022 61 | base_command: powershell .\.github\autobuild\windows.ps1 -Stage 62 | # uses_codeql: true 63 | 64 | runs-on: windows-2022 65 | steps: 66 | # Checkout code 67 | - name: Checkout code 68 | uses: actions/checkout@v2 69 | with: 70 | submodules: true 71 | 72 | # Prepare (install QT & dependencies) 73 | - name: "Prepare Build for ${{ matrix.config.config_name }}" 74 | id: setup 75 | run: ${{ matrix.config.base_command }} setup 76 | env: 77 | koordasio_project_path: ${{ github.workspace }} 78 | koordasio_buildversionstring: ${{ needs.create_release.outputs.version }} 79 | 80 | # # from Koord build .... 81 | # - name: Pre-build KoordASIO on Windows - set up msvc dev cmd 82 | # uses: ilammy/msvc-dev-cmd@v1 83 | # with: 84 | # arch: amd64 85 | # - name: Pre-build KoordASIO on Windows - cmake 86 | # run: cmake -S KoordASIO/src -B KoordASIO/src/out/build/x64-Release -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX:PATH=${{ github.workspace }}/KoordASIO/src/out/install/x64-Release 87 | # - name: Pre-build KoordASIO on Windows - cmake --build 88 | # run: cmake --build KoordASIO/src/out/build/x64-Release 89 | # - name: Pre-build KoordASIO on Windows - cmake --install 90 | # run: cmake --install KoordASIO/src/out/build/x64-Release 91 | 92 | # Build 93 | - name: "Build for ${{ matrix.config.config_name }}" 94 | id: build 95 | run: ${{ matrix.config.base_command }} build 96 | env: 97 | koordasio_project_path: ${{ github.workspace }} 98 | koordasio_buildversionstring: ${{ needs.create_release.outputs.version }} 99 | WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} 100 | WINDOWS_CODESIGN_PWD: ${{ secrets.WINDOWS_CODESIGN_PWD }} 101 | 102 | # Get artifacts 103 | - name: "Post-Build for ${{ matrix.config.config_name }}" 104 | id: get-artifacts 105 | run: ${{ matrix.config.base_command }} get-artifacts 106 | env: 107 | koordasio_project_path: ${{ github.workspace }} 108 | koordasio_buildversionstring: ${{ needs.create_release.outputs.version }} 109 | 110 | # Upload Artifact1 to Job 111 | - name: Upload Artifact 1 to Job 112 | if: ${{ steps.get-artifacts.outputs.artifact_1 }} 113 | uses: actions/upload-artifact@v3 114 | with: 115 | name: ${{ steps.get-artifacts.outputs.artifact_1 }} 116 | path: deploy/${{ steps.get-artifacts.outputs.artifact_1 }} 117 | retention-days: 31 118 | if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` 119 | 120 | # Upload Artifact2 to Job 121 | - name: Upload Artifact 2 to Job 122 | if: ${{ steps.get-artifacts.outputs.artifact_2 }} 123 | uses: actions/upload-artifact@v3 124 | with: 125 | name: ${{ steps.get-artifacts.outputs.artifact_2 }} 126 | path: deploy/${{ steps.get-artifacts.outputs.artifact_2 }} 127 | retention-days: 31 128 | if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` 129 | 130 | # Create Release and upload Artifact1 131 | - name: Create Release1 ${{needs.create_release.outputs.release_tag}} ${{needs.create_release.outputs.release_title}} 132 | if: >- 133 | needs.create_release.outputs.publish_to_release == 'true' 134 | id: create-release-n-upload1 135 | uses: softprops/action-gh-release@v1 136 | env: 137 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 138 | with: 139 | tag_name: ${{ steps.get-build-vars.outputs.RELEASE_TAG }} 140 | name: ${{ steps.get-build-vars.outputs.RELEASE_TITLE }} 141 | # body_path: ${{ env.release_changelog_path }} 142 | prerelease: ${{ steps.get-build-vars.outputs.IS_PRERELEASE }} 143 | draft: true 144 | files: deploy/${{ steps.get-artifacts.outputs.artifact_1 }} 145 | 146 | # Upload Artifact2 to release 147 | - name: Create Release2 ${{needs.create_release.outputs.release_tag}} ${{needs.create_release.outputs.release_title}} 148 | if: >- 149 | steps.get-artifacts.outputs.artifact_2 != '' && 150 | needs.create_release.outputs.publish_to_release == 'true' 151 | id: create-release-n-upload2 152 | uses: softprops/action-gh-release@v1 153 | env: 154 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 155 | with: 156 | tag_name: ${{ steps.get-build-vars.outputs.RELEASE_TAG }} 157 | name: ${{ steps.get-build-vars.outputs.RELEASE_TITLE }} 158 | # body_path: ${{ env.release_changelog_path }} 159 | prerelease: ${{ steps.get-build-vars.outputs.IS_PRERELEASE }} 160 | draft: true 161 | files: deploy/${{ steps.get-artifacts.outputs.artifact_2 }} 162 | 163 | # # Upload Artifact1 to Release 164 | # - name: Upload Artifact 1 to Release 165 | # if: ${{ (steps.step_cmd3_postbuild.outputs.artifact_1 != '') && contains(needs.create_release.outputs.publish_to_release, 'true') }} 166 | # id: upload-release-asset1 167 | # uses: actions/upload-release-asset@v1 168 | # env: 169 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 170 | # with: 171 | # upload_url: ${{ needs.create_release.outputs.upload_url }} # See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 172 | # asset_path: deploy/${{ steps.step_cmd3_postbuild.outputs.artifact_1 }} 173 | # asset_name: ${{ steps.step_cmd3_postbuild.outputs.artifact_1 }} 174 | # asset_content_type: application/octet-stream 175 | # # Upload Artifact2 to Release 176 | # - name: Upload Artifact 2 to Release 177 | # if: ${{ (steps.step_cmd3_postbuild.outputs.artifact_2 != '') && contains(needs.create_release.outputs.publish_to_release, 'true') }} 178 | # id: upload-release-asset2 179 | # uses: actions/upload-release-asset@v1 180 | # env: 181 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 182 | # with: 183 | # upload_url: ${{ needs.create_release.outputs.upload_url }} # See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 184 | # asset_path: deploy/${{ steps.step_cmd3_postbuild.outputs.artifact_2 }} 185 | # asset_name: ${{ steps.step_cmd3_postbuild.outputs.artifact_2 }} 186 | # asset_content_type: application/octet-stream 187 | 188 | 189 | #FIXME - leave out for now 190 | # # Run CodeQL tools for code-scanning for security 191 | # - name: Perform CodeQL Analysis 192 | # if: matrix.config.uses_codeql 193 | # uses: github/codeql-action/analyze@v1 194 | -------------------------------------------------------------------------------- /src/kdasioconfig/kdasioconfig.cpp: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** KoordASIO 3 | */ 4 | #include 5 | #include 6 | #include 7 | #include "kdasioconfig.h" 8 | #include "toml.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | KdASIOConfigBase::KdASIOConfigBase(QWidget *parent) 17 | : QMainWindow(parent) 18 | { 19 | setupUi(this); 20 | } 21 | 22 | KdASIOConfigBase::~KdASIOConfigBase() {} 23 | 24 | KdASIOConfig::KdASIOConfig(QWidget *parent) 25 | : KdASIOConfigBase(parent) 26 | { 27 | this->setAttribute(Qt::WA_AlwaysShowToolTips,true); 28 | 29 | // init mmcpl proc 30 | mmcplProc = nullptr; 31 | // init our singleton QMediaDevices object 32 | m_devices = new QMediaDevices(); 33 | 34 | // set up signals 35 | connect(sharedPushButton, &QPushButton::clicked, this, &KdASIOConfig::sharedModeSet); 36 | connect(exclusivePushButton, &QPushButton::clicked, this, &KdASIOConfig::exclusiveModeSet); 37 | connect(inputDeviceBox, QOverload::of(&QComboBox::activated), this, &KdASIOConfig::inputDeviceChanged); 38 | connect(outputDeviceBox, QOverload::of(&QComboBox::activated), this, &KdASIOConfig::outputDeviceChanged); 39 | connect(inputAudioSettButton, &QPushButton::pressed, this, &KdASIOConfig::inputAudioSettClicked); 40 | connect(outputAudioSettButton, &QPushButton::pressed, this, &KdASIOConfig::outputAudioSettClicked); 41 | connect(bufferSizeSlider, &QSlider::valueChanged, this, &KdASIOConfig::bufferSizeChanged); 42 | connect(bufferSizeSlider, &QSlider::valueChanged, this, &KdASIOConfig::bufferSizeDisplayChange); 43 | // connect footer buttons 44 | connect(koordLiveButton, &QPushButton::pressed, this, &KdASIOConfig::koordLiveClicked); 45 | connect(githubButton, &QPushButton::pressed, this, &KdASIOConfig::githubClicked); 46 | connect(versionButton, &QPushButton::pressed, this, &KdASIOConfig::versionButtonClicked); 47 | // for device refresh 48 | connect(m_devices, &QMediaDevices::audioInputsChanged, this, &KdASIOConfig::updateInputsList); 49 | connect(m_devices, &QMediaDevices::audioOutputsChanged, this, &KdASIOConfig::updateOutputsList); 50 | 51 | // for URLs 52 | koordLiveButton->setCursor(Qt::PointingHandCursor); 53 | githubButton->setCursor(Qt::PointingHandCursor); 54 | versionButton->setCursor(Qt::PointingHandCursor); 55 | 56 | // populate input device choices 57 | inputDeviceBox->clear(); 58 | const auto input_devices = m_devices->audioInputs(); 59 | for (auto &deviceInfo: input_devices) 60 | inputDeviceBox->addItem(deviceInfo.description(), QVariant::fromValue(deviceInfo)); 61 | 62 | // populate output device choices 63 | outputDeviceBox->clear(); 64 | const auto output_devices = m_devices->audioOutputs(); 65 | for (auto &deviceInfo: output_devices) 66 | outputDeviceBox->addItem(deviceInfo.description(), QVariant::fromValue(deviceInfo)); 67 | 68 | // parse .KoordASIO.toml 69 | // FIXME - doesn't actually test that the selected devices are correct with current device list 70 | std::ifstream ifs; 71 | ifs.exceptions ( std::ifstream::failbit | std::ifstream::badbit ); 72 | try { 73 | ifs.open(fullpath.toStdString(), std::ifstream::in); 74 | toml::ParseResult pr = toml::parse(ifs); 75 | qDebug("Attempted to parse toml file..."); 76 | ifs.close(); 77 | if (!pr.valid()) { 78 | setDefaults(); 79 | } else { 80 | setValuesFromToml(&ifs, &pr); 81 | } 82 | } 83 | catch (std::ifstream::failure e) { 84 | qDebug("Failed to open file ..."); 85 | setDefaults(); 86 | } 87 | 88 | } 89 | 90 | void KdASIOConfig::updateInputsList() { 91 | inputDeviceBox->clear(); 92 | const auto input_devices = m_devices->audioInputs(); 93 | for (auto &deviceInfo: input_devices) 94 | inputDeviceBox->addItem(deviceInfo.description(), QVariant::fromValue(deviceInfo)); 95 | // write the new resultant config 96 | inputDeviceName = inputDeviceBox->currentText(); 97 | writeTomlFile(); 98 | } 99 | 100 | void KdASIOConfig::updateOutputsList() { 101 | // populate output device choices 102 | outputDeviceBox->clear(); 103 | const auto output_devices = m_devices->audioOutputs(); 104 | for (auto &deviceInfo: output_devices) 105 | outputDeviceBox->addItem(deviceInfo.description(), QVariant::fromValue(deviceInfo)); 106 | qInfo() << "Updating Outputs ..."; 107 | // write the new resultant config 108 | outputDeviceName = outputDeviceBox->currentText(); 109 | writeTomlFile(); 110 | } 111 | 112 | void KdASIOConfig::setValuesFromToml(std::ifstream *ifs, toml::ParseResult *pr) 113 | { 114 | qInfo("Parsed a valid TOML file."); 115 | // only recognise our accepted INPUT values - the others are hardcoded 116 | const toml::Value& v = pr->value; 117 | // get bufferSize 118 | const toml::Value* bss = v.find("bufferSizeSamples"); 119 | if (bss && bss->is()) { 120 | if (bss->as() == 32||64||128||256||512||1024||2048) { 121 | bufferSize = bss->as(); 122 | } else { 123 | bufferSize = 64; 124 | } 125 | // update UI 126 | bufferSizeSlider->setValue(bufferSizes.indexOf(bufferSize)); 127 | bufferSizeDisplay->display(bufferSize); 128 | // update conf 129 | bufferSizeChanged(bufferSizes.indexOf(bufferSize)); 130 | } 131 | // get input stream stuff 132 | const toml::Value* input_dev = v.find("input.device"); 133 | if (input_dev && input_dev->is()) { 134 | // if setCurrentText fails some sensible choice is made 135 | inputDeviceBox->setCurrentText(QString::fromStdString(input_dev->as())); 136 | inputDeviceChanged(inputDeviceBox->currentIndex()); 137 | } else { 138 | inputDeviceBox->setCurrentText("Default Input Device"); 139 | inputDeviceChanged(inputDeviceBox->currentIndex()); 140 | } 141 | const toml::Value* input_excl = v.find("input.wasapiExclusiveMode"); 142 | if (input_excl && input_excl->is()) { 143 | exclusive_mode = input_excl->as(); 144 | } else { 145 | exclusive_mode = false; 146 | } 147 | // get output stream stuff 148 | const toml::Value* output_dev = v.find("output.device"); 149 | if (output_dev && output_dev->is()) { 150 | // if setCurrentText fails some sensible choice is made 151 | outputDeviceBox->setCurrentText(QString::fromStdString(output_dev->as())); 152 | outputDeviceChanged(outputDeviceBox->currentIndex()); 153 | } else { 154 | outputDeviceBox->setCurrentText("Default Output Device"); 155 | outputDeviceChanged(outputDeviceBox->currentIndex()); 156 | } 157 | const toml::Value* output_excl = v.find("output.wasapiExclusiveMode"); 158 | if (output_excl && output_excl->is()) { 159 | exclusive_mode = output_excl->as(); 160 | } else { 161 | exclusive_mode = false; 162 | } 163 | setOperationMode(); 164 | 165 | } 166 | 167 | void KdASIOConfig::setDefaults() 168 | { 169 | // set defaults 170 | qInfo("Setting defaults"); 171 | bufferSize = 32; 172 | exclusive_mode = true; 173 | // find system audio device defaults 174 | QAudioDevice inputInfo(QMediaDevices::defaultAudioInput()); 175 | inputDeviceName = inputInfo.description(); 176 | QAudioDevice outputInfo(QMediaDevices::defaultAudioOutput()); 177 | outputDeviceName = outputInfo.description(); 178 | // set stuff - up to 4 file updates in quick succession - FIXME 179 | bufferSizeSlider->setValue(bufferSizes.indexOf(bufferSize)); 180 | bufferSizeDisplay->display(bufferSize); 181 | bufferSizeChanged(bufferSizes.indexOf(bufferSize)); 182 | inputDeviceBox->setCurrentText(inputDeviceName); 183 | inputDeviceChanged(inputDeviceBox->currentIndex()); 184 | outputDeviceBox->setCurrentText(outputDeviceName); 185 | outputDeviceChanged(outputDeviceBox->currentIndex()); 186 | setOperationMode(); 187 | } 188 | 189 | 190 | void KdASIOConfig::writeTomlFile() 191 | { 192 | // REF: https://github.com/dechamps/FlexASIO/blob/master/CONFIGURATION.md 193 | // Write MINIMAL config to .KoordASIO.toml, like this: 194 | /* 195 | backend = "Windows WASAPI" 196 | bufferSizeSamples = bufferSize 197 | 198 | [input] 199 | device=inputDevice 200 | suggestedLatencySeconds = 0.0 201 | wasapiExclusiveMode = inputExclusiveMode 202 | 203 | [output] 204 | device=outputDevice 205 | suggestedLatencySeconds = 0.0 206 | wasapiExclusiveMode = outputExclusiveMode 207 | */ 208 | QFile file(fullpath); 209 | if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) 210 | return; 211 | QTextStream out(&file); 212 | // need to explicitly set UTF-8 for non-ASCII character support 213 | out.setEncoding(QStringConverter::Utf8); 214 | // out.setCodec("UTF-8"); 215 | //FIXME should really write to intermediate buffer, THEN to file - to make single write on file 216 | out << "backend = \"Windows WASAPI\"" << "\n" 217 | << "bufferSizeSamples = " << bufferSize << "\n" 218 | << "\n" 219 | << "[input]" << "\n" 220 | << "device = \"" << inputDeviceName << "\"\n" 221 | << "suggestedLatencySeconds = 0.0" << "\n" 222 | << "wasapiExclusiveMode = " << (exclusive_mode ? "true" : "false") << "\n" 223 | << "\n" 224 | << "[output]" << "\n" 225 | << "device = \"" << outputDeviceName << "\"\n" 226 | << "suggestedLatencySeconds = 0.0" << "\n" 227 | << "wasapiExclusiveMode = " << (exclusive_mode ? "true" : "false") << "\n"; 228 | // qInfo("Just wrote toml file..."); 229 | 230 | } 231 | 232 | void KdASIOConfig::bufferSizeChanged(int idx) 233 | { 234 | // select from 32 , 64, 128, 256, 512, 1024, 2048 235 | // This a) gives a nice easy UI rather than choosing your own integer 236 | // AND b) makes it easier to do a live-refresh of the toml file, 237 | // THUS avoiding lots of spurious intermediate updates on buffer changes 238 | bufferSize = bufferSizes[idx]; 239 | bufferSizeSlider->setValue(idx); 240 | // Don't do any latency calculation for now, it is misleading as doesn't account for much of the whole audio chain 241 | // latencyLabel->setText(QString::number(double(bufferSize) / 48, 'f', 2)); 242 | writeTomlFile(); 243 | } 244 | 245 | void KdASIOConfig::bufferSizeDisplayChange(int idx) 246 | { 247 | bufferSize = bufferSizes[idx]; 248 | bufferSizeDisplay->display(bufferSize); 249 | } 250 | 251 | void KdASIOConfig::setOperationMode() 252 | { 253 | if (exclusive_mode) { 254 | exclusiveModeSet(); 255 | } 256 | else { 257 | sharedModeSet(); 258 | } 259 | } 260 | 261 | void KdASIOConfig::sharedModeSet() 262 | { 263 | sharedPushButton->setChecked(true); 264 | exclusive_mode = false; 265 | writeTomlFile(); 266 | } 267 | 268 | void KdASIOConfig::exclusiveModeSet() 269 | { 270 | exclusivePushButton->setChecked(true); 271 | exclusive_mode = true; 272 | writeTomlFile(); 273 | } 274 | 275 | void KdASIOConfig::inputDeviceChanged(int idx) 276 | { 277 | if (inputDeviceBox->count() == 0) 278 | return; 279 | // device has changed 280 | m_inputDeviceInfo = inputDeviceBox->itemData(idx).value(); 281 | inputDeviceName = m_inputDeviceInfo.description(); 282 | writeTomlFile(); 283 | } 284 | 285 | void KdASIOConfig::outputDeviceChanged(int idx) 286 | { 287 | if (outputDeviceBox->count() == 0) 288 | return; 289 | // device has changed 290 | m_outputDeviceInfo = outputDeviceBox->itemData(idx).value(); 291 | outputDeviceName = m_outputDeviceInfo.description(); 292 | writeTomlFile(); 293 | } 294 | 295 | void KdASIOConfig::inputAudioSettClicked() 296 | { 297 | // open Windows audio input settings control panel 298 | //FIXME - this process control does NOT work as Windows forks+kills the started process immediately? or something 299 | if (mmcplProc != nullptr) { 300 | mmcplProc->kill(); 301 | } 302 | mmcplProc = new QProcess(this); 303 | mmcplProc->start("control", QStringList() << inputAudioSettPath); 304 | } 305 | 306 | void KdASIOConfig::outputAudioSettClicked() 307 | { 308 | // open Windows audio output settings control panel 309 | //FIXME - this process control does NOT work as Windows forks+kills the started process immediately? or something 310 | if (mmcplProc != nullptr) { 311 | mmcplProc->kill(); 312 | } 313 | mmcplProc = new QProcess(this); 314 | mmcplProc->start("control", QStringList() << outputAudioSettPath); 315 | } 316 | 317 | 318 | //void KdASIOConfig::bufferInfoClicked() 319 | //{ 320 | // QDialog *qd = new QDialog(this); 321 | // QLabel *qlab = new QLabel(); 322 | // QString inputInfoText = "" + 323 | // tr ( "BUFFER SIZE - Tips" ) + 324 | // " " + 325 | // "
" + "
" + 326 | // "Select the size of the ASIO Buffer, by the number of samples. " + 327 | // "
" + "
" + 328 | // "A lower size may cause glitches in your sound, while higher size causes higher latency."; 329 | // qlab->setText(inputInfoText); 330 | // QVBoxLayout *layout = new QVBoxLayout(); 331 | // layout->addWidget(qlab); 332 | // qd->setLayout(layout); 333 | // qd->setPalette(QPalette("#1d1f21")); 334 | // qd->show(); 335 | //} 336 | 337 | void KdASIOConfig::koordLiveClicked() 338 | { 339 | QDesktopServices::openUrl(QUrl("https://koord.live", QUrl::TolerantMode)); 340 | } 341 | 342 | void KdASIOConfig::versionButtonClicked() 343 | { 344 | QDesktopServices::openUrl(QUrl("https://github.com/koord-live/KoordASIO/releases", QUrl::TolerantMode)); 345 | } 346 | 347 | void KdASIOConfig::githubClicked() 348 | { 349 | QDesktopServices::openUrl(QUrl("https://github.com/koord-live/KoordASIO", QUrl::TolerantMode)); 350 | } 351 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIO/config.cpp: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS // Avoid issues with toml.h 2 | 3 | #include "config.h" 4 | 5 | #include 6 | #include 7 | 8 | #include "log.h" 9 | #include "../FlexASIOUtil/shell.h" 10 | 11 | namespace flexasio { 12 | 13 | namespace { 14 | 15 | // constexpr auto configFileName = L".KoordASIO-builtin.toml"; 16 | constexpr auto configFileName = L".KoordASIO.toml"; 17 | 18 | toml::Value LoadConfigToml(const std::filesystem::path& path) { 19 | Log() << "Attempting to load configuration file: " << path; 20 | 21 | std::ifstream stream; 22 | stream.exceptions(stream.badbit | stream.failbit); 23 | try { 24 | stream.open(path); 25 | } 26 | catch (const std::exception& exception) { 27 | Log() << "Unable to open configuration file: " << exception.what(); 28 | return toml::Table(); 29 | } 30 | stream.exceptions(stream.badbit); 31 | 32 | const auto parseResult = [&] { 33 | try { 34 | const auto parseResult = toml::parse(stream); 35 | if (!parseResult.valid()) throw std::runtime_error(parseResult.errorReason); 36 | return parseResult; 37 | } 38 | catch (...) { 39 | std::throw_with_nested(std::runtime_error("TOML parse error")); 40 | } 41 | }(); 42 | 43 | Log() << "Configuration file successfully parsed as valid TOML: " << parseResult.value; 44 | 45 | return parseResult.value; 46 | } 47 | 48 | template void ProcessOption(const toml::Table& table, const std::string& key, Functor functor) { 49 | const auto value = table.find(key); 50 | if (value == table.end()) return; 51 | try { 52 | return functor(value->second); 53 | } 54 | catch (const std::exception& exception) { 55 | throw std::runtime_error(std::string("in option '") + key + "': " + exception.what()); 56 | } 57 | } 58 | 59 | template void ProcessTypedOption(const toml::Table& table, const std::string& key, Functor functor) { 60 | return ProcessOption(table, key, [&](const toml::Value& value) { return functor(value.as()); }); 61 | } 62 | 63 | template struct RemoveOptional { using Value = T; }; 64 | template struct RemoveOptional> { using Value = T; }; 65 | 66 | template void SetOption(const toml::Table& table, const std::string& key, T& option, Validator validator) { 67 | ProcessTypedOption::Value>(table, key, [&](const RemoveOptional::Value& value) { 68 | validator(value); 69 | option = value; 70 | }); 71 | } 72 | template void SetOption(const toml::Table& table, const std::string& key, T& option) { 73 | return SetOption(table, key, option, [](const T&) {}); 74 | } 75 | 76 | void ValidateChannelCount(const int& channelCount) { 77 | if (channelCount <= 0) throw std::runtime_error("channel count must be strictly positive - to disable a stream direction, set the 'device' option to the empty string \"\" instead"); 78 | } 79 | 80 | void ValidateSuggestedLatency(const double& suggestedLatencySeconds) { 81 | if (!(suggestedLatencySeconds >= 0 && suggestedLatencySeconds <= 3600)) throw std::runtime_error("suggested latency must be between 0 and 3600 seconds"); 82 | } 83 | 84 | void ValidateBufferSize(const int64_t& bufferSizeSamples) { 85 | if (bufferSizeSamples <= 0) throw std::runtime_error("buffer size must be strictly positive"); 86 | if (bufferSizeSamples >= (std::numeric_limits::max)()) throw std::runtime_error("buffer size is too large"); 87 | } 88 | 89 | void SetStream(const toml::Table& table, Config::Stream& stream) { 90 | if (table.find("device") != table.end() && table.find("deviceRegex") != table.end()) 91 | throw std::runtime_error("the device and deviceRegex options cannot be specified at the same time"); 92 | ProcessTypedOption(table, "device", [&](const std::string& deviceString) { 93 | if (deviceString == "") stream.device = Config::NoDevice(); 94 | else stream.device = deviceString; 95 | }); 96 | ProcessTypedOption(table, "deviceRegex", [&](const std::string& deviceRegexString) { 97 | if (deviceRegexString == "") throw std::runtime_error("the deviceRegex option cannot be empty"); 98 | try { 99 | stream.device = Config::DeviceRegex(deviceRegexString); 100 | } 101 | catch (...) { 102 | std::throw_with_nested(std::runtime_error("Invalid regex in deviceRegex option")); 103 | } 104 | }); 105 | 106 | SetOption(table, "channels", stream.channels, ValidateChannelCount); 107 | SetOption(table, "sampleType", stream.sampleType); 108 | SetOption(table, "suggestedLatencySeconds", stream.suggestedLatencySeconds, ValidateSuggestedLatency); 109 | SetOption(table, "wasapiExclusiveMode", stream.wasapiExclusiveMode); 110 | SetOption(table, "wasapiAutoConvert", stream.wasapiAutoConvert); 111 | SetOption(table, "wasapiExplicitSampleFormat", stream.wasapiExplicitSampleFormat); 112 | } 113 | 114 | void SetConfig(const toml::Table& table, Config& config) { 115 | SetOption(table, "backend", config.backend); 116 | SetOption(table, "bufferSizeSamples", config.bufferSizeSamples, ValidateBufferSize); 117 | ProcessTypedOption(table, "input", [&](const toml::Table& table) { SetStream(table, config.input); }); 118 | ProcessTypedOption(table, "output", [&](const toml::Table& table) { SetStream(table, config.output); }); 119 | } 120 | 121 | 122 | Config LoadConfig(const std::filesystem::path& path) { 123 | toml::Value tomlValue; 124 | try { 125 | tomlValue = LoadConfigToml(path); 126 | } 127 | catch (...) { 128 | std::throw_with_nested(std::runtime_error("Unable to load configuration file")); 129 | } 130 | 131 | try { 132 | Config config; 133 | SetConfig(tomlValue.as(), config); 134 | return config; 135 | } 136 | catch (...) { 137 | std::throw_with_nested(std::runtime_error("Invalid configuration")); 138 | } 139 | } 140 | 141 | } 142 | 143 | void ConfigLoader::Watcher::HandleCloser::operator()(HANDLE handle) { 144 | if (::CloseHandle(handle) == 0) 145 | throw std::system_error(::GetLastError(), std::system_category(), "unable to close handle"); 146 | } 147 | 148 | ConfigLoader::Watcher::Watcher(const ConfigLoader& configLoader, std::function onConfigChange) : 149 | configLoader(configLoader), 150 | onConfigChange(std::move(onConfigChange)), 151 | stopEvent([&] { 152 | const auto handle = CreateEventA(NULL, TRUE, FALSE, NULL); 153 | if (handle == NULL) 154 | throw std::system_error(::GetLastError(), std::system_category(), "Unable to create stop event"); 155 | return UniqueHandle(handle); 156 | }()), 157 | directory([&] { 158 | Log() << "Opening config directory for watching"; 159 | const auto handle = ::CreateFileW( 160 | configLoader.configDirectory.wstring().c_str(), 161 | FILE_LIST_DIRECTORY, 162 | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 163 | /*lpSecurityAttributes=*/NULL, 164 | OPEN_EXISTING, 165 | FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, 166 | /*hTemplateFile=*/NULL); 167 | if (handle == INVALID_HANDLE_VALUE) 168 | throw std::system_error(::GetLastError(), std::system_category(), "Unable to open config directory for watching"); 169 | return UniqueHandle(handle); 170 | }()) { 171 | Log() << "Starting configuration file watcher"; 172 | StartWatching(); 173 | OnConfigFileEvent(); 174 | thread = std::thread([this] { RunThread(); }); 175 | } 176 | 177 | ConfigLoader::Watcher::~Watcher() noexcept(false) { 178 | Log() << "Signaling config watcher thread to stop"; 179 | if (!SetEvent(stopEvent.get())) 180 | throw std::system_error(::GetLastError(), std::system_category(), "Unable to set stop event"); 181 | Log() << "Waiting for config watcher thread to finish"; 182 | thread.join(); 183 | Log() << "Joined config watcher thread"; 184 | } 185 | 186 | void ConfigLoader::Watcher::RunThread() { 187 | Log() << "Config watcher thread running"; 188 | 189 | try { 190 | for (;;) { 191 | std::array handles = { stopEvent.get(), overlapped.overlapped.hEvent }; 192 | const auto waitResult = ::WaitForMultipleObjects(DWORD(handles.size()), handles.data(), /*bWaitAll=*/FALSE, INFINITE); 193 | if (waitResult == WAIT_OBJECT_0) break; 194 | else if (waitResult == WAIT_OBJECT_0 + 1) OnEvent(); 195 | else throw std::system_error(::GetLastError(), std::system_category(), "Unable to wait for events"); 196 | } 197 | } 198 | catch (const std::exception& exception) { 199 | Log() << "Config watcher thread encountered error: " << ::dechamps_cpputil::GetNestedExceptionMessage(exception); 200 | } 201 | catch (...) { 202 | Log() << "Config watcher thread encountered unknown exception"; 203 | } 204 | 205 | Log() << "Config watcher thread stopping"; 206 | } 207 | 208 | void ConfigLoader::Watcher::OnEvent() { 209 | // Note: we need to be careful about logging here - since the logfile is in the same directory as the config file, 210 | // we could end up with directory change events entering an infinite feedback loop. 211 | 212 | bool configFileEvent = false; 213 | if (!FillNotifyInformationBuffer()) { 214 | Log() << "Config directory event buffer overflow"; 215 | // We don't know if something happened to the logfile, so assume it did. 216 | configFileEvent = true; 217 | } 218 | else configFileEvent = FindConfigFileEvents(); 219 | 220 | if (configFileEvent) { 221 | Debounce(); 222 | OnConfigFileEvent(); 223 | } 224 | 225 | StartWatching(); 226 | } 227 | 228 | void ConfigLoader::Watcher::Debounce() { 229 | // It's best to debounce events that arrive in quick succession, otherwise we might attempt to read the file while it's being changed, 230 | // resulting in spurious resets. 231 | // (e.g. the Visual Studio Code editor will empty the file first before writing the new contents) 232 | // Another reason to debounce is that it might make it less likely we'll run into file locking issues. 233 | // We do this by sleeping for a while, and getting rid of all events that occurred in the mean time. 234 | 235 | Log() << "Debouncing config file events"; 236 | StartWatching(); 237 | Log() << "Sleeping"; 238 | ::Sleep(250); 239 | Log() << "Cancelling directory event watch"; 240 | // We could use CancelIoEx(), but for some reason that doesn't work (it fails with ERROR_NOT_FOUND). 241 | if (!::CancelIo(directory.get())) 242 | throw std::system_error(::GetLastError(), std::system_category(), "Unable to cancel debounce"); 243 | Log() << "Draining directory event buffer"; 244 | FillNotifyInformationBuffer(); 245 | } 246 | 247 | bool ConfigLoader::Watcher::FillNotifyInformationBuffer() { 248 | DWORD size; 249 | if (!::GetOverlappedResult(directory.get(), &overlapped.overlapped, &size, /*bWait=*/TRUE)) 250 | throw std::system_error(::GetLastError(), std::system_category(), "GetOverlappedResult() failed"); 251 | return size > 0; 252 | } 253 | 254 | bool ConfigLoader::Watcher::FindConfigFileEvents() { 255 | const char* fileNotifyInformationPtr = fileNotifyInformationBuffer; 256 | for (;;) { 257 | constexpr auto fileNotifyInformationHeaderSize = offsetof(FILE_NOTIFY_INFORMATION, FileName); 258 | FILE_NOTIFY_INFORMATION fileNotifyInformationHeader; 259 | memcpy(&fileNotifyInformationHeader, fileNotifyInformationPtr, fileNotifyInformationHeaderSize); 260 | 261 | std::wstring fileName(fileNotifyInformationHeader.FileNameLength / sizeof(wchar_t), 0); 262 | memcpy(fileName.data(), fileNotifyInformationPtr + fileNotifyInformationHeaderSize, fileNotifyInformationHeader.FileNameLength); 263 | if (fileName == configFileName) { 264 | // Here we can safely log. 265 | Log() << "Configuration file directory change received: NextEntryOffset = " << fileNotifyInformationHeader.NextEntryOffset 266 | << " Action = " << fileNotifyInformationHeader.Action 267 | << " FileNameLength = " << fileNotifyInformationHeader.FileNameLength; 268 | 269 | if (fileNotifyInformationHeader.Action == FILE_ACTION_ADDED || 270 | fileNotifyInformationHeader.Action == FILE_ACTION_REMOVED || 271 | fileNotifyInformationHeader.Action == FILE_ACTION_MODIFIED || 272 | fileNotifyInformationHeader.Action == FILE_ACTION_RENAMED_NEW_NAME) { 273 | return true; 274 | } 275 | } 276 | 277 | if (fileNotifyInformationHeader.NextEntryOffset == 0) break; 278 | fileNotifyInformationPtr += fileNotifyInformationHeader.NextEntryOffset; 279 | } 280 | return false; 281 | } 282 | 283 | ConfigLoader::Watcher::OverlappedWithEvent::OverlappedWithEvent() { 284 | overlapped.hEvent = CreateEventA(NULL, TRUE, FALSE, NULL); 285 | if (overlapped.hEvent == NULL) 286 | throw std::system_error(::GetLastError(), std::system_category(), "Unable to create watch event"); 287 | } 288 | ConfigLoader::Watcher::OverlappedWithEvent::~OverlappedWithEvent() { 289 | UniqueHandle(overlapped.hEvent); 290 | } 291 | 292 | void ConfigLoader::Watcher::StartWatching() { 293 | if (::ReadDirectoryChangesW( 294 | directory.get(), 295 | fileNotifyInformationBuffer, sizeof(fileNotifyInformationBuffer), 296 | /*bWatchSubtree=*/FALSE, 297 | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE, 298 | /*lpBytesReturned=*/NULL, 299 | &overlapped.overlapped, 300 | /*lpCompletionRoutine=*/NULL) == 0) 301 | throw std::system_error(::GetLastError(), std::system_category(), "Unable to watch for directory changes"); 302 | } 303 | 304 | ConfigLoader::ConfigLoader() : 305 | configDirectory(GetUserDirectory()), 306 | initialConfig(LoadConfig(configDirectory / configFileName)) {} 307 | 308 | void ConfigLoader::Watcher::OnConfigFileEvent() { 309 | Log() << "Handling config file event"; 310 | 311 | Config newConfig; 312 | try { 313 | newConfig = LoadConfig(configLoader.configDirectory / configFileName); 314 | } 315 | catch (const std::exception& exception) { 316 | Log() << "Unable to load config, ignoring event: " << ::dechamps_cpputil::GetNestedExceptionMessage(exception); 317 | return; 318 | } 319 | if (newConfig == configLoader.Initial()) { 320 | Log() << "New config is identical to initial config, not taking any action"; 321 | return; 322 | } 323 | 324 | onConfigChange(); 325 | } 326 | 327 | } -------------------------------------------------------------------------------- /windows/deploy_windows.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string] $APP_BUILD_VERSION = "1.0.0", 3 | # Replace default path with system Qt installation folder if necessary 4 | [string] $QtPath = "C:\Qt", 5 | [string] $QtInstallPath = "none", 6 | # [string] $QtCompile32 = "msvc2019", 7 | [string] $QtCompile64 = "msvc2019_64", 8 | # [string] $AsioSDKName = "ASIOSDK2.3.3", 9 | [string] $AsioSDKName = "asiosdk_2.3.3_2019-06-14", 10 | [string] $AsioSDKUrl = "https://download.steinberg.net/sdk_downloads/asiosdk_2.3.3_2019-06-14.zip", 11 | # [string] $InnoSetupUrl = "https://jrsoftware.org/download.php/is.exe", 12 | # [string] $InnoSetupIsccPath = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe", 13 | [string] $VsDistFile64Path = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Redist\MSVC\14.32.31326\x64\Microsoft.VC143.CRT" 14 | ) 15 | 16 | # change directory to the directory above (if needed) 17 | Set-Location -Path "$PSScriptRoot\..\" 18 | 19 | # Global constants 20 | $RootPath = "$PWD" 21 | $BuildPath = "$RootPath\build" 22 | $DeployPath = "$RootPath\deploy" 23 | $WindowsPath ="$RootPath\windows" 24 | $AppName = "KoordASIO" 25 | 26 | # Stop at all errors 27 | $ErrorActionPreference = "Stop" 28 | 29 | 30 | # Execute native command with errorlevel handling 31 | Function Invoke-Native-Command { 32 | Param( 33 | [string] $Command, 34 | [string[]] $Arguments 35 | ) 36 | 37 | & "$Command" @Arguments 38 | 39 | if ($LastExitCode -Ne 0) 40 | { 41 | Throw "Native command $Command returned with exit code $LastExitCode" 42 | } 43 | } 44 | 45 | # Cleanup existing build folders 46 | Function Clean-Build-Environment 47 | { 48 | if (Test-Path -Path $BuildPath) { Remove-Item -Path $BuildPath -Recurse -Force } 49 | if (Test-Path -Path $DeployPath) { Remove-Item -Path $DeployPath -Recurse -Force } 50 | 51 | New-Item -Path $BuildPath -ItemType Directory 52 | New-Item -Path $DeployPath -ItemType Directory 53 | } 54 | 55 | # For sourceforge links we need to get the correct mirror (especially NISIS) Thanks: https://www.powershellmagazine.com/2013/01/29/pstip-retrieve-a-redirected-url/ 56 | Function Get-RedirectedUrl { 57 | 58 | Param ( 59 | [Parameter(Mandatory=$true)] 60 | [String]$URL 61 | ) 62 | 63 | $request = [System.Net.WebRequest]::Create($url) 64 | $request.AllowAutoRedirect=$false 65 | $response=$request.GetResponse() 66 | 67 | if ($response.StatusCode -eq "Found") 68 | { 69 | $response.GetResponseHeader("Location") 70 | } 71 | } 72 | 73 | function Initialize-Module-Here ($m) { # see https://stackoverflow.com/a/51692402 74 | 75 | # If module is imported say that and do nothing 76 | if (Get-Module | Where-Object {$_.Name -eq $m}) { 77 | Write-Output "Module $m is already imported." 78 | } 79 | else { 80 | 81 | # If module is not imported, but available on disk then import 82 | if (Get-Module -ListAvailable | Where-Object {$_.Name -eq $m}) { 83 | Import-Module $m 84 | } 85 | else { 86 | 87 | # If module is not imported, not available on disk, but is in online gallery then install and import 88 | if (Find-Module -Name $m | Where-Object {$_.Name -eq $m}) { 89 | Install-Module -Name $m -Force -Verbose -Scope CurrentUser 90 | Import-Module $m 91 | } 92 | else { 93 | 94 | # If module is not imported, not available and not in online gallery then abort 95 | Write-Output "Module $m not imported, not available and not in online gallery, exiting." 96 | EXIT 1 97 | } 98 | } 99 | } 100 | } 101 | 102 | # Download and uncompress dependency in ZIP format 103 | Function Install-Dependency 104 | { 105 | param( 106 | [Parameter(Mandatory=$true)] 107 | [string] $Uri, 108 | [Parameter(Mandatory=$true)] 109 | [string] $Name, 110 | [Parameter(Mandatory=$true)] 111 | [string] $Destination 112 | ) 113 | 114 | if (Test-Path -Path "$WindowsPath\$Destination") { return } 115 | 116 | $TempFileName = [System.IO.Path]::GetTempFileName() + ".zip" 117 | $TempDir = [System.IO.Path]::GetTempPath() 118 | 119 | if ($Uri -Match "downloads.sourceforge.net") 120 | { 121 | $Uri = Get-RedirectedUrl -URL $Uri 122 | } 123 | 124 | Invoke-WebRequest -Uri $Uri -OutFile $TempFileName 125 | echo $TempFileName 126 | Expand-Archive -Path $TempFileName -DestinationPath $TempDir -Force 127 | echo $WindowsPath\$Destination 128 | Move-Item -Path "$TempDir\$Name" -Destination "$WindowsPath\$Destination" -Force 129 | Remove-Item -Path $TempFileName -Force 130 | } 131 | 132 | # Install VSSetup (Visual Studio detection), ASIO SDK and Innosetup 133 | Function Install-Dependencies 134 | { 135 | if (-not (Get-PackageProvider -Name nuget).Name -eq "nuget") { 136 | Install-PackageProvider -Name "Nuget" -Scope CurrentUser -Force 137 | } 138 | Initialize-Module-Here -m "VSSetup" 139 | Install-Dependency -Uri $AsioSDKUrl ` 140 | -Name $AsioSDKName -Destination "ASIOSDK2" 141 | 142 | # # assuming Powershell3, install Chocolatey 143 | # Set-ExecutionPolicy Bypass -Scope Process -Force; iwr https://community.chocolatey.org/install.ps1 -UseBasicParsing | iex 144 | # # now install Innosetup 145 | # choco install innosetup 146 | } 147 | 148 | # Setup environment variables and build tool paths 149 | Function Initialize-Build-Environment 150 | { 151 | param( 152 | [Parameter(Mandatory=$true)] 153 | [string] $QtInstallPath, 154 | [Parameter(Mandatory=$true)] 155 | [string] $BuildArch 156 | ) 157 | 158 | # Look for Visual Studio/Build Tools 2017 or later (version 15.0 or above) 159 | $VsInstallPath = Get-VSSetupInstance | ` 160 | Select-VSSetupInstance -Product "*" -Version "15.0" -Latest | ` 161 | Select-Object -ExpandProperty "InstallationPath" 162 | 163 | if ($VsInstallPath -Eq "") { $VsInstallPath = "" } 164 | 165 | if ($BuildArch -Eq "x86_64") 166 | { 167 | $VcVarsBin = "$VsInstallPath\VC\Auxiliary\build\vcvars64.bat" 168 | $QtMsvcSpecPath = "$QtInstallPath\$QtCompile64\bin" 169 | } 170 | # else 171 | # { 172 | # $VcVarsBin = "$VsInstallPath\VC\Auxiliary\build\vcvars32.bat" 173 | # $QtMsvcSpecPath = "$QtInstallPath\$QtCompile32\bin" 174 | # } 175 | 176 | # Setup Qt executables paths for later calls 177 | Set-Item Env:QtQmakePath "$QtMsvcSpecPath\qmake.exe" 178 | Set-Item Env:QtCmakePath "$QtPath\Tools\CMake_64\bin\cmake.exe" 179 | Set-Item Env:QtCmakePath "C:\Qt\Tools\CMake_64\bin\cmake.exe" 180 | Set-Item Env:QtWinDeployPath "$QtMsvcSpecPath\windeployqt.exe" 181 | 182 | "" 183 | "**********************************************************************" 184 | "Using Visual Studio/Build Tools environment settings located at" 185 | $VcVarsBin 186 | "**********************************************************************" 187 | "" 188 | "**********************************************************************" 189 | "Using Qt binaries for Visual C++ located at" 190 | $QtMsvcSpecPath 191 | "**********************************************************************" 192 | "" 193 | 194 | if (-Not (Test-Path -Path $VcVarsBin)) 195 | { 196 | Throw "Microsoft Visual Studio ($BuildArch variant) is not installed. " + ` 197 | "Please install Visual Studio 2017 or above it before running this script." 198 | } 199 | 200 | if (-Not (Test-Path -Path $Env:QtQmakePath)) 201 | { 202 | Throw "The Qt binaries for Microsoft Visual C++ 2017 or above could not be located at $QtMsvcSpecPath. " + ` 203 | "Please install Qt with support for MSVC 2017 or above before running this script," + ` 204 | "then call this script with the Qt install location, for example C:\Qt\6.3.0" 205 | } 206 | 207 | if (-Not (Test-Path -Path $Env:QtCmakePath)) 208 | { 209 | Throw "The Qt binaries for CMake for Microsoft Visual C++ 2017 or above could not be located at $QtPath. " + ` 210 | "Please install Qt with support for MSVC 2017 or above before running this script," + ` 211 | "then call this script with the Qt install location, for example C:\Qt\6.3.0" 212 | } 213 | 214 | # Import environment variables set by vcvarsXX.bat into current scope 215 | $EnvDump = [System.IO.Path]::GetTempFileName() 216 | Invoke-Native-Command -Command "cmd" ` 217 | -Arguments ("/c", "`"$VcVarsBin`" && set > `"$EnvDump`"") 218 | 219 | foreach ($_ in Get-Content -Path $EnvDump) 220 | { 221 | if ($_ -Match "^([^=]+)=(.*)$") 222 | { 223 | Set-Item "Env:$($Matches[1])" $Matches[2] 224 | } 225 | } 226 | 227 | Remove-Item -Path $EnvDump -Force 228 | } 229 | 230 | # Build KoordASIO x86_64 and x86 231 | Function Build-App 232 | { 233 | param( 234 | [Parameter(Mandatory=$true)] 235 | [string] $BuildConfig, 236 | [Parameter(Mandatory=$true)] 237 | [string] $BuildArch 238 | ) 239 | 240 | # Build kdasioconfig Qt project with CMake / nmake 241 | # # Build FlexASIO dlls with CMake / nmake 242 | Invoke-Native-Command -Command "$Env:QtCmakePath" ` 243 | -Arguments ("-DCMAKE_PREFIX_PATH='$QtInstallPath\$QtCompile64\lib\cmake'", ` 244 | "-DCMAKE_BUILD_TYPE=Release", ` 245 | "-S", "$RootPath\src\kdasioconfig", ` 246 | "-B", "$BuildPath\$BuildConfig\kdasioconfig", ` 247 | "-G", "NMake Makefiles") 248 | Set-Location -Path "$BuildPath\$BuildConfig\kdasioconfig" 249 | # Invoke-Native-Command -Command "nmake" -Arguments ("$BuildConfig") 250 | Invoke-Native-Command -Command "nmake" 251 | 252 | Set-Location -Path "$RootPath" 253 | 254 | # Ninja! 255 | Invoke-Native-Command -Command "$Env:QtCmakePath" ` 256 | -Arguments ("-S", "$RootPath\src", ` 257 | "-B", "$BuildPath\$BuildConfig\flexasio", ` 258 | "-G", "Ninja", ` 259 | "-DCMAKE_BUILD_TYPE=Release") 260 | 261 | # Build! 262 | Invoke-Native-Command -Command "$Env:QtCmakePath" ` 263 | -Arguments ("--build", "$BuildPath\$BuildConfig\flexasio") 264 | 265 | # Collect! necessary Qt dlls for kdasioconfig 266 | Set-Location -Path "$BuildPath\$BuildConfig\flexasio" 267 | Invoke-Native-Command -Command "$Env:QtWinDeployPath" ` 268 | -Arguments ("--$BuildConfig", "--no-compiler-runtime", "--dir=$DeployPath\$BuildArch", ` 269 | "--no-system-d3d-compiler", "--no-opengl-sw", ` 270 | "$BuildPath\$BuildConfig\kdasioconfig\KoordASIOControl.exe") 271 | 272 | # Get-ChildItem -Recurse "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Redist\MSVC\" 273 | 274 | # Transfer VS dist DLLs for x64 275 | Copy-Item -Path "$VsDistFile64Path\*" -Destination "$DeployPath\$BuildArch" 276 | 277 | # all build files: 278 | # kdasioconfig files inc qt dlls now in 279 | # D:/a/KoordASIO/KoordASIO/deploy/x86_64/ 280 | # - KoordASIOControl.exe 281 | # all qt dlls etc ... 282 | # flexasio files in: 283 | # D:\a\KoordASIO\KoordASIO\build\flexasio\install\bin 284 | # - FlexASIO.dll - renamed to KoordASIO.dll 285 | # - portaudio.dll 286 | # .... 287 | 288 | # Move KoordASIOControl.exe to deploy dir 289 | Move-Item -Path "$BuildPath\$BuildConfig\kdasioconfig\KoordASIOControl.exe" -Destination "$DeployPath\$BuildArch" -Force 290 | # Move 2 x FlexASIO dlls to deploy dir, rename DLL here for separation 291 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\KoordASIO.dll" -Destination "$DeployPath\$BuildArch" -Force 292 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\portaudio.dll" -Destination "$DeployPath\$BuildArch" -Force 293 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\ASIOTest.dll" -Destination "$DeployPath\$BuildArch" -Force 294 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\sndfile.dll" -Destination "$DeployPath\$BuildArch" -Force 295 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\FlexASIOTest.exe" -Destination "$DeployPath\$BuildArch" -Force 296 | Move-Item -Path "$BuildPath\$BuildConfig\flexasio\install\bin\PortAudioDevices.exe" -Destination "$DeployPath\$BuildArch" -Force 297 | # move InnoSetup script to deploy dir 298 | Move-Item -Path "$WindowsPath\kdinstaller.iss" -Destination "$RootPath" -Force 299 | 300 | # Get-ChildItem -Recurse $RootPath 301 | 302 | # Invoke-Native-Command -Command "nmake" -Arguments ("clean") 303 | Set-Location -Path $RootPath 304 | 305 | } 306 | 307 | # Build and deploy KoordASIO 64bit and 32bit variants 308 | function Build-App-Variants 309 | { 310 | param( 311 | [Parameter(Mandatory=$true)] 312 | [string] $QtInstallPath 313 | ) 314 | 315 | # foreach ($_ in ("x86_64", "x86")) 316 | foreach ($_ in ("x86_64")) # only build x64 now 317 | { 318 | $OriginalEnv = Get-ChildItem Env: 319 | Initialize-Build-Environment -QtInstallPath $QtInstallPath -BuildArch $_ 320 | Build-App -BuildConfig "release" -BuildArch $_ 321 | $OriginalEnv | % { Set-Item "Env:$($_.Name)" $_.Value } 322 | } 323 | } 324 | 325 | # Build Windows installer 326 | Function Build-Installer 327 | { 328 | #FIXME for 64bit build only 329 | Set-Location -Path "$RootPath" 330 | # /Program Files (x86)/Inno Setup 6/ISCC.exe 331 | Invoke-Native-Command -Command "ISCC.exe" ` 332 | -Arguments ("$RootPath\kdinstaller.iss", ` 333 | "/FKoordASIO-$APP_BUILD_VERSION", ` 334 | "/DApplicationVersion=${APP_BUILD_VERSION}") 335 | 336 | } 337 | 338 | Function SignExe 339 | { 340 | # echo path for debug 341 | $env:PATH 342 | 343 | $WindowsOVCertPwd = Get-Content "C:\KoordOVCertPwd" 344 | 345 | #FIXME - use hardcoded path right now - for some reason Windows Kits are not in path 346 | # "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\\x64\signtool.exe" 347 | # Invoke-Native-Command -Command "SignTool" ` 348 | Invoke-Native-Command -Command "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" ` 349 | -Arguments ( "sign", "/f", "C:\KoordOVCert.pfx", ` 350 | "/p", $WindowsOVCertPwd, ` 351 | "/fd", "SHA256", "/td", "SHA256", ` 352 | "/tr", "http://timestamp.sectigo.com", ` 353 | "Output\KoordASIO-${APP_BUILD_VERSION}.exe" ) 354 | 355 | } 356 | 357 | 358 | Clean-Build-Environment 359 | Install-Dependencies 360 | Build-App-Variants -QtInstallPath $QtInstallPath 361 | Build-Installer 362 | SignExe 363 | -------------------------------------------------------------------------------- /src/flexasio/FlexASIOUtil/portaudio.cpp: -------------------------------------------------------------------------------- 1 | #include "portaudio.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | // From pa_debugprint.h. The PortAudio DLL exports this function, but sadly it is not exposed in a public header file. 16 | extern "C" { 17 | typedef void(*PaUtilLogCallback) (const char *log); 18 | extern void PaUtil_SetDebugPrintFunction(PaUtilLogCallback cb); 19 | } 20 | 21 | // From src/common/pa_hostapi.h, which is not exposed publicly but is nonetheless useful here. 22 | // 23 | /** The common header for all data structures whose pointers are passed through 24 | the hostApiSpecificStreamInfo field of the PaStreamParameters structure. 25 | Note that in order to keep the public PortAudio interface clean, this structure 26 | is not used explicitly when declaring hostApiSpecificStreamInfo data structures. 27 | However, some code in pa_front depends on the first 3 members being equivalent 28 | with this structure. 29 | @see PaStreamParameters 30 | */ 31 | typedef struct PaUtilHostApiSpecificStreamInfoHeader 32 | { 33 | unsigned long size; /**< size of whole structure including this header */ 34 | PaHostApiTypeId hostApiType; /**< host API for which this data is intended */ 35 | unsigned long version; /**< structure version */ 36 | } PaUtilHostApiSpecificStreamInfoHeader; 37 | 38 | namespace flexasio { 39 | 40 | PortAudioDebugRedirector::PortAudioDebugRedirector(Write write) { 41 | write(std::string("PortAudio version: ") + Pa_GetVersionText()); 42 | write("Enabling PortAudio debug output redirection"); 43 | if (this->write) abort(); 44 | this->write = std::move(write); 45 | PaUtil_SetDebugPrintFunction(DebugPrint); 46 | } 47 | 48 | PortAudioDebugRedirector::~PortAudioDebugRedirector() { 49 | this->write("Disabling PortAudio debug output redirection"); 50 | PaUtil_SetDebugPrintFunction(NULL); 51 | if (!this->write) abort(); 52 | this->write = nullptr; 53 | } 54 | 55 | void PortAudioDebugRedirector::DebugPrint(const char* str) { 56 | if (!PortAudioDebugRedirector::write) abort(); 57 | 58 | std::string_view line(str); 59 | while (!line.empty() && isspace(static_cast(line.back()))) line.remove_suffix(1); 60 | PortAudioDebugRedirector::write(line); 61 | } 62 | 63 | PortAudioDebugRedirector::Write PortAudioDebugRedirector::write; 64 | 65 | std::string GetHostApiTypeIdString(PaHostApiTypeId hostApiTypeId) { 66 | return ::dechamps_cpputil::EnumToString(hostApiTypeId, { 67 | { paInDevelopment, "In development" }, 68 | { paDirectSound, "DirectSound" }, 69 | { paMME, "MME" }, 70 | { paASIO, "ASIO" }, 71 | { paSoundManager, "SoundManager" }, 72 | { paCoreAudio, "CoreAudio" }, 73 | { paOSS, "OSS" }, 74 | { paALSA, "ALSA" }, 75 | { paAL, "AL" }, 76 | { paBeOS, "BeOS" }, 77 | { paWDMKS, "WDMKS" }, 78 | { paJACK, "JACK" }, 79 | { paWASAPI, "WASAPI" }, 80 | { paAudioScienceHPI, "AudioScienceHPI" }, 81 | }); 82 | } 83 | 84 | std::string GetSampleFormatString(PaSampleFormat sampleFormat) { 85 | auto result = ::dechamps_cpputil::BitfieldToString(sampleFormat, { 86 | { paFloat32, "Float32" }, 87 | { paInt32, "Int32" }, 88 | { paInt24, "Int24" }, 89 | { paInt16, "Int16" }, 90 | { paInt8, "Int8" }, 91 | { paUInt8, "UInt8" }, 92 | { paCustomFormat, "CustomFormat" }, 93 | { paNonInterleaved, "NonInterleaved" }, 94 | }); 95 | return result; 96 | } 97 | 98 | std::string GetStreamFlagsString(PaStreamFlags streamFlags) { 99 | return ::dechamps_cpputil::BitfieldToString(streamFlags, { 100 | { paClipOff, "ClipOff" }, 101 | { paDitherOff, "DitherOff" }, 102 | { paNeverDropInput, "NeverDropInput" }, 103 | { paPrimeOutputBuffersUsingStreamCallback, "PrimeOutputBuffersUsingStreamCallback" }, 104 | }); 105 | } 106 | 107 | std::string GetWasapiFlagsString(PaWasapiFlags wasapiFlags) { 108 | return ::dechamps_cpputil::BitfieldToString(wasapiFlags, { 109 | { paWinWasapiExclusive, "Exclusive" }, 110 | { paWinWasapiRedirectHostProcessor, "RedirectHostProcessor" }, 111 | { paWinWasapiUseChannelMask, "UseChannelMask" }, 112 | { paWinWasapiPolling, "Polling" }, 113 | { paWinWasapiExplicitSampleFormat, "ExplicitSampleFormat" }, 114 | { paWinWasapiAutoConvert, "AutoConvert" }, 115 | }); 116 | } 117 | 118 | std::string GetWasapiThreadPriorityString(PaWasapiThreadPriority threadPriority) { 119 | return ::dechamps_cpputil::EnumToString(threadPriority, { 120 | { eThreadPriorityNone, "None" }, 121 | { eThreadPriorityAudio, "Audio" }, 122 | { eThreadPriorityCapture, "Capture" }, 123 | { eThreadPriorityDistribution, "Distribution" }, 124 | { eThreadPriorityGames, "Games" }, 125 | { eThreadPriorityPlayback, "Playback" }, 126 | { eThreadPriorityProAudio, "ProAudio" }, 127 | { eThreadPriorityWindowManager, "WindowManager" }, 128 | }); 129 | } 130 | 131 | std::string GetWasapiStreamCategoryString(PaWasapiStreamCategory streamCategory) { 132 | return ::dechamps_cpputil::EnumToString(streamCategory, { 133 | { eAudioCategoryOther, "Other" }, 134 | { eAudioCategoryCommunications, "Communications" }, 135 | { eAudioCategoryAlerts, "Alerts" }, 136 | { eAudioCategorySoundEffects, "SoundEffects" }, 137 | { eAudioCategoryGameEffects, "GameEffects" }, 138 | { eAudioCategoryGameMedia, "GameMedia" }, 139 | { eAudioCategoryGameChat, "GameChat" }, 140 | { eAudioCategorySpeech, "Speech" }, 141 | { eAudioCategoryMovie, "Movie" }, 142 | { eAudioCategoryMedia, "Media" }, 143 | }); 144 | } 145 | 146 | std::string GetWasapiStreamOptionString(PaWasapiStreamOption streamOption) { 147 | return ::dechamps_cpputil::EnumToString(streamOption, { 148 | { eStreamOptionNone, "None" }, 149 | { eStreamOptionRaw, "Raw" }, 150 | { eStreamOptionMatchFormat, "MatchFormat" }, 151 | }); 152 | } 153 | 154 | std::string GetStreamCallbackFlagsString(PaStreamCallbackFlags streamCallbackFlags) { 155 | return ::dechamps_cpputil::BitfieldToString(streamCallbackFlags, { 156 | { paInputUnderflow, "InputUnderflow" }, 157 | { paInputOverflow, "InputOverflow" }, 158 | { paOutputUnderflow, "OutputUnderflow" }, 159 | { paOutputOverflow, "OutputOverflow" }, 160 | { paPrimingOutput, "PrimingOutput" }, 161 | }); 162 | } 163 | 164 | std::ostream& operator<<(std::ostream& os, const HostApi& hostApi) { 165 | os << "PortAudio host API index " << hostApi.index 166 | << " (name: '" << hostApi.info.name 167 | << "', type: " << GetHostApiTypeIdString(hostApi.info.type) 168 | << ", default input device: " << hostApi.info.defaultInputDevice 169 | << ", default output device: " << hostApi.info.defaultOutputDevice << ")"; 170 | return os; 171 | } 172 | 173 | const PaHostApiInfo& HostApi::GetInfo(PaHostApiIndex index) { 174 | const auto info = Pa_GetHostApiInfo(index); 175 | if (info == nullptr) throw std::runtime_error(std::string("Unable to get host API info for host API index ") + std::to_string(index)); 176 | return *info; 177 | } 178 | 179 | std::ostream& operator<<(std::ostream& os, const Device& device) { 180 | os << "PortAudio device index " << device.index 181 | << " (name: '" << device.info.name 182 | << "', host API: " << device.info.hostApi 183 | << ", default sample rate: " << device.info.defaultSampleRate 184 | << ", max input channels: " << device.info.maxInputChannels 185 | << ", max output channels: " << device.info.maxOutputChannels 186 | << ", input latency: " << device.info.defaultLowInputLatency << " (low) " << device.info.defaultHighInputLatency << " (high)" 187 | << ", output latency: " << device.info.defaultLowOutputLatency << " (low) " << device.info.defaultHighOutputLatency << " (high)" << ")"; 188 | return os; 189 | } 190 | 191 | const PaDeviceInfo& Device::GetInfo(PaDeviceIndex index) { 192 | const auto info = Pa_GetDeviceInfo(index); 193 | if (info == nullptr) throw std::runtime_error(std::string("Unable to get device info for device index ") + std::to_string(index)); 194 | return *info; 195 | } 196 | 197 | WAVEFORMATEXTENSIBLE GetWasapiDeviceDefaultFormat(PaDeviceIndex index) { 198 | WAVEFORMATEXTENSIBLE format = { 0 }; 199 | const auto result = PaWasapi_GetDeviceDefaultFormat(&format, sizeof(format), index); 200 | if (result <= 0) throw std::runtime_error(std::string("Unable to get WASAPI device default format for device ") + std::to_string(index) + ": " + Pa_GetErrorText(result)); 201 | return format; 202 | } 203 | WAVEFORMATEXTENSIBLE GetWasapiDeviceMixFormat(PaDeviceIndex index) { 204 | WAVEFORMATEXTENSIBLE format = { 0 }; 205 | const auto result = PaWasapi_GetDeviceMixFormat(&format, sizeof(format), index); 206 | if (result <= 0) throw std::runtime_error(std::string("Unable to get WASAPI device mix format for device ") + std::to_string(index) + ": " + Pa_GetErrorText(result)); 207 | return format; 208 | } 209 | 210 | std::string GetWaveFormatTagString(WORD formatTag) { 211 | return ::dechamps_cpputil::EnumToString(int(formatTag), { 212 | { WAVE_FORMAT_EXTENSIBLE, "EXTENSIBLE" }, 213 | { WAVE_FORMAT_MPEG, "MPEG" }, 214 | { WAVE_FORMAT_MPEGLAYER3, "MPEGLAYER3" }, 215 | }); 216 | } 217 | 218 | std::string GetWaveFormatChannelMaskString(DWORD channelMask) { 219 | return ::dechamps_cpputil::BitfieldToString(channelMask, { 220 | {SPEAKER_FRONT_LEFT, "Front Left"}, 221 | {SPEAKER_FRONT_RIGHT, "Front Right"}, 222 | {SPEAKER_FRONT_CENTER, "Front Center"}, 223 | {SPEAKER_LOW_FREQUENCY, "Low Frequency"}, 224 | {SPEAKER_BACK_LEFT, "Back Left"}, 225 | {SPEAKER_BACK_RIGHT, "Back Right"}, 226 | {SPEAKER_FRONT_LEFT_OF_CENTER, "Front Left of Center"}, 227 | {SPEAKER_FRONT_RIGHT_OF_CENTER, "Front Right of Center"}, 228 | {SPEAKER_BACK_CENTER, "Back Center"}, 229 | {SPEAKER_SIDE_LEFT, "Side Left"}, 230 | {SPEAKER_SIDE_RIGHT, "Side Right"}, 231 | {SPEAKER_TOP_CENTER, "Top Center"}, 232 | {SPEAKER_TOP_FRONT_LEFT, "Top Front Left"}, 233 | {SPEAKER_TOP_FRONT_CENTER, "Top Front Center"}, 234 | {SPEAKER_TOP_FRONT_RIGHT, "Top Front Right"}, 235 | {SPEAKER_TOP_BACK_LEFT, "Top Back Left"}, 236 | {SPEAKER_TOP_BACK_CENTER, "Top Back Center"}, 237 | {SPEAKER_TOP_BACK_RIGHT, "Top Back Right"}, 238 | }); 239 | } 240 | 241 | std::string GetWaveSubFormatString(const GUID& subFormat) { 242 | return ::dechamps_cpputil::EnumToString(subFormat, { 243 | { KSDATAFORMAT_SUBTYPE_ADPCM, "ADPCM" }, 244 | { KSDATAFORMAT_SUBTYPE_ALAW, "A-law" }, 245 | { KSDATAFORMAT_SUBTYPE_DRM, "DRM" }, 246 | { KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS, "IEC61937 Dolby Digital Plus" }, 247 | { KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL, "IEC61937 Dolby Digital" }, 248 | { KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, "IEEE Float" }, 249 | { KSDATAFORMAT_SUBTYPE_MPEG, "MPEG-1" }, 250 | { KSDATAFORMAT_SUBTYPE_MULAW, "Mu-law" }, 251 | { KSDATAFORMAT_SUBTYPE_PCM, "PCM" }, 252 | }, [](const GUID& guid) { 253 | char str[128]; 254 | // Shamelessly stolen from https://stackoverflow.com/a/18555932/172594 255 | snprintf(str, sizeof(str), "{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); 256 | return std::string(str); 257 | }); 258 | } 259 | 260 | std::string DescribeWaveFormat(const WAVEFORMATEXTENSIBLE& waveFormatExtensible) { 261 | const auto& waveFormat = waveFormatExtensible.Format; 262 | 263 | std::stringstream result; 264 | result << "WAVEFORMAT with format tag " << GetWaveFormatTagString(waveFormat.wFormatTag) << ", " 265 | << waveFormat.nChannels << " channels, " 266 | << waveFormat.nSamplesPerSec << " samples/second, " 267 | << waveFormat.nAvgBytesPerSec << " average bytes/second, " 268 | << "block alignment " << waveFormat.nBlockAlign << " bytes, " 269 | << waveFormat.wBitsPerSample << " bits per sample"; 270 | 271 | if (waveFormat.wFormatTag == WAVE_FORMAT_EXTENSIBLE) { 272 | result << ", " << waveFormatExtensible.Samples.wValidBitsPerSample << " valid bits per sample, " 273 | << "channel mask " << GetWaveFormatChannelMaskString(waveFormatExtensible.dwChannelMask) << ", " 274 | << "subformat " << GetWaveSubFormatString(waveFormatExtensible.SubFormat); 275 | } 276 | 277 | return result.str(); 278 | } 279 | 280 | std::string DescribeStreamParameters(const PaStreamParameters& parameters) { 281 | std::stringstream result; 282 | 283 | result << "PortAudio stream parameters for device index " << parameters.device << ", " 284 | << parameters.channelCount << " channels, sample format " 285 | << GetSampleFormatString(parameters.sampleFormat) << ", suggested latency " 286 | << parameters.suggestedLatency << "s"; 287 | 288 | if (parameters.hostApiSpecificStreamInfo != nullptr) { 289 | const auto hostApiSpecificHeader = static_cast(parameters.hostApiSpecificStreamInfo); 290 | result << ", host API specific: " << hostApiSpecificHeader->size << " bytes structure, type " 291 | << GetHostApiTypeIdString(hostApiSpecificHeader->hostApiType) << ", version " 292 | << hostApiSpecificHeader->version; 293 | if (hostApiSpecificHeader->hostApiType == paWASAPI) { 294 | const auto wasapiSpecific = static_cast(parameters.hostApiSpecificStreamInfo); 295 | result << ", WASAPI specific: flags " << GetWasapiFlagsString(PaWasapiFlags(wasapiSpecific->flags)) << ", channel mask " 296 | << GetWaveFormatChannelMaskString(wasapiSpecific->channelMask) << ", host processor output " 297 | << wasapiSpecific->hostProcessorOutput << ", host processor input " 298 | << wasapiSpecific->hostProcessorInput << ", thread priority " 299 | << GetWasapiThreadPriorityString(wasapiSpecific->threadPriority) << ", stream category " 300 | << GetWasapiStreamCategoryString(wasapiSpecific->streamCategory) << ", stream option " 301 | << GetWasapiStreamOptionString(wasapiSpecific->streamOption); 302 | } 303 | } 304 | 305 | return result.str(); 306 | } 307 | 308 | std::string DescribeStreamInfo(const PaStreamInfo& info) { 309 | std::stringstream result; 310 | result << "PortAudio stream info version " << info.structVersion << ", input latency " 311 | << info.inputLatency << "s, output latency " 312 | << info.outputLatency << "s, sample rate " 313 | << info.sampleRate << " Hz"; 314 | return result.str(); 315 | } 316 | 317 | std::string DescribeStreamCallbackTimeInfo(const PaStreamCallbackTimeInfo& streamCallbackTimeInfo) { 318 | std::stringstream result; 319 | result << "PortAudio stream callback time info with input buffer ADC time " << streamCallbackTimeInfo.inputBufferAdcTime << ", current time " 320 | << streamCallbackTimeInfo.currentTime << ", output buffer DAC time " 321 | << streamCallbackTimeInfo.outputBufferDacTime; 322 | return result.str(); 323 | } 324 | 325 | } 326 | --------------------------------------------------------------------------------