From 68787a4d8b4e068eaab9c845c09cf711cf170af9 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 30 Aug 2023 17:32:51 -0500 Subject: [PATCH] add PortAudio backend - PLEASE READ PLEASE DO: ``` git submodule update --init --recursive ``` AFTER PULLING THIS COMMIT. --- .gitmodules | 3 + CMakeLists.txt | 31 ++++++ README.md | 1 + extern/portaudio | 1 + src/audio/pa.cpp | 222 ++++++++++++++++++++++++++++++++++++++++++ src/audio/pa.h | 38 ++++++++ src/engine/engine.cpp | 20 ++++ src/engine/engine.h | 1 + src/gui/settings.cpp | 27 ++++- 9 files changed, 340 insertions(+), 4 deletions(-) create mode 160000 extern/portaudio create mode 100644 src/audio/pa.cpp create mode 100644 src/audio/pa.h diff --git a/.gitmodules b/.gitmodules index f3ef8bbd..c78fee42 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,3 +12,6 @@ [submodule "extern/adpcm"] path = extern/adpcm url = https://github.com/superctr/adpcm +[submodule "extern/portaudio"] + path = extern/portaudio + url = https://github.com/PortAudio/portaudio.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 63053c78..11174ba3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ include(TestBigEndian) if (ANDROID) set(USE_RTMIDI_DEFAULT OFF) + set(WITH_PORTAUDIO_DEFAULT OFF) set(USE_BACKWARD_DEFAULT OFF) find_library(TERMUX rt) if (TERMUX) @@ -34,6 +35,7 @@ if (ANDROID) endif() else() set(USE_RTMIDI_DEFAULT ON) + set(WITH_PORTAUDIO_DEFAULT ON) if (WIN32 OR APPLE) set(USE_BACKWARD_DEFAULT ON) else() @@ -78,6 +80,7 @@ option(USE_SDL2 "Build with SDL2. Required to build with GUI." ${USE_SDL2_DEFAUL option(USE_SNDFILE "Build with libsndfile. Required in order to work with audio files." ${USE_SNDFILE_DEFAULT}) option(USE_BACKWARD "Use backward-cpp to print a backtrace on crash/abort." ${USE_BACKWARD_DEFAULT}) option(WITH_JACK "Whether to build with JACK support. Auto-detects if JACK is available" ${WITH_JACK_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}) option(WITH_RENDER_DX11 "Whether to build with the DirectX 11 render backend." ${WITH_RENDER_DX11_DEFAULT}) @@ -85,6 +88,7 @@ option(USE_GLES "Use OpenGL ES for the OpenGL render backend." ${USE_GLES_DEFAUL option(SYSTEM_FFTW "Use a system-installed version of FFTW instead of the vendored one" OFF) option(SYSTEM_FMT "Use a system-installed version of fmt instead of the vendored one" OFF) option(SYSTEM_LIBSNDFILE "Use a system-installed version of libsndfile instead of the vendored one" OFF) +option(SYSTEM_PORTAUDIO "Use a system-installed version of PortAudio instead of the vendored one" OFF) option(SYSTEM_RTMIDI "Use a system-installed version of RtMidi instead of the vendored one" OFF) option(SYSTEM_ZLIB "Use a system-installed version of zlib instead of the vendored one" OFF) option(SYSTEM_SDL2 "Use a system-installed version of SDL2 instead of the vendored one" ${SYSTEM_SDL2_DEFAULT}) @@ -204,6 +208,25 @@ else() message(STATUS "Not using libsndfile") endif() +if (WITH_PORTAUDIO) + if (SYSTEM_PORTAUDIO) + find_package(PkgConfig REQUIRED) + pkg_check_modules(PORTAUDIO REQUIRED portaudio) + list(APPEND DEPENDENCIES_INCLUDE_DIRS ${PORTAUDIO_INCLUDE_DIRS}) + list(APPEND DEPENDENCIES_COMPILE_OPTIONS ${PORTAUDIO_CFLAGS_OTHER}) + list(APPEND DEPENDENCIES_LIBRARIES ${PORTAUDIO_LIBRARIES}) + list(APPEND DEPENDENCIES_LIBRARY_DIRS ${PORTAUDIO_LIBRARY_DIRS}) + list(APPEND DEPENDENCIES_LINK_OPTIONS ${PORTAUDIO_LDFLAGS_OTHER}) + list(APPEND DEPENDENCIES_LEGACY_LDFLAGS ${PORTAUDIO_LDFLAGS}) + message(STATUS "Using system-installed PortAudio") + else() + set(PA_BUILD_SHARED_LIBS OFF CACHE BOOL "Build dynamic library" FORCE) + add_subdirectory(extern/portaudio EXCLUDE_FROM_ALL) + list(APPEND DEPENDENCIES_LIBRARIES PortAudio) + message(STATUS "Using vendored PortAudio") + endif() +endif() + if (USE_RTMIDI) if (SYSTEM_RTMIDI) find_package(PkgConfig REQUIRED) @@ -345,6 +368,14 @@ else() message(STATUS "Building without JACK support") endif() +if (WITH_PORTAUDIO) + list(APPEND AUDIO_SOURCES src/audio/pa.cpp) + message(STATUS "Building with PortAudio") + list(APPEND DEPENDENCIES_DEFINES HAVE_PA) +else() + message(STATUS "Building without PortAudio") +endif() + if (USE_RTMIDI) list(APPEND AUDIO_SOURCES src/audio/rtmidi.cpp) message(STATUS "Building with RtMidi") diff --git a/README.md b/README.md index 08ccfc85..37549dbd 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,7 @@ Available options: | `USE_SNDFILE` | `ON` | Build with libsndfile (required in order to work with audio files) | | `USE_BACKWARD` | `ON` | Use backward-cpp to print a backtrace on crash/abort | | `WITH_JACK` | `ON` if system-installed JACK detected, otherwise `OFF` | Whether to build with JACK support. Auto-detects if JACK is available | +| `WITH_PORTAUDIO` | `ON` | Whether to build with PortAudio. | | `SYSTEM_FFTW` | `OFF` | Use a system-installed version of FFTW instead of the vendored one | | `SYSTEM_FMT` | `OFF` | Use a system-installed version of fmt instead of the vendored one | | `SYSTEM_LIBSNDFILE` | `OFF` | Use a system-installed version of libsndfile instead of the vendored one | diff --git a/extern/portaudio b/extern/portaudio new file mode 160000 index 00000000..6ee9836a --- /dev/null +++ b/extern/portaudio @@ -0,0 +1 @@ +Subproject commit 6ee9836a08d201c118b4715d4d70242816584000 diff --git a/src/audio/pa.cpp b/src/audio/pa.cpp new file mode 100644 index 00000000..722d29fd --- /dev/null +++ b/src/audio/pa.cpp @@ -0,0 +1,222 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 +#include +#include "../ta-log.h" +#include "pa.h" + +int taPAProcess(const void* in, void* out, unsigned long nframes, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags flags, void* inst) { + TAAudioPA* instance=(TAAudioPA*)inst; + return instance->onProcess(in,out,nframes,timeInfo,flags); +} + +int TAAudioPA::onProcess(const void* in, void* out, unsigned long nframes, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags flags) { + for (int i=0; idesc.bufsize) { + delete[] inBufs[i]; + inBufs[i]=new float[nframes]; + } + } + for (int i=0; idesc.bufsize) { + delete[] outBufs[i]; + outBufs[i]=new float[nframes]; + } + } + if (nframes!=desc.bufsize) { + desc.bufsize=nframes; + } + + if (audioProcCallback!=NULL) { + if (midiIn!=NULL) midiIn->gather(); + audioProcCallback(audioProcCallbackUser,inBufs,outBufs,desc.inChans,desc.outChans,desc.bufsize); + } + float* fbuf=(float*)out; + for (size_t j=0; j TAAudioPA::listAudioDevices() { + std::vector ret; + if (!audioSysStarted) { + PaError status=Pa_Initialize(); + if (status!=paNoError) { + logE("could not initialize PortAudio to list audio devices"); + return ret; + } else { + audioSysStarted=true; + } + } + + int count=Pa_GetDeviceCount(); + if (count<0) return ret; + + for (int i=0; imaxOutputChannels<1) continue; + + if (devInfo->name!=NULL) { + ret.push_back(String(devInfo->name)); + } + } + + return ret; +} + +bool TAAudioPA::init(TAAudioDesc& request, TAAudioDesc& response) { + if (initialized) { + logE("audio already initialized"); + return false; + } + PaError status; + + if (!audioSysStarted) { + status=Pa_Initialize(); + if (status!=paNoError) { + logE("could not initialize PortAudio"); + return false; + } else { + audioSysStarted=true; + } + } + + desc=request; + desc.outFormat=TA_AUDIO_FORMAT_F32; + + const PaDeviceInfo* devInfo=NULL; + int outDeviceID=0; + + if (desc.deviceName.empty()) { + outDeviceID=Pa_GetDefaultOutputDevice(); + devInfo=Pa_GetDeviceInfo(outDeviceID); + } else { + int count=Pa_GetDeviceCount(); + bool found=false; + if (count<0) { + logE("audio device not found"); + return false; + } + + for (int i=0; imaxOutputChannels<1) continue; + + if (devInfo->name!=NULL) { + if (strcmp(devInfo->name,desc.deviceName.c_str())==0) { + outDeviceID=i; + found=true; + break; + } + } + } + if (!found) { + logE("audio device not found"); + return false; + } + } + + // check output channels and sample rate + if (devInfo!=NULL) { + if (desc.outChans>devInfo->maxOutputChannels) desc.outChans=devInfo->maxOutputChannels; + } + + PaStreamParameters outParams; + outParams.device=outDeviceID; + outParams.channelCount=desc.outChans; + outParams.sampleFormat=paFloat32; + outParams.suggestedLatency=(double)(desc.bufsize*desc.fragments)/desc.rate; + outParams.hostApiSpecificStreamInfo=NULL; + + logV("opening audio device..."); + status=Pa_OpenStream( + &ac, + NULL, + &outParams, + desc.rate, + 0, + paClipOff|paDitherOff, + taPAProcess, + this + ); + if (status!=paNoError) { + logE("could not open audio device: %s",Pa_GetErrorText(status)); + return false; + } + + desc.deviceName=devInfo->name; + desc.inChans=0; + + if (desc.outChans>0) { + outBufs=new float*[desc.outChans]; + for (int i=0; i + +class TAAudioPA: public TAAudio { + PaStream* ac; + bool audioSysStarted; + + public: + int onProcess(const void* in, void* out, unsigned long nframes, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags flags); + + void* getContext(); + bool quit(); + bool setRun(bool run); + std::vector listAudioDevices(); + bool init(TAAudioDesc& request, TAAudioDesc& response); + TAAudioPA(): + ac(NULL), + audioSysStarted(false) {} +}; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 7428de45..f60bdf6a 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -32,6 +32,9 @@ #ifdef HAVE_JACK #include "../audio/jack.h" #endif +#ifdef HAVE_PA +#include "../audio/pa.h" +#endif #include #include #include @@ -3279,6 +3282,8 @@ bool DivEngine::initAudioBackend() { if (audioEngine==DIV_AUDIO_NULL) { if (getConfString("audioEngine","SDL")=="JACK") { audioEngine=DIV_AUDIO_JACK; + } else if (getConfString("audioEngine","SDL")=="PortAudio") { + audioEngine=DIV_AUDIO_PORTAUDIO; } else { audioEngine=DIV_AUDIO_SDL; } @@ -3322,6 +3327,21 @@ bool DivEngine::initAudioBackend() { #endif #else output=new TAAudioJACK; +#endif + break; + case DIV_AUDIO_PORTAUDIO: +#ifndef HAVE_PA + logE("Furnace was not compiled with PortAudio!"); + 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 TAAudioPA; #endif break; case DIV_AUDIO_SDL: diff --git a/src/engine/engine.h b/src/engine/engine.h index 66d64529..0c755f8f 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -73,6 +73,7 @@ enum DivStatusView { enum DivAudioEngines { DIV_AUDIO_JACK=0, DIV_AUDIO_SDL=1, + DIV_AUDIO_PORTAUDIO=2, DIV_AUDIO_NULL=126, DIV_AUDIO_DUMMY=127 diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 3f3c5aab..bd9f949b 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -82,11 +82,13 @@ const char* patFonts[]={ const char* audioBackends[]={ "JACK", - "SDL" + "SDL", + "PortAudio" }; const bool isProAudio[]={ true, + false, false }; @@ -724,17 +726,27 @@ void FurnaceGUI::drawSettings() { if (ImGui::BeginTable("##Output",2)) { ImGui::TableSetupColumn("##Label",ImGuiTableColumnFlags_WidthFixed); ImGui::TableSetupColumn("##Combo",ImGuiTableColumnFlags_WidthStretch); -#ifdef HAVE_JACK +#if defined(HAVE_JACK) || defined(HAVE_PA) ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::AlignTextToFramePadding(); ImGui::Text("Backend"); ImGui::TableNextColumn(); int prevAudioEngine=settings.audioEngine; - if (ImGui::Combo("##Backend",&settings.audioEngine,audioBackends,2)) { + if (ImGui::BeginCombo("##Backend",audioBackends[settings.audioEngine])) { + if (ImGui::Selectable("JACK",settings.audioEngine==DIV_AUDIO_JACK)) { + settings.audioEngine=DIV_AUDIO_JACK; + } + if (ImGui::Selectable("SDL",settings.audioEngine==DIV_AUDIO_SDL)) { + settings.audioEngine=DIV_AUDIO_SDL; + } + if (ImGui::Selectable("PortAudio",settings.audioEngine==DIV_AUDIO_PORTAUDIO)) { + settings.audioEngine=DIV_AUDIO_PORTAUDIO; + } if (settings.audioEngine!=prevAudioEngine) { if (!isProAudio[settings.audioEngine]) settings.audioChans=2; } + ImGui::EndCombo(); } #endif @@ -3045,6 +3057,13 @@ void FurnaceGUI::syncSettings() { settings.patFontSize=e->getConfInt("patFontSize",18); settings.iconSize=e->getConfInt("iconSize",16); settings.audioEngine=(e->getConfString("audioEngine","SDL")=="SDL")?1:0; + if (e->getConfString("audioEngine","SDL")=="PortAudio") { + settings.audioEngine=DIV_AUDIO_JACK; + } else if (e->getConfString("audioEngine","SDL")=="PortAudio") { + settings.audioEngine=DIV_AUDIO_PORTAUDIO; + } else { + settings.audioEngine=DIV_AUDIO_SDL; + } settings.audioDevice=e->getConfString("audioDevice",""); settings.audioChans=e->getConfInt("audioChans",2); settings.midiInDevice=e->getConfString("midiInDevice",""); @@ -3218,7 +3237,7 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.headFontSize,2,96); clampSetting(settings.patFontSize,2,96); clampSetting(settings.iconSize,2,48); - clampSetting(settings.audioEngine,0,1); + clampSetting(settings.audioEngine,0,2); clampSetting(settings.audioQuality,0,1); clampSetting(settings.audioBufSize,32,4096); clampSetting(settings.audioRate,8000,384000);