ASIO backend, part 1

DOES NOT FUCKING PLAY SOUND, so don't complain if you can't hear a thing

OK?!?!!
This commit is contained in:
tildearrow 2025-10-25 00:16:15 -05:00
parent 63685ed0a5
commit 57f3b9cd4d
6 changed files with 392 additions and 1 deletions

View file

@ -73,9 +73,11 @@ set(WITH_RENDER_OPENGL_DEFAULT ON)
if (WIN32)
set(WITH_RENDER_DX11_DEFAULT ON)
set(WITH_RENDER_DX9_DEFAULT ON)
set(WITH_ASIO_DEFAULT ON)
else()
set(WITH_RENDER_DX11_DEFAULT OFF)
set(WITH_RENDER_DX9_DEFAULT OFF)
set(WITH_ASIO_DEFAULT OFF)
endif()
if (APPLE)
set(WITH_RENDER_METAL_DEFAULT ON)
@ -116,6 +118,7 @@ option(USE_MOMO "Build a libintl implementation instead of using the system one.
option(WITH_OGG "Whether to build with Ogg and Vorbis support." ${WITH_OGG_DEFAULT})
option(WITH_MPEG "Whether to build with MPEG audio support (including MP3)." ${WITH_MPEG_DEFAULT})
option(WITH_JACK "Whether to build with JACK support. Auto-detects if JACK is available" ${WITH_JACK_DEFAULT})
option(WITH_ASIO "Whether to build with ASIO support. Upgrades Furnace license to GPLv3." ${WITH_ASIO_DEFAULT})
option(WITH_PORTAUDIO "Whether to build with PortAudio for audio output." ${WITH_PORTAUDIO_DEFAULT})
option(WITH_RENDER_SDL "Whether to build with the SDL_Renderer render backend." ${WITH_RENDER_SDL_DEFAULT})
option(WITH_RENDER_OPENGL "Whether to build with the OpenGL render backend." ${WITH_RENDER_OPENGL_DEFAULT})
@ -508,6 +511,25 @@ else()
message(STATUS "Building without JACK support")
endif()
if (WITH_ASIO)
if (NOT WIN32)
message(FATAL_ERROR "ASIO is only available on Windows!")
endif()
add_subdirectory(extern/asio)
list(APPEND DEPENDENCIES_DEFINES HAVE_ASIO)
list(APPEND DEPENDENCIES_DEFINES FURNACE_GPL3)
list(APPEND AUDIO_SOURCES src/audio/asio.cpp)
list(APPEND DEPENDENCIES_INCLUDE_DIRS extern/asio/common extern/asio/host)
list(APPEND DEPENDENCIES_LIBRARIES ASIO)
message(STATUS "Building with ASIO support")
set(FURNACE_LICENSE "GPLv3")
else()
if (WIN32)
message(STATUS "Building without ASIO support")
endif()
set(FURNACE_LICENSE "GPLv2")
endif()
if (WITH_PORTAUDIO)
list(APPEND AUDIO_SOURCES src/audio/pa.cpp)
message(STATUS "Building with PortAudio")
@ -1365,3 +1387,5 @@ if (NOT ANDROID OR TERMUX)
endif()
target_compile_definitions(${FURNACE} PRIVATE ${DEPENDENCIES_DEFINES})
message(STATUS "License: ${FURNACE_LICENSE}")

View file

@ -148,7 +148,7 @@ for other operating systems, you may [build the source](#developer-info).
- built-in sample editor
- chip mixing settings
- built-in visualizer in pattern view
- open-source under GPLv2 or later.
- open-source under GPLv2 or later/GPLv3. see [LICENSE](LICENSE).
---

278
src/audio/asio.cpp Normal file
View file

@ -0,0 +1,278 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2025 tildearrow and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <string.h>
#include "asio.h"
#include "../ta-log.h"
static TAAudioASIO* callbackInstance=NULL;
static void _onBufferSwitch(long index, ASIOBool isDirect) {
if (callbackInstance==NULL) return;
callbackInstance->onProcess(index);
}
static void _onSampleRate(ASIOSampleRate rate) {
if (callbackInstance==NULL) return;
callbackInstance->onSampleRate(*(double*)(&rate));
}
static long _onMessage(long type, long value, void* msg, double* opt) {
return 0;
}
void TAAudioASIO::onSampleRate(double rate) {
sampleRateChanged(SampleRateChangeEvent(rate));
}
void TAAudioASIO::onBufferSize(int bufsize) {
bufferSizeChanged(BufferSizeChangeEvent(bufsize));
}
void TAAudioASIO::onProcess(int index) {
if (audioProcCallback!=NULL) {
if (midiIn!=NULL) midiIn->gather();
audioProcCallback(audioProcCallbackUser,inBufs,outBufs,desc.inChans,desc.outChans,desc.bufsize);
}
// upload here...
/*if (nframes!=desc.bufsize) {
desc.bufsize=nframes;
}*/
}
void* TAAudioASIO::getContext() {
return (void*)&driverInfo;
}
bool TAAudioASIO::quit() {
if (!initialized) return false;
if (running) {
ASIOStop();
running=false;
}
ASIODisposeBuffers();
for (int i=0; i<desc.inChans; i++) {
delete[] inBufs[i];
inBufs[i]=NULL;
}
for (int i=0; i<desc.outChans; i++) {
delete[] outBufs[i];
outBufs[i]=NULL;
}
delete[] inBufs;
delete[] outBufs;
inBufs=NULL;
outBufs=NULL;
ASIOExit();
drivers.removeCurrentDriver();
callbackInstance=NULL;
initialized=false;
return true;
}
bool TAAudioASIO::setRun(bool run) {
if (!initialized) return false;
if (running==run) {
return running;
}
if (run) {
if (ASIOStart()!=ASE_OK) return false;
running=true;
} else {
// does it matter whether stop was successful?
ASIOStop();
running=false;
}
return running;
}
bool TAAudioASIO::init(TAAudioDesc& request, TAAudioDesc& response) {
if (initialized) return false;
if (callbackInstance) {
logE("can't initialize more than one output!");
return false;
}
desc=request;
desc.outFormat=TA_AUDIO_FORMAT_F32;
if (desc.deviceName.empty()) {
// load first driver if not specified
logV("getting driver names...");
driverCount=drivers.getDriverNames(driverNames,ASIO_DRIVER_MAX);
// quit if we couldn't find any drivers
if (driverCount<1) {
logE("no ASIO drivers available");
return false;
}
desc.deviceName=driverNames[0];
}
// load driver
logV("loading ASIO driver...");
strncpy(deviceNameCopy,desc.deviceName.c_str(),63);
if (!drivers.loadDriver(deviceNameCopy)) {
logE("failed to load ASIO driver!");
return false;
}
// init
memset(&driverInfo,0,sizeof(driverInfo));
memset(bufInfo,0,sizeof(ASIOBufferInfo)*ASIO_CHANNEL_MAX*2);
memset(chanInfo,0,sizeof(ASIOChannelInfo)*ASIO_CHANNEL_MAX*2);
driverInfo.asioVersion=2;
driverInfo.sysRef=NULL;
ASIOError result=ASIOInit(&driverInfo);
if (result!=ASE_OK) {
logE("could not init device!");
drivers.removeCurrentDriver();
return false;
}
// setup callbacks
// unfortunately, only one TAAudio instance may exist at a time when using ASIO...
callbackInstance=this;
memset(&callbacks,0,sizeof(ASIOCallbacks));
callbacks.bufferSwitch=_onBufferSwitch;
callbacks.sampleRateDidChange=_onSampleRate;
callbacks.asioMessage=_onMessage;
// get driver information
long maxInChans=0;
long maxOutChans=0;
result=ASIOGetChannels(&maxInChans,&maxOutChans);
if (result!=ASE_OK) {
logE("could not get channel count!");
ASIOExit();
drivers.removeCurrentDriver();
return false;
}
if (maxInChans>ASIO_CHANNEL_MAX) maxInChans=ASIO_CHANNEL_MAX;
if (maxOutChans>ASIO_CHANNEL_MAX) maxOutChans=ASIO_CHANNEL_MAX;
if (desc.inChans>maxInChans) desc.inChans=maxInChans;
if (desc.outChans>maxOutChans) desc.outChans=maxOutChans;
long minBufSize=0;
long maxBufSize=0;
long actualBufSize=0;
long bufSizeGranularity=0;
result=ASIOGetBufferSize(&minBufSize,&maxBufSize,&actualBufSize,&bufSizeGranularity);
if (result!=ASE_OK) {
logE("could not get buffer size!");
ASIOExit();
drivers.removeCurrentDriver();
return false;
}
ASIOSampleRate outRate;
result=ASIOGetSampleRate(&outRate);
if (result!=ASE_OK) {
logE("could not get sample rate!");
ASIOExit();
drivers.removeCurrentDriver();
return false;
}
desc.rate=*(double*)(&outRate);
totalChans=0;
if (desc.inChans>0) {
inBufs=new float*[desc.inChans];
for (int i=0; i<desc.inChans; i++) {
chanInfo[totalChans].channel=i;
chanInfo[totalChans].isInput=ASIOTrue;
ASIOGetChannelInfo(&chanInfo[totalChans]);
bufInfo[totalChans].channelNum=i;
bufInfo[totalChans++].isInput=ASIOTrue;
inBufs[i]=new float[actualBufSize];
}
}
if (desc.outChans>0) {
outBufs=new float*[desc.outChans];
for (int i=0; i<desc.outChans; i++) {
chanInfo[totalChans].channel=i;
chanInfo[totalChans].isInput=ASIOFalse;
ASIOGetChannelInfo(&chanInfo[totalChans]);
bufInfo[totalChans].channelNum=i;
bufInfo[totalChans++].isInput=ASIOTrue;
outBufs[i]=new float[actualBufSize];
}
}
result=ASIOCreateBuffers(bufInfo,totalChans,actualBufSize,&callbacks);
if (result!=ASE_OK) {
logE("could not create buffers!");
if (inBufs!=NULL) {
for (int i=0; i<desc.inChans; i++) {
if (inBufs[i]!=NULL) {
delete[] inBufs[i];
inBufs[i]=NULL;
}
}
delete[] inBufs;
inBufs=NULL;
}
if (outBufs!=NULL) {
for (int i=0; i<desc.outChans; i++) {
if (outBufs[i]!=NULL) {
delete[] outBufs[i];
outBufs[i]=NULL;
}
}
delete[] outBufs;
outBufs=NULL;
}
ASIOExit();
drivers.removeCurrentDriver();
return false;
}
desc.bufsize=actualBufSize;
desc.fragments=2;
response=desc;
initialized=true;
return true;
}
std::vector<String> TAAudioASIO::listAudioDevices() {
std::vector<String> ret;
memset(driverNames,0,sizeof(void*)*ASIO_DRIVER_MAX);
driverCount=drivers.getDriverNames(driverNames,ASIO_DRIVER_MAX);
for (int i=0; i<driverCount; i++) {
ret.push_back(driverNames[i]);
}
return ret;
}

55
src/audio/asio.h Normal file
View file

@ -0,0 +1,55 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2025 tildearrow and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "taAudio.h"
#include <asiodrivers.h>
#include <asio.h>
#define ASIO_DRIVER_MAX 64
#define ASIO_CHANNEL_MAX 16
class TAAudioASIO: public TAAudio {
AsioDrivers drivers;
ASIODriverInfo driverInfo;
ASIOBufferInfo bufInfo[ASIO_CHANNEL_MAX*2];
ASIOChannelInfo chanInfo[ASIO_CHANNEL_MAX*2];
ASIOCallbacks callbacks;
int totalChans;
char* driverNames[ASIO_DRIVER_MAX];
int driverCount;
char deviceNameCopy[64];
public:
void onSampleRate(double rate);
void onBufferSize(int bufsize);
void onProcess(int nframes);
void* getContext();
bool quit();
bool setRun(bool run);
bool init(TAAudioDesc& request, TAAudioDesc& response);
std::vector<String> listAudioDevices();
TAAudioASIO():
totalChans(0),
driverCount(0) {}
};

View file

@ -196,8 +196,13 @@ const char* aboutLine[]={
"",
_N("copyright © 2021-2025 tildearrow"),
_N("(and contributors)."),
#ifdef FURNACE_GPL3
_N("licensed under GPLv3! see"),
_N("LICENSE for more information."),
#else
_N("licensed under GPLv2+! see"),
_N("LICENSE for more information."),
#endif
"",
_N("help Furnace grow:"),
"https://github.com/tildearrow/furnace",
@ -213,6 +218,10 @@ const char* aboutLine[]={
_N("the original program."),
"",
_N("it also comes with ABSOLUTELY NO WARRANTY."),
#ifdef HAVE_ASIO
"",
_N("ASIO is a registered trademark of Steinberg Media Technologies GmbH."),
#endif
"",
_N("thanks to all contributors/bug reporters!")
};

View file

@ -274,8 +274,13 @@ TAParamResult pLogLevel(String val) {
TAParamResult pVersion(String) {
printf("Furnace version " DIV_VERSION ".\n\n");
printf("copyright (C) 2021-2025 tildearrow and contributors.\n");
#ifdef FURNACE_GPL3
printf("licensed under the GNU General Public License version 3\n");
printf("<https://www.gnu.org/licenses/gpl-3.0.en.html>.\n\n");
#else
printf("licensed under the GNU General Public License version 2 or later\n");
printf("<https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.\n\n");
#endif
printf("this is free software with ABSOLUTELY NO WARRANTY.\n");
printf("pass the -warranty parameter for more information.\n\n");
printf("DISCLAIMER: this program is not affiliated with Delek in any form.\n");
@ -296,6 +301,9 @@ TAParamResult pVersion(String) {
printf("- zlib by Jean-loup Gailly and Mark Adler (zlib license)\n");
printf("- PortAudio (PortAudio license)\n");
printf("- Weak-JACK by x42 (GPLv2)\n");
#ifdef HAVE_ASIO
printf("- ASIO® by Steinberg (GPLv3)\n");
#endif
printf("- RtMidi by Gary P. Scavone (RtMidi license)\n");
printf("- backward-cpp by Google (MIT)\n");
printf("- Dear ImGui by Omar Cornut (MIT)\n");
@ -360,10 +368,26 @@ TAParamResult pVersion(String) {
printf("- SID2 emulator by LTVA (GPLv2, modification of reSID emulator)\n");
printf("- SID3 emulator by LTVA (MIT)\n");
printf("- openMSX YMF278 emulator (modified version) by the openMSX developers (GPLv2)\n");
#ifdef HAVE_ASIO
printf("\nASIO is a registered trademark of Steinberg Media Technologies GmbH.\n");
#endif
return TA_PARAM_QUIT;
}
TAParamResult pWarranty(String) {
#ifdef FURNACE_GPL3
printf("This program is free software: you can redistribute it and/or modify\n"
"it under the terms of the GNU General Public License as published by\n"
"the Free Software Foundation, version 3.\n\n"
"This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
"GNU General Public License for more details.\n\n"
"You should have received a copy of the GNU General Public License\n"
"along with this program. If not, see <https://www.gnu.org/licenses/>.\n");
#else
printf("This program is free software; you can redistribute it and/or\n"
"modify it under the terms of the GNU General Public License\n"
"as published by the Free Software Foundation; either version 2\n"
@ -377,6 +401,7 @@ TAParamResult pWarranty(String) {
"You should have received a copy of the GNU General Public License\n"
"along with this program; if not, write to the Free Software\n"
"Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n");
#endif
return TA_PARAM_QUIT;
}