Merge branch 'asio' - READ

thanks Steinberg for the open-source release of your ASIO SDK!
I am glad you're finally embracing the vast world of free and
open-source software, and allowing us to tap into technologies that
are industry standards.

--- NOTICE --- NOTICE --- NOTICE --- NOTICE --- NOTICE ---

ENABLING ASIO SUPPORT WITHIN FURNACE (enabled by default for Windows)
WILL RESULT IN A GPLv3-LICENSED BINARY.

this is permitted under the GPLv2-or-later's upgrade clause, which
can be found in Section 9 of the GNU General Public License, version 2.

if you do not wish to accept version 3 of the GPL, pass
`-DWITH_ASIO=OFF` to CMake at build time.

future Furnace releases (starting with 0.7) will have ASIO support
enabled - this decision comes after a poll in the Furnace Discord
where the majority (86 versus 22 hitherto) voted in favor.

https://discord.com/channels/936515318220722197/937611049182584852/1431092762278625321
This commit is contained in:
tildearrow 2025-10-26 03:56:36 -05:00
commit a887b72d36
19 changed files with 1889 additions and 349 deletions

View file

@ -37,6 +37,18 @@ void* TAAudio::getContext() {
return NULL;
}
TAAudioDeviceStatus TAAudio::getDeviceStatus() {
return deviceStatus;
}
void TAAudio::acceptDeviceStatus() {
deviceStatus=TA_AUDIO_DEVICE_OK;
}
int TAAudio::specialCommand(TAAudioCommand which) {
return -1;
}
bool TAAudio::quit() {
return true;
}
@ -121,4 +133,4 @@ TAMidiIn::~TAMidiIn() {
}
TAMidiOut::~TAMidiOut() {
}
}

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

@ -0,0 +1,617 @@
/**
* 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;
extern AsioDrivers* asioDrivers;
bool loadAsioDriver(char *name);
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) {
if (callbackInstance==NULL) return 0;
switch (type) {
case kAsioSelectorSupported:
switch (value) {
case kAsioSelectorSupported:
case kAsioEngineVersion:
case kAsioResetRequest:
case kAsioBufferSizeChange:
case kAsioResyncRequest:
return 1;
default:
return 0;
}
break;
case kAsioEngineVersion:
return 2;
break;
case kAsioResetRequest:
callbackInstance->requestDeviceChange();
return 1;
break;
case kAsioBufferSizeChange:
callbackInstance->onBufferSize(value);
return 1;
break;
case kAsioResyncRequest:
// ignore
return 1;
break;
}
return 0;
}
void TAAudioASIO::onSampleRate(double rate) {
sampleRateChanged(SampleRateChangeEvent(rate));
}
void TAAudioASIO::onBufferSize(int bufsize) {
bufferSizeChanged(BufferSizeChangeEvent(bufsize));
desc.bufsize=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...
for (int i=0; i<totalChans; i++) {
if (chanInfo[i].isInput==ASIOTrue) continue;
int ch=chanInfo[i].channel;
if (ch>=desc.outChans) continue;
float* srcBuf=outBufs[ch];
switch (chanInfo[i].type) {
// little-endian
case ASIOSTInt16LSB: {
short* buf=(short*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
buf[j]=CLAMP(srcBuf[j],-1.0,1.0)*32767.0f;
}
break;
}
case ASIOSTInt24LSB: {
unsigned char* buf=(unsigned char*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
int val=CLAMP(srcBuf[j],-1.0,1.0)*8388608.0f;
if (val<-8388608) val=-8388608;
if (val>8388607) val=-8388607;
*(buf++)=(val)&0xff;
*(buf++)=(val>>8)&0xff;
*(buf++)=(val>>16)&0xff;
}
break;
}
case ASIOSTInt32LSB:
case ASIOSTInt32LSB16:
case ASIOSTInt32LSB18:
case ASIOSTInt32LSB20:
case ASIOSTInt32LSB24: {
int* buf=(int*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
int val=CLAMP(srcBuf[j],-1.0,1.0)*8388608.0f;
if (val<-8388608) val=-8388608;
if (val>8388607) val=-8388607;
val<<=8;
buf[j]=val;
}
break;
}
case ASIOSTFloat32LSB: {
float* buf=(float*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
buf[j]=srcBuf[j];
}
break;
}
case ASIOSTFloat64LSB: {
double* buf=(double*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
buf[j]=srcBuf[j];
}
break;
}
// big-endian
case ASIOSTInt16MSB: {
unsigned short* buf=(unsigned short*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
unsigned short val=(unsigned short)((short)(CLAMP(srcBuf[j],-1.0,1.0)*32767.0f));
buf[j]=(val>>8)|(val<<8);
}
break;
}
case ASIOSTInt24MSB: {
unsigned char* buf=(unsigned char*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
int val=CLAMP(srcBuf[j],-1.0,1.0)*8388608.0f;
if (val<-8388608) val=-8388608;
if (val>8388607) val=-8388607;
*(buf++)=(val>>16)&0xff;
*(buf++)=(val>>8)&0xff;
*(buf++)=(val)&0xff;
}
break;
}
case ASIOSTInt32MSB:
case ASIOSTInt32MSB16:
case ASIOSTInt32MSB18:
case ASIOSTInt32MSB20:
case ASIOSTInt32MSB24: {
unsigned int* buf=(unsigned int*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
int val=CLAMP(srcBuf[j],-1.0,1.0)*8388608.0f;
if (val<-8388608) val=-8388608;
if (val>8388607) val=-8388607;
val<<=8;
unsigned char* uVal=(unsigned char*)&val;
buf[j]=(uVal[0]<<24)|(uVal[1]<<16)|(uVal[2]<<8)|(uVal[3]);
}
break;
}
case ASIOSTFloat32MSB: {
unsigned int* buf=(unsigned int*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
float val=srcBuf[j];
unsigned char* uVal=(unsigned char*)&val;
buf[j]=(uVal[0]<<24)|(uVal[1]<<16)|(uVal[2]<<8)|(uVal[3]);
}
break;
}
case ASIOSTFloat64MSB: {
unsigned char* buf=(unsigned char*)bufInfo[i].buffers[index];
for (unsigned int j=0; j<desc.bufsize; j++) {
double val=srcBuf[j];
unsigned char* uVal=(unsigned char*)&val;
*(buf++)=uVal[7];
*(buf++)=uVal[6];
*(buf++)=uVal[5];
*(buf++)=uVal[4];
*(buf++)=uVal[3];
*(buf++)=uVal[2];
*(buf++)=uVal[1];
*(buf++)=uVal[0];
}
break;
}
default: // unsupported
break;
}
}
}
String TAAudioASIO::getErrorStr(ASIOError which) {
switch (which) {
case ASE_OK:
return "OK";
break;
case ASE_SUCCESS:
return "Success";
break;
case ASE_NotPresent:
return "Not present";
break;
case ASE_HWMalfunction:
return "Hardware error";
break;
case ASE_InvalidParameter:
return "Invalid parameter";
break;
case ASE_InvalidMode:
return "Invalid mode";
break;
case ASE_SPNotAdvancing:
return "Sample position not advancing";
break;
case ASE_NoClock:
return "Clock not initialized";
break;
case ASE_NoMemory:
return "Out of memory";
break;
default:
break;
}
return "Unknown error";
}
String TAAudioASIO::getFormatName(ASIOSampleType which) {
switch (which) {
case ASIOSTInt16LSB:
return "16-bit LSB";
break;
case ASIOSTInt24LSB:
return "24-bit packed LSB";
break;
case ASIOSTInt32LSB:
return "32-bit LSB";
break;
case ASIOSTFloat32LSB:
return "32-bit float LSB";
break;
case ASIOSTFloat64LSB:
return "64-bit float LSB";
break;
case ASIOSTInt16MSB:
return "16-bit MSB";
break;
case ASIOSTInt24MSB:
return "24-bit packed MSB";
break;
case ASIOSTInt32MSB:
return "32-bit MSB";
break;
case ASIOSTFloat32MSB:
return "32-bit float MSB";
break;
case ASIOSTFloat64MSB:
return "64-bit float MSB";
break;
case ASIOSTInt32LSB16:
return "16-bit padded LSB";
break;
case ASIOSTInt32LSB18:
return "18-bit padded LSB";
break;
case ASIOSTInt32LSB20:
return "20-bit padded LSB";
break;
case ASIOSTInt32LSB24:
return "24-bit padded LSB";
break;
case ASIOSTInt32MSB16:
return "16-bit padded MSB";
break;
case ASIOSTInt32MSB18:
return "18-bit padded MSB";
break;
case ASIOSTInt32MSB20:
return "20-bit padded MSB";
break;
case ASIOSTInt32MSB24:
return "24-bit padded MSB";
break;
case ASIOSTDSDInt8LSB1:
return "1-bit LSB";
break;
case ASIOSTDSDInt8MSB1:
return "1-bit MSB";
break;
case ASIOSTDSDInt8NER8:
return "1-bit padded";
break;
}
return "Unknown";
}
void* TAAudioASIO::getContext() {
return (void*)&driverInfo;
}
int TAAudioASIO::specialCommand(TAAudioCommand which) {
switch (which) {
case TA_AUDIO_CMD_SETUP:
if (ASIOControlPanel()==ASE_NotPresent) return 0;
return 1;
break;
}
return -1;
}
bool TAAudioASIO::quit() {
if (!initialized) return false;
ASIOError result;
if (running) {
logV("CRASH: STOPPING NOW (QUIT)......");
result=ASIOStop();
if (result!=ASE_OK) {
logE("could not stop on quit! (%s)",getErrorStr(result));
}
running=false;
}
logV("CRASH: ASIODisposeBuffers()");
result=ASIODisposeBuffers();
if (result!=ASE_OK) {
logE("could not destroy buffers! (%s)",getErrorStr(result));
}
logV("CRASH: erase inBufs");
for (int i=0; i<desc.inChans; i++) {
delete[] inBufs[i];
inBufs[i]=NULL;
}
logV("CRASH: erase outBufs");
for (int i=0; i<desc.outChans; i++) {
delete[] outBufs[i];
outBufs[i]=NULL;
}
logV("CRASH: erase arrays");
if (inBufs!=NULL) {
delete[] inBufs;
inBufs=NULL;
}
if (outBufs!=NULL) {
delete[] outBufs;
outBufs=NULL;
}
logV("CRASH: ASIOExit()");
result=ASIOExit();
if (result!=ASE_OK) {
logE("could not exit!",getErrorStr(result));
}
logV("CRASH: removeCurrentDriver()");
asioDrivers->removeCurrentDriver();
logV("CRASH: reset callback instance");
callbackInstance=NULL;
initialized=false;
return true;
}
bool TAAudioASIO::setRun(bool run) {
if (!initialized) return false;
if (running==run) {
return running;
}
ASIOError result;
if (run) {
result=ASIOStart();
if (result!=ASE_OK) {
logE("could not start running! (%s)",getErrorStr(result));
return false;
}
running=true;
} else {
// does it matter whether stop was successful?
logV("CRASH: STOPPING NOW......");
result=ASIOStop();
if (result!=ASE_OK) {
logE("could not stop running! (%s)",getErrorStr(result));
}
running=false;
}
return running;
}
void TAAudioASIO::requestDeviceChange() {
deviceStatus=TA_AUDIO_DEVICE_RESET;
}
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...");
if (!driverNamesInit) {
for (int i=0; i<ASIO_DRIVER_MAX; i++) {
// 64 just in case
driverNames[i]=new char[64];
}
driverNamesInit=true;
}
driverCount=asioDrivers->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... (%s)",desc.deviceName);
strncpy(deviceNameCopy,desc.deviceName.c_str(),63);
if (!loadAsioDriver(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! (%s)",getErrorStr(result));
asioDrivers->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! (%s)",getErrorStr(result));
ASIOExit();
asioDrivers->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! (%s)",getErrorStr(result));
ASIOExit();
asioDrivers->removeCurrentDriver();
return false;
}
ASIOSampleRate outRate;
result=ASIOGetSampleRate(&outRate);
if (result!=ASE_OK) {
logE("could not get sample rate! (%s)",getErrorStr(result));
ASIOExit();
asioDrivers->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;
result=ASIOGetChannelInfo(&chanInfo[totalChans]);
if (result!=ASE_OK) {
logW("failed to get channel info for input channel %d! (%s)",i,getErrorStr(result));
}
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;
result=ASIOGetChannelInfo(&chanInfo[totalChans]);
if (result!=ASE_OK) {
logW("failed to get channel info for output channel %d! (%s)",i,getErrorStr(result));
}
bufInfo[totalChans].channelNum=i;
bufInfo[totalChans++].isInput=ASIOFalse;
outBufs[i]=new float[actualBufSize];
}
}
for (int i=0; i<totalChans; i++) {
logV("channel %d info: (index %d)",chanInfo[i].channel,i);
logV("- name: %s",chanInfo[i].name);
logV("- sample type: %s",getFormatName(chanInfo[i].type));
logV("- group: %d",chanInfo[i].channelGroup);
logV("- is input: %s",(chanInfo[i].isInput==ASIOTrue)?"yes":"no");
logV("- is active: %s",(chanInfo[i].isActive==ASIOTrue)?"yes":"no");
}
result=ASIOCreateBuffers(bufInfo,totalChans,actualBufSize,&callbacks);
if (result!=ASE_OK) {
logE("could not create buffers! (%s)",getErrorStr(result));
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;
}
result=ASIOExit();
if (result!=ASE_OK) {
logE("could not exit either! (%s)",getErrorStr(result));
}
asioDrivers->removeCurrentDriver();
return false;
}
desc.bufsize=actualBufSize;
desc.fragments=2;
response=desc;
initialized=true;
return true;
}
std::vector<String> TAAudioASIO::listAudioDevices() {
std::vector<String> ret;
if (!asioDrivers) asioDrivers=new AsioDrivers;
if (!driverNamesInit) {
for (int i=0; i<ASIO_DRIVER_MAX; i++) {
// 64 just in case
driverNames[i]=new char[64];
}
driverNamesInit=true;
}
driverCount=asioDrivers->getDriverNames(driverNames,ASIO_DRIVER_MAX);
for (int i=0; i<driverCount; i++) {
ret.push_back(driverNames[i]);
}
return ret;
}

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

@ -0,0 +1,61 @@
/**
* 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 {
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;
bool driverNamesInit;
char deviceNameCopy[64];
public:
void onSampleRate(double rate);
void onBufferSize(int bufsize);
void onProcess(int nframes);
void requestDeviceChange();
String getErrorStr(ASIOError which);
String getFormatName(ASIOSampleType which);
void* getContext();
int specialCommand(TAAudioCommand which);
bool quit();
bool setRun(bool run);
bool init(TAAudioDesc& request, TAAudioDesc& response);
std::vector<String> listAudioDevices();
TAAudioASIO():
totalChans(0),
driverCount(0),
driverNamesInit(false) {}
};

View file

@ -36,6 +36,20 @@ struct BufferSizeChangeEvent {
bufsize(bs) {}
};
enum TAAudioDeviceStatus {
// device is working
TA_AUDIO_DEVICE_OK=0,
// device has reset - reload audio engine
TA_AUDIO_DEVICE_RESET,
// device has been removed - reload audio engine with another device
TA_AUDIO_DEVICE_REMOVED
};
enum TAAudioCommand {
// open control panel for audio device
TA_AUDIO_CMD_SETUP=0
};
enum TAAudioFormat {
TA_AUDIO_FORMAT_F32=0,
TA_AUDIO_FORMAT_F64,
@ -157,6 +171,8 @@ class TAAudio {
protected:
TAAudioDesc desc;
TAAudioFormat outFormat;
TAAudioDeviceStatus deviceStatus;
bool running, initialized;
float** inBufs;
float** outBufs;
@ -173,15 +189,19 @@ class TAAudio {
void setCallback(void (*callback)(void*,float**,float**,int,int,unsigned int), void* user);
virtual void* getContext();
virtual int specialCommand(TAAudioCommand which);
virtual bool quit();
virtual bool setRun(bool run);
virtual std::vector<String> listAudioDevices();
TAAudioDeviceStatus getDeviceStatus();
void acceptDeviceStatus();
bool initMidi(bool jack);
void quitMidi();
virtual bool init(TAAudioDesc& request, TAAudioDesc& response);
TAAudio():
outFormat(TA_AUDIO_FORMAT_F32),
deviceStatus(TA_AUDIO_DEVICE_OK),
running(false),
initialized(false),
inBufs(NULL),

View file

@ -36,6 +36,9 @@
#ifdef HAVE_PA
#include "../audio/pa.h"
#endif
#ifdef HAVE_ASIO
#include "../audio/asio.h"
#endif
#include "../audio/pipe.h"
#include <math.h>
#include <float.h>
@ -3795,6 +3798,21 @@ TAAudioDesc& DivEngine::getAudioDescGot() {
return got;
}
TAAudioDeviceStatus DivEngine::getAudioDeviceStatus() {
if (output==NULL) return TA_AUDIO_DEVICE_OK;
return output->getDeviceStatus();
}
void DivEngine::acceptAudioDeviceStatus() {
if (output==NULL) return;
output->acceptDeviceStatus();
}
int DivEngine::audioBackendCommand(TAAudioCommand which) {
if (output==NULL) return -1;
return output->specialCommand(which);
}
std::vector<String>& DivEngine::getAudioDevices() {
return audioDevs;
}
@ -3909,6 +3927,8 @@ bool DivEngine::initAudioBackend() {
audioEngine=DIV_AUDIO_JACK;
} else if (getConfString("audioEngine","SDL")=="PortAudio") {
audioEngine=DIV_AUDIO_PORTAUDIO;
} else if (getConfString("audioEngine","SDL")=="ASIO") {
audioEngine=DIV_AUDIO_ASIO;
} else {
audioEngine=DIV_AUDIO_SDL;
}
@ -3970,6 +3990,21 @@ bool DivEngine::initAudioBackend() {
#endif
#else
output=new TAAudioPA;
#endif
break;
case DIV_AUDIO_ASIO:
#ifndef HAVE_ASIO
logE("Furnace was not compiled with ASIO support!");
setConf("audioEngine","SDL");
saveConf();
#ifdef HAVE_SDL2
output=new TAAudioSDL;
#else
logE("Furnace was not compiled with SDL support either!");
output=new TAAudio;
#endif
#else
output=new TAAudioASIO;
#endif
break;
case DIV_AUDIO_SDL:

View file

@ -76,6 +76,7 @@ enum DivAudioEngines {
DIV_AUDIO_SDL=1,
DIV_AUDIO_PORTAUDIO=2,
DIV_AUDIO_PIPE=3,
DIV_AUDIO_ASIO=4,
DIV_AUDIO_NULL=126,
DIV_AUDIO_DUMMY=127
@ -1402,6 +1403,15 @@ class DivEngine {
// get audio desc
TAAudioDesc& getAudioDescGot();
// get audio device status
TAAudioDeviceStatus getAudioDeviceStatus();
// acknowledge an audio device status change
void acceptAudioDeviceStatus();
// send command to audio backend
int audioBackendCommand(TAAudioCommand which);
// init dispatch
void initDispatch(bool isRender=false);

View file

@ -3394,8 +3394,8 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi
if (clampSamples) {
for (size_t i=0; i<size; i++) {
for (int j=0; j<outChans; j++) {
if (out[j][i]<-1.0) out[j][i]=-1.0;
if (out[j][i]>1.0) out[j][i]=1.0;
if (out[j][i]<-0.9999) out[j][i]=-0.9999;
if (out[j][i]>0.9999) out[j][i]=0.9999;
}
}
}

View file

@ -131,6 +131,9 @@ const char* aboutLine[]={
_N("Portable File Dialogs by Sam Hocevar"),
_N("Native File Dialog by Frogtoss Games"),
"PortAudio",
#ifdef HAVE_ASIO
_N("ASIO® by Steinberg Media Technologies"),
#endif
_N("Weak-JACK by x42"),
_N("RtMidi by Gary P. Scavone"),
_N("FFTW by Matteo Frigo and Steven G. Johnson"),
@ -196,8 +199,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 +221,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

@ -4333,6 +4333,17 @@ bool FurnaceGUI::loop() {
});
}
// recover from audio resets
TAAudioDeviceStatus audioStatus=e->getAudioDeviceStatus();
if (audioStatus!=TA_AUDIO_DEVICE_OK) {
logI("audio device reset!");
e->acceptAudioDeviceStatus();
if (!e->switchMaster(false)) {
showError(_("audio device has reset or has been disconnected! check audio settings."));
}
}
// recover from dead graphics
if (rend->isDead() || killGraphics) {
killGraphics=false;

View file

@ -130,7 +130,10 @@ const char* patFonts[]={
const char* audioBackends[]={
"JACK",
"SDL",
"PortAudio"
"PortAudio",
// pipe (invalid choice in GUI)
"Uhh, can you explain to me what exactly you were trying to do?",
"ASIO"
};
const char* audioQualities[]={
@ -1248,7 +1251,7 @@ void FurnaceGUI::drawSettings() {
if (ImGui::BeginTable("##Output",2)) {
ImGui::TableSetupColumn("##Label",ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("##Combo",ImGuiTableColumnFlags_WidthStretch);
#if defined(HAVE_JACK) || defined(HAVE_PA)
#if defined(HAVE_JACK) || defined(HAVE_PA) || defined(HAVE_ASIO)
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
@ -1271,6 +1274,12 @@ void FurnaceGUI::drawSettings() {
settings.audioEngine=DIV_AUDIO_PORTAUDIO;
settingsChanged=true;
}
#endif
#ifdef HAVE_ASIO
if (ImGui::Selectable("ASIO",settings.audioEngine==DIV_AUDIO_ASIO)) {
settings.audioEngine=DIV_AUDIO_ASIO;
settingsChanged=true;
}
#endif
if (settings.audioEngine!=prevAudioEngine) {
audioEngineChanged=true;
@ -1338,6 +1347,15 @@ void FurnaceGUI::drawSettings() {
}
}
if (settings.audioEngine==DIV_AUDIO_ASIO) {
ImGui::SameLine();
if (ImGui::Button(_("Control panel"))) {
if (e->audioBackendCommand(TA_AUDIO_CMD_SETUP)!=1) {
showError(_("this driver doesn't have a control panel."));
}
}
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
@ -4917,6 +4935,8 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) {
settings.audioEngine=DIV_AUDIO_JACK;
} else if (conf.getString("audioEngine","SDL")=="PortAudio") {
settings.audioEngine=DIV_AUDIO_PORTAUDIO;
} else if (conf.getString("audioEngine","SDL")=="ASIO") {
settings.audioEngine=DIV_AUDIO_ASIO;
} else {
settings.audioEngine=DIV_AUDIO_SDL;
}
@ -5201,7 +5221,7 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) {
clampSetting(settings.headFontSize,2,96);
clampSetting(settings.patFontSize,2,96);
clampSetting(settings.iconSize,2,48);
clampSetting(settings.audioEngine,0,2);
clampSetting(settings.audioEngine,0,4);
clampSetting(settings.audioQuality,0,1);
clampSetting(settings.audioHiPass,0,1);
clampSetting(settings.audioBufSize,32,4096);

View file

@ -178,11 +178,13 @@ TAParamResult pAudio(String val) {
e.setAudio(DIV_AUDIO_SDL);
} else if (val=="portaudio") {
e.setAudio(DIV_AUDIO_PORTAUDIO);
} else if (val=="asio") {
e.setAudio(DIV_AUDIO_ASIO);
} else if (val=="pipe") {
e.setAudio(DIV_AUDIO_PIPE);
changeLogOutput(stderr);
} else {
logE("invalid value for audio engine! valid values are: jack, sdl, portaudio, pipe.");
logE("invalid value for audio engine! valid values are: jack, sdl, portaudio, asio, pipe.");
return TA_PARAM_ERROR;
}
return TA_PARAM_SUCCESS;
@ -274,8 +276,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 +303,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 +370,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 +403,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;
}
@ -588,7 +615,7 @@ int main(int argc, char** argv) {
}
// co initialize ex
HRESULT coResult=CoInitializeEx(NULL,COINIT_MULTITHREADED);
HRESULT coResult=CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
if (coResult!=S_OK) {
logE("CoInitializeEx failed!");
}