diff --git a/.gitignore b/.gitignore index 7defc56f..41405e54 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ Release/ Vsts/build/ Tests/PlayerTest/build/ build/ +cmake-build/ Vst3.x/* !Vst3.x/README Data/data.aps diff --git a/CMakeLists.txt b/CMakeLists.txt index fa768de4..44656a90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,24 +3,43 @@ project("WaveSabre") cmake_minimum_required(VERSION 3.11) set_property(GLOBAL PROPERTY USE_FOLDERS ON) +if(NOT WIN32) + # compo build defaults + option(ENABLE_PTHREADS "Use Pthreads for threading" OFF) + option(ENABLE_APLAY "Enable aplay(1) backend for audio playback" ON) + option(ENABLE_SDL2 "Enable SDL2_Audio backend for audio playback" OFF) + option(ENABLE_FFMPEG_GSM "Enable GSM 6.10 sample decoding using FFmpeg. Required for Thunder and Specimen." OFF) + option(ENABLE_EXTERNAL_GMDLS "Enable using a gm.dls file provided at runtime" OFF) + # for standalone playback, or playing back Windows-specific tracks, you + # probably want to enable all of the above options +endif() + if(MSVC) # disable exceptions globally (will be added back for VSTs) string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) endif() -set(VSTSDK3_DIR "./Vst3.x/" CACHE PATH "VSTSDK location") +if(WIN32) + set(VSTSDK3_DIR "./Vst3.x/" CACHE PATH "VSTSDK location") +endif() # shared code -add_subdirectory(MSVCRT) +if(WIN32) + add_subdirectory(MSVCRT) +endif() add_subdirectory(WaveSabreCore) add_subdirectory(WaveSabrePlayerLib) # binaries -add_subdirectory(Tests/PlayerTest) +if(WIN32) + add_subdirectory(Tests/PlayerTest) +endif() add_subdirectory(WaveSabreStandAlonePlayer) # VSTs -if(VSTSDK3_DIR) - add_subdirectory(WaveSabreVstLib) - add_subdirectory(Vsts) +if(WIN32) + if(VSTSDK3_DIR) + add_subdirectory(WaveSabreVstLib) + add_subdirectory(Vsts) + endif() endif() diff --git a/WaveSabreCore/CMakeLists.txt b/WaveSabreCore/CMakeLists.txt index 31ca1e2e..60a5505f 100644 --- a/WaveSabreCore/CMakeLists.txt +++ b/WaveSabreCore/CMakeLists.txt @@ -17,6 +17,7 @@ add_library(WaveSabreCore include/WaveSabreCore/Leveller.h include/WaveSabreCore/MxcsrFlagGuard.h include/WaveSabreCore/ResampleBuffer.h + include/WaveSabreCore/SampleLoader.h include/WaveSabreCore/SamplePlayer.h include/WaveSabreCore/Scissor.h include/WaveSabreCore/Slaughter.h @@ -44,6 +45,7 @@ add_library(WaveSabreCore src/Leveller.cpp src/MxcsrFlagGuard.cpp src/ResampleBuffer.cpp + src/SampleLoader.cpp src/SamplePlayer.cpp src/Scissor.cpp src/Slaughter.cpp @@ -52,9 +54,16 @@ add_library(WaveSabreCore src/StateVariableFilter.cpp src/SynthDevice.cpp src/Thunder.cpp - src/Twister.cpp) + src/Twister.cpp + ) + +if(WIN32) + target_sources(WaveSabreCore PRIVATE + ) + + target_link_libraries(WaveSabreCore PUBLIC Msacm32.lib) +endif() -target_link_libraries(WaveSabreCore Msacm32.lib) target_include_directories(WaveSabreCore PUBLIC include) if(MSVC) @@ -69,4 +78,29 @@ if(MSVC) target_compile_options(WaveSabreCore PUBLIC $<$:/Zc:sizedDealloc->) endif() +else() + # assuming GCC or clang for now + + if(ENABLE_FFMPEG_GSM) + message(NOTICE "Enabling FFmpeg GSM 6.10 decoding") + target_compile_definitions(WaveSabreCore PRIVATE HAVE_FFMPEG_GSM=1) + else() + message(NOTICE "FFmpeg GSM 6.10 decoding disabled") + endif() + + if(ENABLE_EXTERNAL_GMDLS) + message(NOTICE "Enabling gm.dls support") + target_compile_definitions(WaveSabreCore PRIVATE HAVE_EXTERNAL_GMDLS=1) + else() + message(NOTICE "External gm.dls loading disabled") + endif() + + if(CMAKE_BUILD_TYPE EQUAL Debug) + target_compile_options(WaveSabreCore PUBLIC -g -Og) + else() + #set_property(TARGET WaveSabreCore PROPERTY INTERPROCEDURAL_OPTIMIZATION ON) + target_compile_options(WaveSabreCore + PUBLIC -O2 -fno-exceptions -fno-rtti -fno-stack-protector -fno-stack-check -fno-unwind-tables -fno-asynchronous-unwind-tables -fomit-frame-pointer -fno-threadsafe-statics + PRIVATE -ffast-math -march=nocona -ffunction-sections -fdata-sections -Wl,--gc-sections) + endif() endif() diff --git a/WaveSabreCore/include/WaveSabreCore/SampleLoader.h b/WaveSabreCore/include/WaveSabreCore/SampleLoader.h new file mode 100644 index 00000000..cf0c97ea --- /dev/null +++ b/WaveSabreCore/include/WaveSabreCore/SampleLoader.h @@ -0,0 +1,58 @@ +#ifndef __WAVESABRECORE_SAMPLELOADER_H__ +#define __WAVESABRECORE_SAMPLELOADER_H__ + +#if defined(WIN32) || defined(_WIN32) +#include +#include + +#ifdef UNICODE +#define _UNICODE +#endif +#include +#else /* not WIN32 */ +#include + +typedef struct __attribute__((__packed__)) tWAVEFORMATEX { + int16_t wFormatTag; + int16_t nChannels; + int32_t nSamplesPerSec; + int32_t nAvgBytesPerSec; + int16_t nBlockAlign; + int16_t wBitsPerSample; + int16_t cbSize; +} WAVEFORMATEX; +#endif /* WIN32 */ + +namespace WaveSabreCore +{ + class SampleLoader + { + public: + static const int SampleRate = 44100; + + struct LoadedSample { + char *chunkData; + + char *waveFormatData; + int compressedSize, uncompressedSize; + + char *compressedData; + float *sampleData; + + int sampleLength; + }; + + static LoadedSample LoadSampleGSM(char *data, int compressedSize, int uncompressedSize, WAVEFORMATEX *waveFormat); + + private: +#if defined(WIN32) || defined(_WIN32) + static BOOL __stdcall driverEnumCallback(HACMDRIVERID driverId, DWORD_PTR dwInstance, DWORD fdwSupport); + static BOOL __stdcall formatEnumCallback(HACMDRIVERID driverId, LPACMFORMATDETAILS formatDetails, DWORD_PTR dwInstance, DWORD fdwSupport); + + static HACMDRIVERID driverId; +#endif + + }; +} + +#endif diff --git a/WaveSabreCore/include/WaveSabreCore/Specimen.h b/WaveSabreCore/include/WaveSabreCore/Specimen.h index 86c3d7ec..ff3fd498 100644 --- a/WaveSabreCore/include/WaveSabreCore/Specimen.h +++ b/WaveSabreCore/include/WaveSabreCore/Specimen.h @@ -4,21 +4,16 @@ #include "SynthDevice.h" #include "Envelope.h" #include "StateVariableFilter.h" +#include "SampleLoader.h" #include "SamplePlayer.h" -#include -#include - -#ifdef UNICODE -#define _UNICODE -#endif -#include - namespace WaveSabreCore { class Specimen : public SynthDevice { public: + static const int SampleRate = SampleLoader::SampleRate; + enum class ParamIndices { AmpAttack, @@ -59,8 +54,6 @@ namespace WaveSabreCore NumParams, }; - static const int SampleRate = 44100; - Specimen(); virtual ~Specimen(); @@ -70,8 +63,26 @@ namespace WaveSabreCore virtual void SetChunk(void *data, int size); virtual int GetChunk(void **data); - void LoadSample(char *data, int compressedSize, int uncompressedSize, WAVEFORMATEX *waveFormat); - + inline void LoadSample(char *compressedDataPtr, int compressedSize, + int uncompressedSize, WAVEFORMATEX *waveFormatPtr) + { + auto sample = SampleLoader::LoadSampleGSM(compressedDataPtr, + compressedSize, uncompressedSize, waveFormatPtr); + + this->compressedSize = sample.compressedSize; + this->uncompressedSize = sample.uncompressedSize; + + if (waveFormatData) delete [] waveFormatData; + waveFormatData = sample.waveFormatData; + if (compressedData) delete [] compressedData; + compressedData = sample.compressedData; + if (sampleData) delete [] sampleData; + sampleData = sample.sampleData; + + sampleLength = sample.sampleLength; + sampleLoopStart = 0; + sampleLoopLength = sampleLength; + } private: class SpecimenVoice : public Voice { @@ -99,11 +110,6 @@ namespace WaveSabreCore float velocity; }; - static BOOL __stdcall driverEnumCallback(HACMDRIVERID driverId, DWORD_PTR dwInstance, DWORD fdwSupport); - static BOOL __stdcall formatEnumCallback(HACMDRIVERID driverId, LPACMFORMATDETAILS formatDetails, DWORD_PTR dwInstance, DWORD fdwSupport); - - static HACMDRIVERID driverId; - char *chunkData; char *waveFormatData; diff --git a/WaveSabreCore/include/WaveSabreCore/Thunder.h b/WaveSabreCore/include/WaveSabreCore/Thunder.h index 44e5d4b6..c083f450 100644 --- a/WaveSabreCore/include/WaveSabreCore/Thunder.h +++ b/WaveSabreCore/include/WaveSabreCore/Thunder.h @@ -2,21 +2,14 @@ #define __WAVESABRECORE_THUNDER_H__ #include "SynthDevice.h" - -#include -#include - -#ifdef UNICODE -#define _UNICODE -#endif -#include +#include "SampleLoader.h" namespace WaveSabreCore { class Thunder : public SynthDevice { public: - static const int SampleRate = 44100; + static const int SampleRate = SampleLoader::SampleRate; Thunder(); virtual ~Thunder(); @@ -24,8 +17,24 @@ namespace WaveSabreCore virtual void SetChunk(void *data, int size); virtual int GetChunk(void **data); - void LoadSample(char *data, int compressedSize, int uncompressedSize, WAVEFORMATEX *waveFormat); + inline void LoadSample(char *compressedDataPtr, int compressedSize, + int uncompressedSize, WAVEFORMATEX *waveFormatPtr) + { + auto sample = SampleLoader::LoadSampleGSM(compressedDataPtr, + compressedSize, uncompressedSize, waveFormatPtr); + + this->compressedSize = sample.compressedSize; + this->uncompressedSize = sample.uncompressedSize; + + if (waveFormatData) delete [] waveFormatData; + waveFormatData = sample.waveFormatData; + if (compressedData) delete [] compressedData; + compressedData = sample.compressedData; + if (sampleData) delete [] sampleData; + sampleData = sample.sampleData; + sampleLength = sample.sampleLength; + } private: class ThunderVoice : public Voice { @@ -43,11 +52,6 @@ namespace WaveSabreCore int samplePos; }; - static BOOL __stdcall driverEnumCallback(HACMDRIVERID driverId, DWORD_PTR dwInstance, DWORD fdwSupport); - static BOOL __stdcall formatEnumCallback(HACMDRIVERID driverId, LPACMFORMATDETAILS formatDetails, DWORD_PTR dwInstance, DWORD fdwSupport); - - static HACMDRIVERID driverId; - char *chunkData; char *waveFormatData; diff --git a/WaveSabreCore/include/WaveSabreCore/Twister.h b/WaveSabreCore/include/WaveSabreCore/Twister.h index 536eeb7c..14fb4fde 100644 --- a/WaveSabreCore/include/WaveSabreCore/Twister.h +++ b/WaveSabreCore/include/WaveSabreCore/Twister.h @@ -65,7 +65,7 @@ namespace WaveSabreCore ResampleBuffer leftBuffer; ResampleBuffer rightBuffer; - + StateVariableFilter lowCutFilter[2], highCutFilter[2]; }; } diff --git a/WaveSabreCore/src/Adultery.cpp b/WaveSabreCore/src/Adultery.cpp index 9447f324..a278daf8 100644 --- a/WaveSabreCore/src/Adultery.cpp +++ b/WaveSabreCore/src/Adultery.cpp @@ -98,6 +98,15 @@ namespace WaveSabreCore if (sampleIndex >= 0) { auto gmDls = GmDls::Load(); + if (gmDls == nullptr) { + sampleLength = 1; + sampleData = new float[sampleLength]; + sampleData[0] = 0.0f; + sampleLoopStart = 0; + sampleLoopLength = 1; + + break; + } // Seek to wave pool chunk's data auto ptr = gmDls + GmDls::WaveListOffset; @@ -240,7 +249,7 @@ namespace WaveSabreCore case ParamIndices::VoicesUnisono: return Helpers::UnisonoToParam(VoicesUnisono); case ParamIndices::VoicesDetune: return VoicesDetune; case ParamIndices::VoicesPan: return VoicesPan; - + case ParamIndices::VoiceMode: return Helpers::VoiceModeToParam(GetVoiceMode()); case ParamIndices::SlideTime: return Slide; @@ -319,7 +328,7 @@ namespace WaveSabreCore modEnv.Sustain = adultery->modSustain; modEnv.Release = adultery->modRelease; modEnv.Trigger(); - + samplePlayer.SampleData = adultery->sampleData; samplePlayer.SampleLength = adultery->sampleLength; samplePlayer.SampleLoopStart = adultery->sampleLoopStart; diff --git a/WaveSabreCore/src/Cathedral.cpp b/WaveSabreCore/src/Cathedral.cpp index e2951355..00452012 100644 --- a/WaveSabreCore/src/Cathedral.cpp +++ b/WaveSabreCore/src/Cathedral.cpp @@ -79,7 +79,7 @@ namespace WaveSabreCore outL += combLeft[i].Process(input); outR += combRight[i].Process(input); } - + // Feed through allpasses in series for (int i = 0; i < numAllPasses; i++) { diff --git a/WaveSabreCore/src/GmDls.cpp b/WaveSabreCore/src/GmDls.cpp index 1ca8e566..59341872 100644 --- a/WaveSabreCore/src/GmDls.cpp +++ b/WaveSabreCore/src/GmDls.cpp @@ -1,8 +1,14 @@ #include +#if defined(WIN32) || defined(_WIN32) #include +#elif HAVE_EXTERNAL_GMDLS +#include +#include +#include +#endif -static char *gmDlsPaths[2] = +static char const *gmDlsPaths[2] = { "drivers/gm.dls", "drivers/etc/gm.dls" @@ -12,6 +18,7 @@ namespace WaveSabreCore { unsigned char *GmDls::Load() { +#if defined(WIN32) || defined(_WIN32) HANDLE gmDlsFile = INVALID_HANDLE_VALUE; for (int i = 0; gmDlsFile == INVALID_HANDLE_VALUE; i++) { @@ -26,5 +33,91 @@ namespace WaveSabreCore CloseHandle(gmDlsFile); return gmDls; +#elif HAVE_EXTERNAL_GMDLS + unsigned char *rv = NULL; + + bool gmDlsPathAlloc = false; + bool dataHomeAlloc = false; + char *gmDlsPath, *dataHome; + char *home = getenv("HOME"); + FILE *filed; + long filesz; + + // first, check if there's a user-specified one available + gmDlsPath = getenv("WAVESABRE_GMDLS_PATH"); + + if (gmDlsPath != NULL && access(gmDlsPath, R_OK) == 0) + goto have_gmdls; + +try_xdgdat: + // try something XDG-conformant next + dataHome = getenv("XDG_DATA_HOME"); + if (dataHome == NULL) { + if (home == NULL) + goto try_winepfx; // fuck it + + dataHomeAlloc = true; + dataHome = (char*)malloc(0x100/*PATH_MAX*/); + snprintf(dataHome, 0x100, "%s/.local/share", home); + } + + gmDlsPathAlloc = true; + gmDlsPath = (char*)malloc(0x100); + snprintf(gmDlsPath, 0x100, "%s/WaveSabre/gm.dls", dataHome); + if (dataHomeAlloc) free(dataHome); + + if (access(gmDlsPath, R_OK) == 0) + goto have_gmdls; + + free(gmDlsPath); + gmDlsPathAlloc = false; + + // ... try grabbing it from the user's wineprefix, because no trick too dirty for us +try_winepfx: + if (home == NULL) + goto try_cwd; + + for (int i = 0; i < sizeof(gmDlsPaths)/sizeof(*gmDlsPaths); ++i) { + gmDlsPathAlloc = true; + gmDlsPath = (char*)malloc(0x100); + // TODO: $WINEPREFIX + snprintf(gmDlsPath, 0x100, "%s/.wine/drive_c/windows/system32/%s", home, gmDlsPaths[i]); + + if (access(gmDlsPath, R_OK) == 0) + goto have_gmdls; + + free(gmDlsPath); + gmDlsPathAlloc = false; + } + +try_cwd: // fuck it, grab it from the current dir + gmDlsPath = "./gm.dls"; + + if (access(gmDlsPath, R_OK) == 0) + goto have_gmdls; + + return rv; + +have_gmdls: // actually read the file + filed = fopen(gmDlsPath, "rb"); + if (filed == NULL) + goto end; + + // get size of file + fseek(filed, 0, SEEK_END); + filesz = ftell(filed); + fseek(filed, 0, SEEK_SET); + + rv = new unsigned char[filesz]; + fread(rv, 1, filesz, filed); + fclose(filed); + +end: + if (gmDlsPathAlloc) free(gmDlsPath); + + return rv; +#else + return NULL; +#endif } } diff --git a/WaveSabreCore/src/Helpers.cpp b/WaveSabreCore/src/Helpers.cpp index 3027da9e..80009aa6 100644 --- a/WaveSabreCore/src/Helpers.cpp +++ b/WaveSabreCore/src/Helpers.cpp @@ -4,8 +4,17 @@ #define _USE_MATH_DEFINES #include +// TODO: make assembly equivalent for Windows x64 (use intrinsic ?) +// ^--- you probably only need to change esp to rsp? -poro + +#if (defined(_MSC_VER) && defined(_M_IX86)) || defined(__GNUC__) + #define ASM_MATH_AVAILABLE (1) +#else /* nor MSVC nor GCC/clang */ + #define ASM_MATH_AVAILABLE (0) +#endif + +#if ASM_MATH_AVAILABLE == 1 #if defined(_MSC_VER) && defined(_M_IX86) -// TODO: make assembly equivalent for x64 (use intrinsic ?) static __declspec(naked) double __vectorcall fpuPow(double x, double y) { __asm @@ -53,7 +62,87 @@ static __declspec(naked) double __vectorcall fpuPow(double x, double y) ret } } +#elif defined(__GNUC__) + #if defined(__x86_64__) || defined(__i386__) +__attribute__((__naked__,__noinline__)) static double fpuPow(double x, double y) +{ + // i386 Linux ABI: pass thru the stack, return in st(0) + // x86_64 SysV ABI: pass/return thru xmm0/1 + asm volatile( +#ifdef __x86_64__ + "subq $8, %%rsp\n" +#else + "movsd 4(%%esp), %%xmm0\n" + "movsd 12(%%esp), %%xmm1\n" + "subl $8, %%esp\n" +#endif + "xorpd %%xmm2, %%xmm2\n" + "comisd %%xmm2, %%xmm1\n" + "jne 1f\n" + + "fld1\n" + "jmp 3f\n" + + "1:\n" + "comisd %%xmm2, %%xmm0\n" + "jne 2f\n" + + "fldz\n" + "jmp 3f\n" + + "2:\n" +#ifdef __x86_64__ + "movsd %%xmm1, (%%rsp)\n" + "fldl (%%rsp)\n" + "movsd %%xmm0, (%%rsp)\n" + "fldl (%%rsp)\n" +#else + "movsd %%xmm1, (%%esp)\n" + "fldl (%%esp)\n" + "movsd %%xmm0, (%%esp)\n" + "fldl (%%esp)\n" +#endif + + "fyl2x\n" + "fld %%st(0)\n" + "frndint\n" + "fsub %%st(0), %%st(1)\n" + "fxch %%st(1)\n" + "fchs\n" + "f2xm1\n" + "fld1\n" + "faddp %%st(0), %%st(1)\n" + "fscale\n" + "fstp %%st(1)\n" + + "3:\n" +#ifdef __x86_64__ + "fstpl (%%rsp)\n" + "movsd (%%rsp), %%xmm0\n" + "addq $8, %%rsp\n" +#else + "addl $8, %%esp\n" +#endif + "ret\n" + :// no output + :// no input + :"xmm2" // clobbered + ); +} + #else + // __builtin_pow only supports integer exponents... so if the exponent + // is an integer, use __builtin_pow, using some preprocessor magic + #define fpuPow(x, y) \ + ((__builtin_constant_p(y) && ((y) == (int)(y))) \ + ? __builtin_pow(x, y) \ + : pow(x, y)) \ + + #endif +#else +#error "Unsupported compiler." +#endif /* compiler */ +#if defined(_MSC_VER) && defined(_M_IX86) static __declspec(naked) float __vectorcall fpuPowF(float x, float y) { __asm @@ -101,7 +190,87 @@ static __declspec(naked) float __vectorcall fpuPowF(float x, float y) ret } } +#elif defined(__GNUC__) + #if defined(__x86_64__) || defined(__i386__) +__attribute__((__naked__,__noinline__)) static float fpuPowF(float x, float y) +{ + // i386 Linux ABI: pass thru the stack, return in st(0) + // x86_64 SysV ABI: pass/return thru xmm0/1 + asm volatile( +#ifdef __x86_64__ + "subq $8, %%rsp\n" +#else + "movss 4(%%esp), %%xmm0\n" + "movss 8(%%esp), %%xmm1\n" + "subl $8, %%esp\n" +#endif + "xorps %%xmm2, %%xmm2\n" + "comiss %%xmm2, %%xmm1\n" + "jne 1f\n" + + "fld1\n" + "jmp 3f\n" + + "1:\n" + "comiss %%xmm2, %%xmm0\n" + "jne 2f\n" + + "fldz\n" + "jmp 3f\n" + + "2:\n" +#ifdef __x86_64__ + "movss %%xmm1, (%%rsp)\n" + "flds (%%rsp)\n" + "movss %%xmm0, (%%rsp)\n" + "flds (%%rsp)\n" +#else + "movss %%xmm1, (%%esp)\n" + "flds (%%esp)\n" + "movss %%xmm0, (%%esp)\n" + "flds (%%esp)\n" +#endif + + "fyl2x\n" + "fld %%st(0)\n" + "frndint\n" + "fsub %%st(0), %%st(1)\n" + "fxch %%st(1)\n" + "fchs\n" + "f2xm1\n" + "fld1\n" + "faddp %%st(0), %%st(1)\n" + "fscale\n" + "fstp %%st(1)\n" + + "3:\n" +#ifdef __x86_64__ + "fstps (%%rsp)\n" + "movss (%%rsp), %%xmm0\n" + "addq $8, %%rsp\n" +#else + "addl $8, %%esp\n" +#endif + "ret\n" + :// no output + :// no input + :"xmm2" // clobbered + ); +} + #else + // __builtin_powf only supports integer exponents... so if the exponent + // is an integer, use __builtin_powf, using some preprocessor magic + #define fpuPowF(x, y) \ + ((__builtin_constant_p(y) && ((y) == (int)(y))) \ + ? __builtin_powf(x, y) \ + : powf(x, y)) \ + + #endif +#else +#error "Unsupported compiler." +#endif /* compiler */ +#if defined(_MSC_VER) && defined(_M_IX86) static __declspec(naked) double __vectorcall fpuCos(double x) { __asm @@ -119,7 +288,24 @@ static __declspec(naked) double __vectorcall fpuCos(double x) ret } } -#endif // defined(_MSC_VER) && defined(_M_IX86) +#elif defined(__GNUC__) + #if defined(__x86_64__) || defined(__i386__) +__attribute__((__always_inline__)) inline static double fpuCos(double x) +{ + // not writing the *entire* function body in assembly actually helps + // gcc and clang with inlining and LTO + // ... except trying this with fpuPow/F somehow got botched, so those I + // wrote as pure assembly + asm volatile("fcos\n":"+t"(x)::); + return x; +} + #else /* x86_64 */ + #define fpuCos(x) __builtin_cos(x) + #endif /* GNUC, platform */ +#else +#error "Unsupported compiler." +#endif /* compiler */ +#endif // ASM_MATH_AVAILABLE == 1 namespace WaveSabreCore { @@ -138,7 +324,7 @@ namespace WaveSabreCore for (int i = 0; i < fastCosTabSize + 1; i++) { double phase = double(i) * ((M_PI * 2) / fastCosTabSize); -#if defined(_MSC_VER) && defined(_M_IX86) +#if ASM_MATH_AVAILABLE == 1 fastCosTab[i] = fpuCos(phase); #else fastCosTab[i] = cos(phase); @@ -153,7 +339,7 @@ namespace WaveSabreCore double Helpers::Pow(double x, double y) { -#if defined(_MSC_VER) && defined(_M_IX86) +#if ASM_MATH_AVAILABLE == 1 return fpuPow(x, y); #else return pow(x, y); @@ -162,7 +348,7 @@ namespace WaveSabreCore float Helpers::PowF(float x, float y) { -#if defined(_MSC_VER) && defined(_M_IX86) +#if ASM_MATH_AVAILABLE == 1 return fpuPowF(x, y); #else return powf(x, y); @@ -365,7 +551,7 @@ namespace WaveSabreCore { return (Spread)(int)(param * 2.0f); } - + float Helpers::SpreadToParam(Spread spread) { return (float)spread / 2.0f; diff --git a/WaveSabreCore/src/Leveller.cpp b/WaveSabreCore/src/Leveller.cpp index 59ba5941..23c9ddd2 100644 --- a/WaveSabreCore/src/Leveller.cpp +++ b/WaveSabreCore/src/Leveller.cpp @@ -124,7 +124,7 @@ namespace WaveSabreCore case ParamIndices::HighCutFreq: return Helpers::FrequencyToParam(highCutFreq); case ParamIndices::HighCutQ: return Helpers::QToParam(highCutQ); - + case ParamIndices::Master: return master; } } diff --git a/WaveSabreCore/src/ResampleBuffer.cpp b/WaveSabreCore/src/ResampleBuffer.cpp index a1759baa..3dbfbb6f 100644 --- a/WaveSabreCore/src/ResampleBuffer.cpp +++ b/WaveSabreCore/src/ResampleBuffer.cpp @@ -50,7 +50,7 @@ namespace WaveSabreCore { int samplePos = (currentPosition + (int)position) % length; // actual sample position determined float fraction = position - floorf(position); // fractional - + float s0 = buffer[samplePos]; float s1 = (samplePos > 0) ? buffer[samplePos - 1] : buffer[length - 1]; return s0 + fraction * (s1 - s0); diff --git a/WaveSabreCore/src/SampleLoader.cpp b/WaveSabreCore/src/SampleLoader.cpp new file mode 100644 index 00000000..3d07a384 --- /dev/null +++ b/WaveSabreCore/src/SampleLoader.cpp @@ -0,0 +1,264 @@ +#include + +#include + +#if !defined(WIN32) && !defined(_WIN32) && HAVE_FFMPEG_GSM +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace WaveSabreCore +{ +#if defined(WIN32) || defined(_WIN32) + HACMDRIVERID SampleLoader::driverId = NULL; +#endif + + SampleLoader::LoadedSample SampleLoader::LoadSampleGSM(char *data, int compressedSize, int uncompressedSize, WAVEFORMATEX *waveFormat) + { + LoadedSample ret; + + ret.compressedSize = compressedSize; + ret.uncompressedSize = uncompressedSize; + + //if (waveFormatData) delete [] waveFormatData; + ret.waveFormatData = new char[sizeof(WAVEFORMATEX) + waveFormat->cbSize]; + memcpy(ret.waveFormatData, waveFormat, sizeof(WAVEFORMATEX) + waveFormat->cbSize); + //if (compressedData) delete [] compressedData; + ret.compressedData = new char[compressedSize]; + memcpy(ret.compressedData, data, compressedSize); + + //if (sampleData) delete [] sampleData; + +#if defined(WIN32) || defined(_WIN32) + acmDriverEnum(driverEnumCallback, NULL, NULL); + HACMDRIVER driver = NULL; + acmDriverOpen(&driver, driverId, 0); + + WAVEFORMATEX dstWaveFormat = + { + WAVE_FORMAT_PCM, + 1, + waveFormat->nSamplesPerSec, + waveFormat->nSamplesPerSec * 2, + sizeof(short), + sizeof(short) * 8, + 0 + }; + + HACMSTREAM stream = NULL; + acmStreamOpen(&stream, driver, waveFormat, &dstWaveFormat, NULL, NULL, NULL, ACM_STREAMOPENF_NONREALTIME); + + ACMSTREAMHEADER streamHeader; + memset(&streamHeader, 0, sizeof(ACMSTREAMHEADER)); + streamHeader.cbStruct = sizeof(ACMSTREAMHEADER); + streamHeader.pbSrc = (LPBYTE)ret.compressedData; + streamHeader.cbSrcLength = compressedSize; + auto uncompressedData = new short[uncompressedSize * 2]; + streamHeader.pbDst = (LPBYTE)uncompressedData; + streamHeader.cbDstLength = uncompressedSize * 2; + acmStreamPrepareHeader(stream, &streamHeader, 0); + + acmStreamConvert(stream, &streamHeader, 0); + + acmStreamClose(stream, 0); + acmDriverClose(driver, 0); + + ret.sampleLength = streamHeader.cbDstLengthUsed / sizeof(short); + ret.sampleData = new float[ret.sampleLength]; + for (int i = 0; i < ret.sampleLength; i++) + ret.sampleData[i] = (float)((double)uncompressedData[i] / 32768.0); + + delete [] uncompressedData; +#elif HAVE_FFMPEG_GSM + // you're going to hate me for this and I'll absolutely deserve it + + // cat test.gsm \ + // | ffmpeg -i - -f f32le -acodec pcm_f32le - 2>/dev/null \ + // | aplay -traw -c1 -r44100 -fFLOAT - + + int gsmpipe[2], wavpipe[2]; + int rv; + rv = pipe(gsmpipe); + assert(rv == 0 && "Can't set up input pipe for ffmpeg"); + rv = pipe(wavpipe); + assert(rv == 0 && "Can't set up output pipe for ffmpeg"); + + int gsmread = gsmpipe[0], gsmwrite = gsmpipe[1], + wavread = wavpipe[0], wavwrite = wavpipe[1]; + + pid_t child = fork(); // dun dun duuuuun + assert(child >= 0 && "fork() failed."); + + if (child == 0) { + // child + close(gsmwrite); + close(wavread); + close(STDERR_FILENO); + + dup2(gsmread, STDIN_FILENO); + dup2(wavwrite, STDOUT_FILENO); + + char *const args[] = {"/usr/bin/ffmpeg", "-i", "-", "-f", "f32le", + "-acodec", "pcm_f32le", "-", NULL}; + rv = execve("/usr/bin/ffmpeg", args, environ); + assert(rv >= 0 && "Failed to run FFmpeg!"); + // unreachable + } else { + // parent + close(gsmread); + close(wavwrite); + + // write fake(ish) WAV header + size_t M = (waveFormat->nAvgBytesPerSec * waveFormat->nSamplesPerSec) / waveFormat->nChannels; + size_t wvfmtsz = sizeof(WAVEFORMATEX) + waveFormat->cbSize; +#ifndef NDEBUG + assert(waveFormat->cbSize <= 8 && "cbSize too high!"); +#endif + char fileheader[4+4/*RIFF*/ + 4/*WAVE*/ + 4+4+wvfmtsz/*fmt */ + 4+4+4/*fact*/ + 4+4/*data*/]; + + memcpy(&fileheader[0], "RIFF", 4); + *(uint32_t*)&fileheader[4] = 4 + 4+4+wvfmtsz + 4+4+4 + 4+4+compressedSize; + + memcpy(&fileheader[8], "WAVEfmt ", 8); + *(uint32_t*)&fileheader[16] = wvfmtsz; + memcpy(&fileheader[20], waveFormat, wvfmtsz); + + memcpy(&fileheader[20+wvfmtsz], "FACT\x04\0\0\0", 8); + *(uint32_t*)&fileheader[28+wvfmtsz] = M; + + memcpy(&fileheader[32+wvfmtsz], "data", 4); + *(uint32_t*)&fileheader[36+wvfmtsz] = compressedSize; + + bool wrote_hdr = false; + uint8_t* uncompr = (uint8_t*)malloc(uncompressedSize); + size_t uncomprOff = 0; + + int rev; + while (true) { + bool needDataOut = compressedSize > 0 || !wrote_hdr; + bool needDataIn = uncomprOff < uncompressedSize; + + if (!needDataOut && !needDataIn) + break; + + struct pollfd watched[2]; + watched[0].fd = wavread; + watched[0].events = POLLIN; + watched[0].revents = 0; + watched[1].fd = gsmwrite; + watched[1].events = needDataOut ? POLLOUT : 0; + watched[1].revents = 0; + + rv = poll(watched, (compressedSize > 0) ? 2 : 1, -1); + assert(rv >= 0 && "poll(2) failed?!"); + + if (watched[0].revents & POLLNVAL) + watched[0].revents |= POLLERR; + if (watched[1].revents & POLLNVAL) + watched[1].revents |= POLLERR; + rev = (watched[0].revents | watched[1].revents); + + if ((rev & POLLOUT) && needDataOut && !(watched[1].revents & POLLERR)) { + if (!wrote_hdr) { + wrote_hdr = true; + write(gsmwrite, fileheader, sizeof(fileheader)); + } else { + size_t towr = compressedSize; + if (towr > PIPE_BUF) + towr = PIPE_BUF; + rv = write(gsmwrite, data, towr); + assert(rv >= 0 && "Couldn't write GSM data to FFmpeg"); + compressedSize -= rv; + + if (compressedSize <= 0) close(gsmwrite); + } + } + if ((rev & POLLIN) && uncomprOff < uncompressedSize && !(watched[0].revents & POLLERR)) { + // TODO: make nonblocking, seek for smallest possible data size left + size_t toRead = 0x100; + if (toRead > (uncompressedSize - uncomprOff)) + toRead = (uncompressedSize - uncomprOff); + rv = read(wavread, uncompr + uncomprOff, toRead); + assert(rv >= 0 && "Couldn't read converted GSM data from FFmpeg"); + uncomprOff += rv; + + if (uncomprOff >= uncompressedSize) close(wavread); + } + + if (rev & (POLLHUP | POLLERR)) + break; + } + + if (rev & POLLERR) { + printf("I/O error to ffmpeg?\n"); + } + + waitpid(child, &rev, 0); + if (rev != 0) { + printf("FFmpeg exited with error %d!\n", rev); + } + + ret.sampleLength = uncompressedSize / sizeof(float); + ret.sampleData = new float[ret.sampleLength]; + memcpy(ret.sampleData, uncompr, uncompressedSize); + free(uncompr); + } +#else + // sorry, not supported. +#ifndef NDEBUG + printf("WARNING: trying to load a GSM sample, while this is unsupported. Output will be silence.\n"); +#endif + ret.sampleLength = 1; + ret.sampleData = new float[1]; + ret.sampleData[0] = 0; +#endif + + return ret; + } + +#if defined(WIN32) || defined(_WIN32) + BOOL __stdcall SampleLoader::driverEnumCallback(HACMDRIVERID driverId, DWORD_PTR dwInstance, DWORD fdwSupport) + { + if (SampleLoader::driverId) return 1; + + HACMDRIVER driver = NULL; + acmDriverOpen(&driver, driverId, 0); + + int waveFormatSize = 0; + acmMetrics(NULL, ACM_METRIC_MAX_SIZE_FORMAT, &waveFormatSize); + auto waveFormat = (WAVEFORMATEX *)(new char[waveFormatSize]); + memset(waveFormat, 0, waveFormatSize); + ACMFORMATDETAILS formatDetails; + memset(&formatDetails, 0, sizeof(formatDetails)); + formatDetails.cbStruct = sizeof(formatDetails); + formatDetails.pwfx = waveFormat; + formatDetails.cbwfx = waveFormatSize; + formatDetails.dwFormatTag = WAVE_FORMAT_UNKNOWN; + acmFormatEnum(driver, &formatDetails, formatEnumCallback, NULL, NULL); + + delete [] (char *)waveFormat; + + acmDriverClose(driver, 0); + + return 1; + } + + BOOL __stdcall SampleLoader::formatEnumCallback(HACMDRIVERID driverId, LPACMFORMATDETAILS formatDetails, DWORD_PTR dwInstance, DWORD fdwSupport) + { + if (formatDetails->pwfx->wFormatTag == WAVE_FORMAT_GSM610 && + formatDetails->pwfx->nChannels == 1 && + formatDetails->pwfx->nSamplesPerSec == SampleRate) + { + SampleLoader::driverId = driverId; + } + return 1; + } +#endif +} diff --git a/WaveSabreCore/src/Specimen.cpp b/WaveSabreCore/src/Specimen.cpp index eb0abd88..aafe84ec 100644 --- a/WaveSabreCore/src/Specimen.cpp +++ b/WaveSabreCore/src/Specimen.cpp @@ -1,12 +1,12 @@ #include #include +#include #include +#include namespace WaveSabreCore { - HACMDRIVERID Specimen::driverId = NULL; - Specimen::Specimen() : SynthDevice(0) { @@ -165,6 +165,7 @@ namespace WaveSabreCore // Read compressed data and load sample auto compressedDataPtr = (char *)waveFormatPtr + waveFormatSize; auto compressedDataSize = headerPtr->CompressedSize; + LoadSample(compressedDataPtr, headerPtr->CompressedSize, headerPtr->UncompressedSize, waveFormatPtr); // Read params @@ -228,62 +229,6 @@ namespace WaveSabreCore return size; } - void Specimen::LoadSample(char *data, int compressedSize, int uncompressedSize, WAVEFORMATEX *waveFormat) - { - this->compressedSize = compressedSize; - this->uncompressedSize = uncompressedSize; - - if (waveFormatData) delete [] waveFormatData; - waveFormatData = new char[sizeof(WAVEFORMATEX) + waveFormat->cbSize]; - memcpy(waveFormatData, waveFormat, sizeof(WAVEFORMATEX) + waveFormat->cbSize); - if (compressedData) delete [] compressedData; - compressedData = new char[compressedSize]; - memcpy(compressedData, data, compressedSize); - - acmDriverEnum(driverEnumCallback, NULL, NULL); - HACMDRIVER driver = NULL; - acmDriverOpen(&driver, driverId, 0); - - WAVEFORMATEX dstWaveFormat = - { - WAVE_FORMAT_PCM, - 1, - waveFormat->nSamplesPerSec, - waveFormat->nSamplesPerSec * 2, - sizeof(short), - sizeof(short) * 8, - 0 - }; - - HACMSTREAM stream = NULL; - acmStreamOpen(&stream, driver, waveFormat, &dstWaveFormat, NULL, NULL, NULL, ACM_STREAMOPENF_NONREALTIME); - - ACMSTREAMHEADER streamHeader; - memset(&streamHeader, 0, sizeof(ACMSTREAMHEADER)); - streamHeader.cbStruct = sizeof(ACMSTREAMHEADER); - streamHeader.pbSrc = (LPBYTE)compressedData; - streamHeader.cbSrcLength = compressedSize; - auto uncompressedData = new short[uncompressedSize * 2]; - streamHeader.pbDst = (LPBYTE)uncompressedData; - streamHeader.cbDstLength = uncompressedSize * 2; - acmStreamPrepareHeader(stream, &streamHeader, 0); - - acmStreamConvert(stream, &streamHeader, 0); - - acmStreamClose(stream, 0); - acmDriverClose(driver, 0); - - sampleLength = streamHeader.cbDstLengthUsed / sizeof(short); - if (sampleData) delete [] sampleData; - sampleData = new float[sampleLength]; - for (int i = 0; i < sampleLength; i++) sampleData[i] = (float)((double)uncompressedData[i] / 32768.0); - - sampleLoopStart = 0; - sampleLoopLength = sampleLength; - - delete [] uncompressedData; - } - Specimen::SpecimenVoice::SpecimenVoice(Specimen *specimen) { this->specimen = specimen; @@ -374,7 +319,7 @@ namespace WaveSabreCore this->velocity = (float)velocity / 128.0f; } - + void Specimen::SpecimenVoice::NoteOff() { ampEnv.Off(); @@ -390,41 +335,4 @@ namespace WaveSabreCore { samplePlayer.CalcPitch(GetNote() - 60 + Detune + specimen->fineTune * 2.0f - 1.0f + SpecimenVoice::coarseDetune(specimen->coarseTune)); } - - BOOL __stdcall Specimen::driverEnumCallback(HACMDRIVERID driverId, DWORD_PTR dwInstance, DWORD fdwSupport) - { - if (Specimen::driverId) return 1; - - HACMDRIVER driver = NULL; - acmDriverOpen(&driver, driverId, 0); - - int waveFormatSize = 0; - acmMetrics(NULL, ACM_METRIC_MAX_SIZE_FORMAT, &waveFormatSize); - auto waveFormat = (WAVEFORMATEX *)(new char[waveFormatSize]); - memset(waveFormat, 0, waveFormatSize); - ACMFORMATDETAILS formatDetails; - memset(&formatDetails, 0, sizeof(formatDetails)); - formatDetails.cbStruct = sizeof(formatDetails); - formatDetails.pwfx = waveFormat; - formatDetails.cbwfx = waveFormatSize; - formatDetails.dwFormatTag = WAVE_FORMAT_UNKNOWN; - acmFormatEnum(driver, &formatDetails, formatEnumCallback, NULL, NULL); - - delete [] (char *)waveFormat; - - acmDriverClose(driver, 0); - - return 1; - } - - BOOL __stdcall Specimen::formatEnumCallback(HACMDRIVERID driverId, LPACMFORMATDETAILS formatDetails, DWORD_PTR dwInstance, DWORD fdwSupport) - { - if (formatDetails->pwfx->wFormatTag == WAVE_FORMAT_GSM610 && - formatDetails->pwfx->nChannels == 1 && - formatDetails->pwfx->nSamplesPerSec == SampleRate) - { - Specimen::driverId = driverId; - } - return 1; - } } diff --git a/WaveSabreCore/src/SynthDevice.cpp b/WaveSabreCore/src/SynthDevice.cpp index c8544a25..5d7b8b29 100644 --- a/WaveSabreCore/src/SynthDevice.cpp +++ b/WaveSabreCore/src/SynthDevice.cpp @@ -223,7 +223,7 @@ namespace WaveSabreCore { return voiceMode; } - + SynthDevice::Voice::Voice() { IsOn = false; @@ -248,7 +248,7 @@ namespace WaveSabreCore { slideActive = true; destinationNote = note; - + double slideTime = 10.f * Helpers::Pow(this->GetSynthDevice()->Slide,4.0); slideDelta = ((double)note - currentNote) / (Helpers::CurrentSampleRate * slideTime); slideSamples = (int)(Helpers::CurrentSampleRate * slideTime); diff --git a/WaveSabreCore/src/Thunder.cpp b/WaveSabreCore/src/Thunder.cpp index c40d9094..9d922904 100644 --- a/WaveSabreCore/src/Thunder.cpp +++ b/WaveSabreCore/src/Thunder.cpp @@ -1,12 +1,12 @@ #include #include +#include +#include #include namespace WaveSabreCore { - HACMDRIVERID Thunder::driverId = NULL; - Thunder::Thunder() : SynthDevice(0) { @@ -62,59 +62,6 @@ namespace WaveSabreCore return chunkSize; } - void Thunder::LoadSample(char *data, int compressedSize, int uncompressedSize, WAVEFORMATEX *waveFormat) - { - this->compressedSize = compressedSize; - this->uncompressedSize = uncompressedSize; - - if (waveFormatData) delete [] waveFormatData; - waveFormatData = new char[sizeof(WAVEFORMATEX) + waveFormat->cbSize]; - memcpy(waveFormatData, waveFormat, sizeof(WAVEFORMATEX) + waveFormat->cbSize); - if (compressedData) delete [] compressedData; - compressedData = new char[compressedSize]; - memcpy(compressedData, data, compressedSize); - - acmDriverEnum(driverEnumCallback, NULL, NULL); - HACMDRIVER driver = NULL; - acmDriverOpen(&driver, driverId, 0); - - WAVEFORMATEX dstWaveFormat = - { - WAVE_FORMAT_PCM, - 1, - waveFormat->nSamplesPerSec, - waveFormat->nSamplesPerSec * 2, - sizeof(short), - sizeof(short) * 8, - 0 - }; - - HACMSTREAM stream = NULL; - acmStreamOpen(&stream, driver, waveFormat, &dstWaveFormat, NULL, NULL, NULL, ACM_STREAMOPENF_NONREALTIME); - - ACMSTREAMHEADER streamHeader; - memset(&streamHeader, 0, sizeof(ACMSTREAMHEADER)); - streamHeader.cbStruct = sizeof(ACMSTREAMHEADER); - streamHeader.pbSrc = (LPBYTE)compressedData; - streamHeader.cbSrcLength = compressedSize; - auto uncompressedData = new short[uncompressedSize * 2]; - streamHeader.pbDst = (LPBYTE)uncompressedData; - streamHeader.cbDstLength = uncompressedSize * 2; - acmStreamPrepareHeader(stream, &streamHeader, 0); - - acmStreamConvert(stream, &streamHeader, 0); - - acmStreamClose(stream, 0); - acmDriverClose(driver, 0); - - sampleLength = streamHeader.cbDstLengthUsed / sizeof(short); - if (sampleData) delete [] sampleData; - sampleData = new float[sampleLength]; - for (int i = 0; i < sampleLength; i++) sampleData[i] = (float)((double)uncompressedData[i] / 32768.0); - - delete [] uncompressedData; - } - Thunder::ThunderVoice::ThunderVoice(Thunder *thunder) { this->thunder = thunder; @@ -146,41 +93,4 @@ namespace WaveSabreCore Voice::NoteOn(note, velocity, detune, pan); samplePos = 0; } - - BOOL __stdcall Thunder::driverEnumCallback(HACMDRIVERID driverId, DWORD_PTR dwInstance, DWORD fdwSupport) - { - if (Thunder::driverId) return 1; - - HACMDRIVER driver = NULL; - acmDriverOpen(&driver, driverId, 0); - - int waveFormatSize = 0; - acmMetrics(NULL, ACM_METRIC_MAX_SIZE_FORMAT, &waveFormatSize); - auto waveFormat = (WAVEFORMATEX *)(new char[waveFormatSize]); - memset(waveFormat, 0, waveFormatSize); - ACMFORMATDETAILS formatDetails; - memset(&formatDetails, 0, sizeof(formatDetails)); - formatDetails.cbStruct = sizeof(formatDetails); - formatDetails.pwfx = waveFormat; - formatDetails.cbwfx = waveFormatSize; - formatDetails.dwFormatTag = WAVE_FORMAT_UNKNOWN; - acmFormatEnum(driver, &formatDetails, formatEnumCallback, NULL, NULL); - - delete [] (char *)waveFormat; - - acmDriverClose(driver, 0); - - return 1; - } - - BOOL __stdcall Thunder::formatEnumCallback(HACMDRIVERID driverId, LPACMFORMATDETAILS formatDetails, DWORD_PTR dwInstance, DWORD fdwSupport) - { - if (formatDetails->pwfx->wFormatTag == WAVE_FORMAT_GSM610 && - formatDetails->pwfx->nChannels == 1 && - formatDetails->pwfx->nSamplesPerSec == SampleRate) - { - Thunder::driverId = driverId; - } - return 1; - } } diff --git a/WaveSabreCore/src/Twister.cpp b/WaveSabreCore/src/Twister.cpp index 5c3c1813..52f2a0a8 100644 --- a/WaveSabreCore/src/Twister.cpp +++ b/WaveSabreCore/src/Twister.cpp @@ -14,9 +14,9 @@ namespace WaveSabreCore spread = Spread::Mono; vibratoFreq = Helpers::ParamToVibratoFreq(0.0f); vibratoAmount = 0.0f; - + vibratoPhase = 0.0; - + lowCutFreq = 20.0f; highCutFreq = 20000.0f- 20.0f; @@ -166,8 +166,8 @@ namespace WaveSabreCore { switch ((ParamIndices)index) { - case ParamIndices::Type: - default: + case ParamIndices::Type: + default: return type / 3.0f; case ParamIndices::Amount: return amount; diff --git a/WaveSabrePlayerLib/CMakeLists.txt b/WaveSabrePlayerLib/CMakeLists.txt index 6a0149d4..f14a6eb9 100644 --- a/WaveSabrePlayerLib/CMakeLists.txt +++ b/WaveSabrePlayerLib/CMakeLists.txt @@ -1,24 +1,78 @@ add_library(WaveSabrePlayerLib include/WaveSabrePlayerLib/CriticalSection.h + include/WaveSabrePlayerLib/IRenderThread.h include/WaveSabrePlayerLib/PreRenderPlayer.h include/WaveSabrePlayerLib/WavWriter.h - include/WaveSabrePlayerLib/DirectSoundRenderThread.h - include/WaveSabrePlayerLib/RealtimePlayer.h include/WaveSabrePlayerLib/IPlayer.h + include/WaveSabrePlayerLib/RealtimePlayer.h include/WaveSabrePlayerLib/SongRenderer.h src/CriticalSection.cpp - src/DirectSoundRenderThread.cpp src/IPlayer.cpp + src/IRenderThread.cpp src/PreRenderPlayer.cpp src/RealtimePlayer.cpp src/SongRenderer.cpp src/SongRenderer.Track.cpp src/WavWriter.cpp) -target_link_libraries(WaveSabrePlayerLib - WaveSabreCore - winmm.lib - dsound.lib) +target_link_libraries(WaveSabrePlayerLib PUBLIC + WaveSabreCore) + +if(WIN32) + message(STATUS "Win32: using DirectSound renderthread") + target_sources(WaveSabrePlayerLib PRIVATE + include/WaveSabrePlayerLib/DirectSoundRenderThread.h + src/DirectSoundRenderThread.cpp) + + target_link_libraries(WaveSabrePlayerLib PUBLIC + winmm.lib + dsound.lib) +else() + if(ENABLE_APLAY) + message(STATUS "Enabling aplay(1)-based backend") + target_compile_definitions(WaveSabrePlayerLib PRIVATE HAVE_APLAY=1) + target_sources(WaveSabrePlayerLib PRIVATE + include/WaveSabrePlayerLib/AplayRenderThread.h + src/AplayRenderThread.cpp) + else() + message(STATUS "aplay(1) backend disabled") + endif() + + if(ENABLE_SDL2) + message(STATUS "Enabling SDL2 backend") + + find_package(SDL2) + if(SDL2_FOUND) + message(STATUS "Found SDL2, enabling SDL2 backend") + target_link_libraries(WaveSabrePlayerLib PUBLIC SDL2::SDL2 SDL2::SDL2main) + target_compile_definitions(WaveSabrePlayerLib PUBLIC HAVE_SDL2=1) + target_sources(WaveSabrePlayerLib PRIVATE + include/WaveSabrePlayerLib/SDL2RenderThread.h + src/SDL2RenderThread.cpp) + else() + message(WARNING "SDL2 not found, disabling SDL2 backend") + endif() + else() + message(STATUS "SDL2 backend disabled") + endif() + + if(ENABLE_PTHREADS) + set_property(TARGET WaveSabrePlayerLib PROPERTY CXX_STANDARD 11) # need a recent one for stdatomic.h + + message(STATUS "Trying to look for pthreads...") + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads) + if(CMAKE_USE_PTHREADS_INIT) + message(STATUS "Found pthreads, enabling pthreads for background rendering threads.") + target_link_libraries(WaveSabrePlayerLib PRIVATE Threads::Threads) + target_compile_definitions(WaveSabrePlayerLib PRIVATE HAVE_PTHREAD=1) + else() + message(WARNING "pthreads not found, using synchronous fallback.") + endif() + else() + message(STATUS "pthreads disabled explicitely.") + endif() +endif() target_include_directories(WaveSabrePlayerLib PUBLIC include) @@ -28,4 +82,21 @@ if(MSVC) $<$:/EHs-c->) set_property(TARGET WaveSabrePlayerLib APPEND_STRING PROPERTY STATIC_LIBRARY_FLAGS_MINSIZEREL " /LTCG") +else() + # assuming GCC or clang for now + + if(CMAKE_BUILD_TYPE EQUAL Debug) + set(CMAKE_CXX_FLAGS "-g -Og") + else() + set_property(TARGET PROPERTY INTERPROCEDURAL_OPTIMIZATION True) # enable LTO + set(CMAKE_CXX_FLAGS "-g -O2 -ffast-math -fno-exceptions -fno-rtti -fno-stack-protector -fno-stack-check -fno-unwind-tables -fno-asynchronous-unwind-tables -fomit-frame-pointer -fno-threadsafe-statics -march=nocona -ffunction-sections -fdata-sections -Wl,--gc-sections") + endif() + if(CMAKE_BUILD_TYPE EQUAL Debug) + target_compile_options(WaveSabrePlayerLib PUBLIC -g -Og) + else() + #set_property(TARGET WaveSabrePlayerLib PROPERTY INTERPROCEDURAL_OPTIMIZATION ON) + target_compile_options(WaveSabrePlayerLib + PUBLIC -O2 -fno-exceptions -fno-rtti -fno-stack-protector -fno-stack-check -fno-unwind-tables -fno-asynchronous-unwind-tables -fomit-frame-pointer -fno-threadsafe-statics + PRIVATE -ffast-math -march=nocona -ffunction-sections -fdata-sections -Wl,--gc-sections) + endif() endif() diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/AplayRenderThread.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/AplayRenderThread.h new file mode 100644 index 00000000..52f7f3e6 --- /dev/null +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/AplayRenderThread.h @@ -0,0 +1,61 @@ +#ifndef __WAVESABREPLAYERLIB_APLAYRENDERTHREAD_H__ +#define __WAVESABREPLAYERLIB_APLAYRENDERTHREAD_H__ + +#include "SongRenderer.h" +#include "CriticalSection.h" +#include "IRenderThread.h" + +#include + +#include +#include + +#if HAVE_PTHREAD +#include +#endif + +namespace WaveSabrePlayerLib +{ + class AplayRenderThread : public IRenderThread + { + public: + // 'pthread': if true, spawn a separate thread using pthreads and do + // the poll/select/send-to-alsa loop there, if not, you have to + // periodically call DoForegroundWork, but, pthread won't be needed + AplayRenderThread(RenderCallback callback, void *callbackData, + int sampleRate, int bufferSizeMs = 1000, bool pthread = false); + virtual ~AplayRenderThread(); + + virtual int GetPlayPositionMs(); + + virtual void DoForegroundWork(); + + private: + static void AplayProc(int readend, int rate); +#if HAVE_PTHREAD + static void* AplayWriterProc(void* ud); +#endif + + void GetBufferTick(bool block = false); + + RenderCallback callback; + void *callbackData; + int sampleRate; + int bufferSizeMs; + int bufferSizeBytes; + int bytesWritten; + SongRenderer::Sample *sampleBuffer; + const SongRenderer::Sample *bufferToWrite; + size_t bufferBytesLeft; + size_t writeBytesMax; + + int aupipe; + pid_t aplay; +#if HAVE_PTHREAD + pthread_t writer; + bool writerStarted; +#endif + }; +} + +#endif diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/CriticalSection.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/CriticalSection.h index 84ce77e5..6ac1d25a 100644 --- a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/CriticalSection.h +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/CriticalSection.h @@ -1,7 +1,14 @@ #ifndef __WAVESABREPLAYERLIB_CRITICALSECTION__ #define __WAVESABREPLAYERLIB_CRITICALSECTION__ +#if defined(WIN32) || defined(_WIN32) #include +#elif HAVE_PTHREAD +#include +#include + +#include +#endif namespace WaveSabrePlayerLib { @@ -24,7 +31,11 @@ namespace WaveSabrePlayerLib CriticalSectionGuard Enter(); private: +#if defined(WIN32) || defined(_WIN32) CRITICAL_SECTION criticalSection; +#elif HAVE_PTHREAD + pthread_mutex_t pmutex; +#endif }; } diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/DirectSoundRenderThread.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/DirectSoundRenderThread.h index f54d1d71..a7a4dc90 100644 --- a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/DirectSoundRenderThread.h +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/DirectSoundRenderThread.h @@ -3,21 +3,22 @@ #include "SongRenderer.h" #include "CriticalSection.h" +#include "IRenderThread.h" #include #include namespace WaveSabrePlayerLib { - class DirectSoundRenderThread + class DirectSoundRenderThread : public IRenderThread { public: - typedef void (*RenderCallback)(SongRenderer::Sample *buffer, int numSamples, void *data); - DirectSoundRenderThread(RenderCallback callback, void *callbackData, int sampleRate, int bufferSizeMs = 1000); - ~DirectSoundRenderThread(); + virtual ~DirectSoundRenderThread(); + + virtual int GetPlayPositionMs(); - int GetPlayPositionMs(); + virtual void DoForegroundWork(); private: static DWORD WINAPI threadProc(LPVOID lpParameter); diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/IPlayer.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/IPlayer.h index 589a9903..9cf16d3b 100644 --- a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/IPlayer.h +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/IPlayer.h @@ -9,6 +9,7 @@ namespace WaveSabrePlayerLib virtual ~IPlayer(); virtual void Play() = 0; + virtual void DoForegroundWork() = 0; virtual int GetTempo() const = 0; virtual int GetSampleRate() const = 0; diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/IRenderThread.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/IRenderThread.h new file mode 100644 index 00000000..96239bff --- /dev/null +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/IRenderThread.h @@ -0,0 +1,21 @@ +#ifndef __WAVESABREPLAYERLIB_IRENDERTHREAD_H__ +#define __WAVESABREPLAYERLIB_IRENDERTHREAD_H__ + +#include "SongRenderer.h" + +namespace WaveSabrePlayerLib +{ + class IRenderThread + { + public: + typedef void (*RenderCallback)(SongRenderer::Sample *buffer, int numSamples, void *data); + + virtual ~IRenderThread(); + + virtual int GetPlayPositionMs() = 0; + + virtual void DoForegroundWork() = 0; + }; +} + +#endif diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/PreRenderPlayer.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/PreRenderPlayer.h index efe30cb1..e670b2d0 100644 --- a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/PreRenderPlayer.h +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/PreRenderPlayer.h @@ -2,8 +2,8 @@ #define __WAVESABREPLAYERLIB_PRERENDERPLAYER_H__ #include "IPlayer.h" +#include "IRenderThread.h" #include "SongRenderer.h" -#include "DirectSoundRenderThread.h" namespace WaveSabrePlayerLib { @@ -16,6 +16,7 @@ namespace WaveSabrePlayerLib virtual ~PreRenderPlayer(); virtual void Play(); + virtual void DoForegroundWork(); virtual int GetTempo() const; virtual int GetSampleRate() const; @@ -35,7 +36,7 @@ namespace WaveSabrePlayerLib int playbackBufferSizeMs; int playbackBufferIndex; - DirectSoundRenderThread *renderThread; + IRenderThread *renderThread; }; } diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/RealtimePlayer.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/RealtimePlayer.h index 1efa0e80..ed10e61c 100644 --- a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/RealtimePlayer.h +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/RealtimePlayer.h @@ -2,8 +2,8 @@ #define __WAVESABREPLAYERLIB_REALTIMEPLAYER_H__ #include "IPlayer.h" +#include "IRenderThread.h" #include "SongRenderer.h" -#include "DirectSoundRenderThread.h" namespace WaveSabrePlayerLib { @@ -14,7 +14,8 @@ namespace WaveSabrePlayerLib virtual ~RealtimePlayer(); virtual void Play(); - + virtual void DoForegroundWork(); + virtual int GetTempo() const; virtual int GetSampleRate() const; virtual double GetLength() const; @@ -28,7 +29,7 @@ namespace WaveSabrePlayerLib int bufferSizeMs; SongRenderer *songRenderer; - DirectSoundRenderThread *renderThread; + IRenderThread *renderThread; }; } diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/SDL2RenderThread.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/SDL2RenderThread.h new file mode 100644 index 00000000..2ef69bf1 --- /dev/null +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/SDL2RenderThread.h @@ -0,0 +1,62 @@ +#ifndef __WAVESABREPLAYERLIB_SDL2RENDERTHREAD_H__ +#define __WAVESABREPLAYERLIB_SDL2RENDERTHREAD_H__ + +#include "SongRenderer.h" +#include "CriticalSection.h" +#include "IRenderThread.h" + +#include +#include + +#include + +#if HAVE_PTHREAD +#include +#include +#endif + +namespace WaveSabrePlayerLib +{ + class SDL2RenderThread : public IRenderThread + { + public: + SDL2RenderThread(RenderCallback callback, void *callbackData, + int sampleRate, int bufferSizeMs = 1000, bool pthread = false); + virtual ~SDL2RenderThread(); + + virtual int GetPlayPositionMs(); + + virtual void DoForegroundWork(); + + private: +#if HAVE_PTHREAD + static void* SDL2WriterProc(void* ud); +#endif + + void SDL2Callback2(uint8_t* buf, int len); + static void SDL2Callback(void* userdata, uint8_t* buf, int len); + + RenderCallback callback; + void *callbackData; + int sampleRate; + int bufferSizeMs; + int bufferSizeBytes; + int bytesWritten; + SongRenderer::Sample *sampleBuffer; +#if HAVE_PTHREAD + SongRenderer::Sample *backupBuffer; +#endif + const SongRenderer::Sample *bufferToWrite; + int bufferBytesLeft; + + SDL_AudioSpec spec; + SDL_AudioDeviceID dev; +#if HAVE_PTHREAD + sem_t needbuf_sem; + pthread_t writer; + bool writerStarted; +#endif + }; +} + +#endif diff --git a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/SongRenderer.h b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/SongRenderer.h index f9c6a122..3eab8787 100644 --- a/WaveSabrePlayerLib/include/WaveSabrePlayerLib/SongRenderer.h +++ b/WaveSabrePlayerLib/include/WaveSabrePlayerLib/SongRenderer.h @@ -89,7 +89,7 @@ namespace WaveSabrePlayerLib Track(SongRenderer *songRenderer, DeviceFactory factory); ~Track(); - + void Run(int numSamples); private: @@ -156,7 +156,11 @@ namespace WaveSabrePlayerLib int renderThreadIndex; } RenderThreadData; +#if defined(WIN32) || defined(_WIN32) static DWORD WINAPI renderThreadProc(LPVOID lpParameter); +#elif HAVE_PTHREAD + static void* renderThreadProc(void* lpParameter); +#endif bool renderThreadWork(int renderThreadIndex); @@ -172,7 +176,7 @@ namespace WaveSabrePlayerLib int bpm; int sampleRate; double length; - + int numDevices; WaveSabreCore::Device **devices; @@ -184,13 +188,23 @@ namespace WaveSabrePlayerLib TrackRenderState *trackRenderStates; int numRenderThreads; +#if defined(WIN32) || defined(_WIN32) HANDLE *additionalRenderThreads; +#elif HAVE_PTHREAD + pthread_t *additionalRenderThreads; +#endif bool renderThreadShutdown; int renderThreadNumFloatSamples; +#if defined(WIN32) || defined(_WIN32) unsigned int renderThreadsRunning; HANDLE *renderThreadStartEvents; HANDLE renderDoneEvent; +#elif HAVE_PTHREAD + std::atomic_int renderThreadsRunning; + sem_t *renderThreadStartEvents; + sem_t renderDoneEvent; +#endif }; } diff --git a/WaveSabrePlayerLib/src/AplayRenderThread.cpp b/WaveSabrePlayerLib/src/AplayRenderThread.cpp new file mode 100644 index 00000000..83dab180 --- /dev/null +++ b/WaveSabrePlayerLib/src/AplayRenderThread.cpp @@ -0,0 +1,251 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if HAVE_PTHREAD +#include +#endif + +inline static unsigned is_le(void) +{ + const union { unsigned u; unsigned char c[4]; } one = { 1 }; + return one.c[0]; +} + +namespace WaveSabrePlayerLib +{ + AplayRenderThread::AplayRenderThread(RenderCallback callback, + void *callbackData, int sampleRate, int bufferSizeMs, + bool pthread) + : callback(callback) + , callbackData(callbackData) + , sampleRate(sampleRate) + , bufferSizeMs(bufferSizeMs) + , bytesWritten(0) + , bufferBytesLeft(0) + , writeBytesMax(PIPE_BUF) +#if HAVE_PTHREAD + , writerStarted(false) +#endif + { +#if !defined(HAVE_PTHREAD) || !HAVE_PTHREAD + pthread = false; +#endif + + bufferSizeBytes = sampleRate * SongRenderer::BlockAlign * bufferSizeMs / 1000; + sampleBuffer = (SongRenderer::Sample*)malloc(bufferSizeBytes); + bufferToWrite = sampleBuffer; + + int fdpair[2]; + int rv = pipe(fdpair); + assert(rv == 0 && "Couldn't set up a pipe for aplay."); + + int readend = fdpair[0], writeend = fdpair[1]; + + this->aupipe = writeend; + + if (!pthread) { + // set the write end of the pipe to nonblocking, so we can limit the + // number of bytes to write to the pipe to the pipe size + int flags = fcntl(writeend, F_GETFL); + assert(flags != -1 && "Couldn't get pipe write-end fd flags."); + flags = fcntl(writeend, F_SETFL, flags|O_NONBLOCK); + assert(flags == 0 && "Couldn't set pipe write-end fd NONBLOCK flag."); + } + + pid_t child = fork(); // TODO: use vfork, needs shuffling stuff around + assert(child >= 0 && "Couldn't fork."); + + if (child == 0) { + // child process + close(writeend); // don't need this + AplayProc(readend, sampleRate); + } else { + // parent process + close(readend); // don't need this + this->aplay = child; + } + +#if HAVE_PTHREAD + if (pthread) { + rv = pthread_create(&writer, NULL, AplayWriterProc, this); + assert(rv == 0 && "Couldn't create background writer thread"); + + writerStarted = true; + } +#endif + } + + AplayRenderThread::~AplayRenderThread() + { + struct timespec ts; + ts.tv_sec = 1; + ts.tv_nsec = 0; + +#if HAVE_PTHREAD + if (writerStarted) { + pthread_cancel(writer); + pthread_join(writer, NULL); + writerStarted = false; + } +#endif + + if (this->aplay > 1) { + int stat; + kill(this->aplay, SIGTERM); + + // wait a second for aplay to ack the SIGTERM + sigset_t sset; + sigemptyset(&sset); + sigaddset(&sset, SIGCHLD); + if (sigtimedwait(&sset, NULL, &ts) < 0) { + // not acked? then you shall die. + kill(this->aplay, SIGKILL); + } + + waitpid(this->aplay, &stat, 0); + + this->aupipe = -1; + this->aplay = -1; + } + + free(sampleBuffer); + } + + void AplayRenderThread::DoForegroundWork() + { +#if HAVE_PTHREAD + if (!writerStarted) +#else + if (true) +#endif + GetBufferTick(false); + } + + int AplayRenderThread::GetPlayPositionMs() + { + return (int)(bytesWritten / SongRenderer::BlockAlign * 1000 / sampleRate); + } + + void AplayRenderThread::AplayProc(int readend, int rate) + { + // stdin becomes readend (properly sets up the pipe) + // doing this instead of using /proc/$pid/fd is more portable + dup2(readend, STDIN_FILENO); + //close(STDERR_FILENO); + + // format aplay args + char arg2[strlen("-fS16_LE")+1]; + char arg3[strlen("-r44100")+1]; + + snprintf(arg2, sizeof(arg2), "-fS%d_%sE", + (int)sizeof(SongRenderer::Sample)*CHAR_BIT, + (is_le() ? "L" : "B")); + snprintf(arg3, sizeof(arg3), "-r%d", rate); + + // now we're ready to start aplay! + //printf("executing /usr/bin/aplay -traw -c2 %s %s\n", arg2, arg3); + char *const args[] = {"/usr/bin/aplay", "-traw", "-c2", arg2, arg3, NULL}; + int rv = execve("/usr/bin/aplay", args, environ); + assert(rv >= 0 && "Failed to run aplay!"); + // unreachable + } + + void AplayRenderThread::GetBufferTick(bool block) + { + //printf("tick! buffer cap 0x%zx\n", bufferSizeBytes); + while (true) { + struct pollfd pfd; + pfd.fd = this->aupipe; + pfd.events = POLLOUT; + pfd.revents = 0; + + int res = poll(&pfd, 1, (block ? (-1) : 0)); + assert(res >= 0 && "poll(2) failed."); + + // can't write? let's try again later then + if (!res || (pfd.revents & POLLERR)) + break; + + assert(!(pfd.revents & POLLHUP) && "aplay suddenly stopped?"); + + if (bufferBytesLeft <= 0) { + callback(sampleBuffer, bufferSizeBytes/sizeof(SongRenderer::Sample), callbackData); + bufferToWrite = sampleBuffer; + bufferBytesLeft = bufferSizeBytes; + //printf("new buffer! 0x%zX bytes\n", bufferSizeBytes); + } + + size_t toWrite = bufferBytesLeft; + if (toWrite > writeBytesMax) + toWrite = writeBytesMax; + + bool haderr = false; + while (true) { + ssize_t ret = write(aupipe, bufferToWrite, toWrite); + + if (ret < 0) { + haderr = true; + int err = errno; + if (err == EAGAIN || err == EWOULDBLOCK) { + toWrite >>= 1; // AIMD + continue; + } else { + printf("Couldn't write to the aplay pipe: %s.\n", strerror(err)); + assert(0); + } + + break; + } + + if (toWrite > (size_t)ret) { + haderr = true; // inhibit AI + toWrite >>= 1; // MD + } + + bufferBytesLeft -= (size_t)ret; + //printf("written buffer %p size 0x%zx, wrote 0x%zx bytes, left: 0x%zx\n", + // bufferToWrite, toWrite, (size_t)ret, bufferBytesLeft); + bufferToWrite += ret / sizeof(SongRenderer::Sample); + bytesWritten += ret; + + break; + } + + writeBytesMax = toWrite; + if (!haderr) { + // AIMD + writeBytesMax += 0x100; + if (writeBytesMax > PIPE_BUF) + writeBytesMax = PIPE_BUF; + } + } + } + +#if HAVE_PTHREAD + void* AplayRenderThread::AplayWriterProc(void* ud) + { + auto self = (AplayRenderThread*)ud; + + while (self->aupipe >= 0) { + self->GetBufferTick(true); + //pthread_yield(); // poll and write are cancellation points anyway + } + + self->writerStarted = false; + return NULL; + } +#endif +} diff --git a/WaveSabrePlayerLib/src/CriticalSection.cpp b/WaveSabrePlayerLib/src/CriticalSection.cpp index 937a4ae8..f2224eb9 100644 --- a/WaveSabrePlayerLib/src/CriticalSection.cpp +++ b/WaveSabrePlayerLib/src/CriticalSection.cpp @@ -5,22 +5,42 @@ namespace WaveSabrePlayerLib CriticalSection::CriticalSectionGuard::CriticalSectionGuard(CriticalSection *criticalSection) : criticalSection(criticalSection) { +#if defined(WIN32) || defined(_WIN32) EnterCriticalSection(&criticalSection->criticalSection); +#elif HAVE_PTHREAD + pthread_mutex_lock(&criticalSection->pmutex); +#endif } CriticalSection::CriticalSectionGuard::~CriticalSectionGuard() { +#if defined(WIN32) || defined(_WIN32) LeaveCriticalSection(&criticalSection->criticalSection); +#elif HAVE_PTHREAD + pthread_mutex_unlock(&criticalSection->pmutex); +#endif } + CriticalSection::CriticalSection() +#if HAVE_PTHREAD + : pmutex(PTHREAD_MUTEX_INITIALIZER) +#endif { +#if defined(WIN32) || defined(_WIN32) InitializeCriticalSection(&criticalSection); +#elif HAVE_PTHREAD + pthread_mutex_init(&pmutex, NULL); +#endif } CriticalSection::~CriticalSection() { +#if defined(WIN32) || defined(_WIN32) DeleteCriticalSection(&criticalSection); +#elif HAVE_PTHREAD + pthread_mutex_destroy(&pmutex); +#endif } CriticalSection::CriticalSectionGuard CriticalSection::Enter() diff --git a/WaveSabrePlayerLib/src/DirectSoundRenderThread.cpp b/WaveSabrePlayerLib/src/DirectSoundRenderThread.cpp index fd50a115..8de2ada6 100644 --- a/WaveSabrePlayerLib/src/DirectSoundRenderThread.cpp +++ b/WaveSabrePlayerLib/src/DirectSoundRenderThread.cpp @@ -26,6 +26,11 @@ namespace WaveSabrePlayerLib WaitForSingleObject(thread, INFINITE); } + void DirectSoundRenderThread::DoForegroundWork() + { + // nop. + } + int DirectSoundRenderThread::GetPlayPositionMs() { if (!buffer) @@ -124,4 +129,4 @@ namespace WaveSabrePlayerLib return 0; } -} \ No newline at end of file +} diff --git a/WaveSabrePlayerLib/src/IRenderThread.cpp b/WaveSabrePlayerLib/src/IRenderThread.cpp new file mode 100644 index 00000000..7e27b939 --- /dev/null +++ b/WaveSabrePlayerLib/src/IRenderThread.cpp @@ -0,0 +1,8 @@ +#include + +namespace WaveSabrePlayerLib +{ + IRenderThread::~IRenderThread() + { + } +} diff --git a/WaveSabrePlayerLib/src/PreRenderPlayer.cpp b/WaveSabrePlayerLib/src/PreRenderPlayer.cpp index cd8f3815..6b5fbf19 100644 --- a/WaveSabrePlayerLib/src/PreRenderPlayer.cpp +++ b/WaveSabrePlayerLib/src/PreRenderPlayer.cpp @@ -1,5 +1,15 @@ #include +#if defined(WIN32) || defined(_WIN32) +#include +#elif HAVE_SDL2 +#include +#elif HAVE_APLAY +#include +#endif + +#include + namespace WaveSabrePlayerLib { PreRenderPlayer::PreRenderPlayer(const SongRenderer::Song *song, int numRenderThreads, ProgressCallback callback, void *data, int playbackBufferSizeMs) @@ -48,6 +58,12 @@ namespace WaveSabrePlayerLib delete [] renderBuffer; } + void PreRenderPlayer::DoForegroundWork() + { + if (renderThread) + renderThread->DoForegroundWork(); + } + void PreRenderPlayer::Play() { if (renderThread) @@ -55,7 +71,13 @@ namespace WaveSabrePlayerLib playbackBufferIndex = 0; +#if defined(WIN32) || defined(_WIN32) renderThread = new DirectSoundRenderThread(renderCallback, this, sampleRate, playbackBufferSizeMs); +#elif HAVE_SDL2 + renderThread = new SDL2RenderThread(renderCallback, this, sampleRate, playbackBufferSizeMs, true); +#elif HAVE_APLAY + renderThread = new AplayRenderThread(renderCallback, this, sampleRate, playbackBufferSizeMs, true); +#endif } int PreRenderPlayer::GetTempo() const @@ -78,7 +100,12 @@ namespace WaveSabrePlayerLib if (!renderThread) return 0.0; - return max(((double)renderThread->GetPlayPositionMs() - (double)playbackBufferSizeMs) / 1000.0, 0.0); + double v = ((double)renderThread->GetPlayPositionMs() - (double)playbackBufferSizeMs) / 1000.0; +#if defined(WIN32) || defined(_WIN32) + return max(v, 0.0); +#else + return (v > 0.0) ? v : 0.0; +#endif } void PreRenderPlayer::renderCallback(SongRenderer::Sample *buffer, int numSamples, void *data) @@ -92,7 +119,13 @@ namespace WaveSabrePlayerLib return; } - int samplesToTake = min(numSamples, samplesLeft); + int samplesToTake; +#if defined(WIN32) || defined(_WIN32) + samplesToTake = min(numSamples, samplesLeft); +#else + // sigh. + samplesToTake = (numSamples > samplesLeft) ? samplesLeft : numSamples; +#endif if (samplesToTake) { memcpy(buffer, player->renderBuffer + player->playbackBufferIndex, samplesToTake * sizeof(SongRenderer::Sample)); diff --git a/WaveSabrePlayerLib/src/RealtimePlayer.cpp b/WaveSabrePlayerLib/src/RealtimePlayer.cpp index 7976b03c..92d6d577 100644 --- a/WaveSabrePlayerLib/src/RealtimePlayer.cpp +++ b/WaveSabrePlayerLib/src/RealtimePlayer.cpp @@ -1,5 +1,13 @@ #include +#if defined(WIN32) || defined(_WIN32) +#include +#elif HAVE_SDL2 +#include +#elif HAVE_APLAY +#include +#endif + namespace WaveSabrePlayerLib { RealtimePlayer::RealtimePlayer(const SongRenderer::Song *song, int numRenderThreads, int bufferSizeMs) @@ -19,6 +27,12 @@ namespace WaveSabrePlayerLib delete songRenderer; } + void RealtimePlayer::DoForegroundWork() + { + if (renderThread) + renderThread->DoForegroundWork(); + } + void RealtimePlayer::Play() { if (renderThread) @@ -27,7 +41,13 @@ namespace WaveSabrePlayerLib delete songRenderer; songRenderer = new SongRenderer(song, numRenderThreads); +#if defined(WIN32) || defined(_WIN32) renderThread = new DirectSoundRenderThread(renderCallback, this, songRenderer->GetSampleRate(), bufferSizeMs); +#elif HAVE_SDL2 + renderThread = new SDL2RenderThread(renderCallback, this, songRenderer->GetSampleRate(), bufferSizeMs, true); +#elif HAVE_APLAY + renderThread = new AplayRenderThread(renderCallback, this, songRenderer->GetSampleRate(), bufferSizeMs, true); +#endif } int RealtimePlayer::GetTempo() const @@ -50,14 +70,23 @@ namespace WaveSabrePlayerLib if (!renderThread) return 0.0; - return max(((double)renderThread->GetPlayPositionMs() - (double)bufferSizeMs) / 1000.0, 0.0); + double v = ((double)renderThread->GetPlayPositionMs() - (double)bufferSizeMs) / 1000.0; +#if defined(WIN32) || defined(_WIN32) + return max(v, 0.0); +#else + return (v > 0.0) ? v : 0.0; +#endif } void RealtimePlayer::renderCallback(SongRenderer::Sample *buffer, int numSamples, void *data) { auto player = (RealtimePlayer *)data; const int stepSize = 100 * SongRenderer::NumChannels; - for (int i = 0; i < numSamples; i += stepSize) - player->songRenderer->RenderSamples(buffer + i, min(numSamples - i, stepSize)); + for (int i = 0; i < numSamples; i += stepSize) { + int v = numSamples - i; + if (v > stepSize) v = stepSize; + + player->songRenderer->RenderSamples(buffer + i, v); + } } } diff --git a/WaveSabrePlayerLib/src/SDL2RenderThread.cpp b/WaveSabrePlayerLib/src/SDL2RenderThread.cpp new file mode 100644 index 00000000..750cb29a --- /dev/null +++ b/WaveSabrePlayerLib/src/SDL2RenderThread.cpp @@ -0,0 +1,156 @@ +#include + +#include +#include +#include +#include + +namespace WaveSabrePlayerLib +{ + SDL2RenderThread::SDL2RenderThread(RenderCallback callback, + void *callbackData, int sampleRate, int bufferSizeMs, bool pthread) + : callback(callback) + , callbackData(callbackData) + , sampleRate(sampleRate) + , bufferSizeMs(bufferSizeMs) + , bytesWritten(0) + , bufferBytesLeft(0) +#if HAVE_PTHREAD + , writerStarted(false) +#endif + { +#if !defined(HAVE_PTHREAD) || !HAVE_PTHREAD + pthread = false; +#endif + + bufferSizeBytes = sampleRate * SongRenderer::BlockAlign * bufferSizeMs / 1000; + sampleBuffer = (SongRenderer::Sample*)malloc(bufferSizeBytes); +#if HAVE_PTHREAD + backupBuffer = (SongRenderer::Sample*)malloc(bufferSizeBytes); +#endif + + int rv = SDL_Init(SDL_INIT_AUDIO); + assert(rv >= 0 && "Can't init SDL2 audio"); + + spec.freq = sampleRate; + spec.format = AUDIO_S16SYS; + spec.channels = 2; + spec.samples = bufferSizeBytes / (sizeof(SongRenderer::Sample) * spec.channels); + spec.callback = SDL2Callback; + spec.userdata = this; + + dev = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0); +#ifndef NDEBUG + if (dev <= 0) { + printf("SDL2 error: %s\n", SDL_GetError()); + } +#endif + assert(dev > 0 && "Can't open SDL2 audio"); + + // pre-buffer stuff a bit, SDL2 tends to be finnicky + callback(sampleBuffer, bufferSizeBytes/sizeof(SongRenderer::Sample), callbackData); + bufferBytesLeft = bufferSizeBytes; + bufferToWrite = sampleBuffer; + +#if HAVE_PTHREAD + if (pthread) { + sem_init(&needbuf_sem, 0, 0); + + rv = pthread_create(&writer, NULL, SDL2WriterProc, this); + assert(rv == 0 && "Couldn't create background writer thread"); + + sem_post(&needbuf_sem); // start filling in the backup buffer.. + + writerStarted = true; + } +#endif + + SDL_PauseAudioDevice(dev, 0); + } + + SDL2RenderThread::~SDL2RenderThread() + { + SDL_CloseAudioDevice(dev); + +#if HAVE_PTHREAD + if (writerStarted) { + pthread_cancel(writer); + pthread_join(writer, NULL); + sem_destroy(&needbuf_sem); + writerStarted = false; + } +#endif + + free(sampleBuffer); +#if HAVE_PTHREAD + free(backupBuffer); +#endif + } + + void SDL2RenderThread::DoForegroundWork() + { + // nop. + } + + int SDL2RenderThread::GetPlayPositionMs() + { + return (int)(bytesWritten / SongRenderer::BlockAlign * 1000 / sampleRate); + } + +#if HAVE_PTHREAD + void* SDL2RenderThread::SDL2WriterProc(void* ud) + { + auto self = (SDL2RenderThread*)ud; + + while (true) { + sem_wait(&self->needbuf_sem); + + self->callback(self->backupBuffer, + self->bufferSizeBytes/sizeof(SongRenderer::Sample), + self->callbackData); + } + + self->writerStarted = false; + return NULL; + } +#endif + + void SDL2RenderThread::SDL2Callback2(uint8_t* buf, int len) + { + if (bufferBytesLeft <= 0) { +#if HAVE_PTHREAD + if (writerStarted) { + // TODO: pingpong backupBuffer<->sampleBuffer? + // (needs an atomic exchange) + SDL_memcpy(sampleBuffer, backupBuffer, bufferSizeBytes); + sem_post(&needbuf_sem); + } else +#else + { + callback(sampleBuffer, bufferSizeBytes/sizeof(SongRenderer::Sample), callbackData); + } +#endif + + bufferToWrite = sampleBuffer; + bufferBytesLeft = bufferSizeBytes; + } + + if (len == 0) + return; + + if (len > bufferBytesLeft) + len = bufferBytesLeft; + + SDL_memcpy(buf, bufferToWrite, len); + + bufferToWrite += len / sizeof(SongRenderer::Sample); + bufferBytesLeft -= len; + bytesWritten += len; + } + + void SDL2RenderThread::SDL2Callback(void* userdata, uint8_t* buf, int len) + { + auto self = (SDL2RenderThread*)userdata; + self->SDL2Callback2(buf, len); + } +} diff --git a/WaveSabrePlayerLib/src/SongRenderer.Track.cpp b/WaveSabrePlayerLib/src/SongRenderer.Track.cpp index 61f1f361..d51c8d2f 100644 --- a/WaveSabrePlayerLib/src/SongRenderer.Track.cpp +++ b/WaveSabrePlayerLib/src/SongRenderer.Track.cpp @@ -1,5 +1,7 @@ #include +#include + using namespace WaveSabreCore; namespace WaveSabrePlayerLib @@ -58,7 +60,7 @@ namespace WaveSabrePlayerLib if (NumReceives) delete [] Receives; - + if (numDevices) { delete[] devicesIndicies; diff --git a/WaveSabrePlayerLib/src/SongRenderer.cpp b/WaveSabrePlayerLib/src/SongRenderer.cpp index 4923ac72..d7a2a6ac 100644 --- a/WaveSabrePlayerLib/src/SongRenderer.cpp +++ b/WaveSabrePlayerLib/src/SongRenderer.cpp @@ -1,5 +1,14 @@ #include +#include +#include + +#include + +#if HAVE_PTHREAD +#include +#endif + using namespace WaveSabreCore; namespace WaveSabrePlayerLib @@ -37,7 +46,7 @@ namespace WaveSabrePlayerLib for (int m = 0; m < numEvents; m++) { midiLanes[i]->events[m].TimeStamp = readInt(); - byte note = readByte(); + uint8_t note = readByte(); if ((note & 0x80) == 0x00) { midiLanes[i]->events[m].Type = (EventType)0; @@ -65,21 +74,40 @@ namespace WaveSabrePlayerLib this->numRenderThreads = numRenderThreads; renderThreadShutdown = false; +#if defined(WIN32) || defined(_WIN32) renderThreadStartEvents = new HANDLE[numRenderThreads]; for (int i = 0; i < numRenderThreads; i++) renderThreadStartEvents[i] = CreateEvent(NULL, FALSE, FALSE, NULL); renderDoneEvent = CreateEvent(NULL, FALSE, FALSE, NULL); +#elif HAVE_PTHREAD + renderThreadStartEvents = new sem_t[numRenderThreads]; + for (int i = 0; i < numRenderThreads; i++) + sem_init(&renderThreadStartEvents[i], 0, 0); + sem_init(&renderDoneEvent, 0, 0); +#endif if (numRenderThreads > 1) { +#if defined(WIN32) || defined(_WIN32) additionalRenderThreads = new HANDLE[numRenderThreads - 1]; +#elif HAVE_PTHREAD + additionalRenderThreads = new pthread_t[numRenderThreads - 1]; +#endif + for (int i = 0; i < numRenderThreads - 1; i++) { auto renderThreadData = new RenderThreadData(); renderThreadData->songRenderer = this; renderThreadData->renderThreadIndex = i + 1; + +#if defined(WIN32) || defined(_WIN32) additionalRenderThreads[i] = CreateThread(0, 0, renderThreadProc, (LPVOID)renderThreadData, 0, 0); SetThreadPriority(additionalRenderThreads[i], THREAD_PRIORITY_HIGHEST); +#elif HAVE_PTHREAD + pthread_create(&additionalRenderThreads[i], NULL, renderThreadProc, + renderThreadData); + pthread_setschedprio(additionalRenderThreads[i], -15); +#endif } } } @@ -91,12 +119,34 @@ namespace WaveSabrePlayerLib if (numRenderThreads > 1) { +#if defined(WIN32) || defined(_WIN32) for (int i = 0; i < numRenderThreads; i++) SetEvent(renderThreadStartEvents[i]); WaitForMultipleObjects(numRenderThreads - 1, additionalRenderThreads, TRUE, INFINITE); for (int i = 0; i < numRenderThreads - 1; i++) CloseHandle(additionalRenderThreads[i]); + + delete [] additionalRenderThreads; +#elif HAVE_PTHREAD + for (int i = 0; i < numRenderThreads; i++) + sem_post(&renderThreadStartEvents[i]); + + // properly stop threads + { + struct timespec to; + to.tv_sec = 0; + // .1sec milli micro + to.tv_nsec = 100 * 1000 * 1000; + for (int i = 0; i < numRenderThreads - 1; i++) { + if (pthread_timedjoin_np(additionalRenderThreads[i], NULL, &to)) { + pthread_cancel(additionalRenderThreads[i]); + pthread_join(additionalRenderThreads[i], NULL); + } + } + } + delete [] additionalRenderThreads; +#endif } for (int i = 0; i < numDevices; i++) delete devices[i]; @@ -109,11 +159,19 @@ namespace WaveSabrePlayerLib delete [] tracks; delete [] trackRenderStates; +#if defined(WIN32) || defined(_WIN32) for (int i = 0; i < numRenderThreads; i++) CloseHandle(renderThreadStartEvents[i]); - CloseHandle(renderDoneEvent); + delete [] renderThreadStartEvents; + CloseHandle(renderDoneEvent); +#elif HAVE_PTHREAD + for (int i = 0; i < numRenderThreads; i++) + sem_destroy(&renderThreadStartEvents[i]); delete [] renderThreadStartEvents; + + sem_destroy(&renderDoneEvent); +#endif } void SongRenderer::RenderSamples(Sample *buffer, int numSamples) @@ -126,14 +184,24 @@ namespace WaveSabrePlayerLib renderThreadNumFloatSamples = numSamples / 2; // Dispatch work +#if defined(WIN32) || defined(_WIN32) renderThreadsRunning = numRenderThreads; for (int i = 0; i < numRenderThreads; i++) SetEvent(renderThreadStartEvents[i]); +#elif HAVE_PTHREAD + renderThreadsRunning = numRenderThreads; + for (int i = 0; i < numRenderThreads; i++) + sem_post(&renderThreadStartEvents[i]); +#endif renderThreadWork(0); +#if defined(WIN32) || defined(_WIN32) // Wait for render threads to complete their work WaitForSingleObject(renderDoneEvent, INFINITE); +#elif HAVE_PTHREAD + sem_wait(&renderDoneEvent); +#endif // Copy final output float **masterTrackBuffers = tracks[numTracks - 1]->Buffers; @@ -146,7 +214,12 @@ namespace WaveSabrePlayerLib } } +#if defined(WIN32) || defined(_WIN32) || HAVE_PTHREAD + #if defined(WIN32) || defined(_WIN32) DWORD WINAPI SongRenderer::renderThreadProc(LPVOID lpParameter) + #elif HAVE_PTHREAD + void* SongRenderer::renderThreadProc(void* lpParameter) + #endif { auto renderThreadData = (RenderThreadData *)lpParameter; @@ -160,10 +233,15 @@ namespace WaveSabrePlayerLib return 0; } +#endif bool SongRenderer::renderThreadWork(int renderThreadIndex) { +#if defined(WIN32) || defined(_WIN32) WaitForSingleObject(renderThreadStartEvents[renderThreadIndex], INFINITE); +#elif HAVE_PTHREAD + sem_wait(&renderThreadStartEvents[renderThreadIndex]); +#endif if (renderThreadShutdown) return false; @@ -195,8 +273,26 @@ namespace WaveSabrePlayerLib // We have a free track that we can work on, yay! // Let's try to mark it so that no other thread takes it - if ((TrackRenderState)InterlockedCompareExchange((unsigned int *)&trackRenderStates[i], (unsigned int)TrackRenderState::Rendering, (unsigned int)TrackRenderState::Idle) == TrackRenderState::Idle) +#if defined(WIN32) || defined(_WIN32) + if ((TrackRenderState)InterlockedCompareExchange( + (unsigned int *)&trackRenderStates[i], + (unsigned int)TrackRenderState::Rendering, + (unsigned int)TrackRenderState::Idle) + == TrackRenderState::Idle) +#elif HAVE_PTHREAD + int xv = (int)TrackRenderState::Idle; + if (std::atomic_compare_exchange_strong( + (std::atomic_int *)&trackRenderStates[i], + &xv, (int)TrackRenderState::Rendering) + ) +#else + if (trackRenderStates[i] == TrackRenderState::Idle) +#endif { +#if !(defined(WIN32) || defined(_WIN32)) && !(defined(HAVE_PTHREAD) && HAVE_PTHREAD) + trackRenderStates[i] = TrackRenderState::Rendering; +#endif + // We marked it successfully, so now we'll do the work tracks[i]->Run(renderThreadNumFloatSamples); // And mark it as finished :) @@ -204,10 +300,21 @@ namespace WaveSabrePlayerLib break; } } + + // might take a wihle before more work becomes available +#if HAVE_PTHREAD + pthread_yield(); +#endif } +#if defined(WIN32) || defined(_WIN32) if (!InterlockedDecrement(&renderThreadsRunning)) SetEvent(renderDoneEvent); +#elif HAVE_PTHREAD + // returns the value *before* the call + if (std::atomic_fetch_sub(&renderThreadsRunning, 1) == 1) + sem_post(&renderDoneEvent); +#endif return true; } diff --git a/WaveSabrePlayerLib/src/WavWriter.cpp b/WaveSabrePlayerLib/src/WavWriter.cpp index fdca1417..def6f5ea 100644 --- a/WaveSabrePlayerLib/src/WavWriter.cpp +++ b/WaveSabrePlayerLib/src/WavWriter.cpp @@ -1,6 +1,12 @@ #include +#if defined(WIN32) || defined(_WIN32) #include +#endif + +#ifndef WAVE_FORMAT_PCM +#define WAVE_FORMAT_PCM (0x0001) +#endif namespace WaveSabrePlayerLib { @@ -25,7 +31,7 @@ namespace WaveSabrePlayerLib auto file = fopen(fileName, "wb"); int dataSubChunkSize = numSamples * bitsPerSample / 8; - + // RIFF header fputs("RIFF", file); writeInt(36 + dataSubChunkSize, file); diff --git a/WaveSabreStandAlonePlayer/CMakeLists.txt b/WaveSabreStandAlonePlayer/CMakeLists.txt index 5b301ec4..a59e4df7 100644 --- a/WaveSabreStandAlonePlayer/CMakeLists.txt +++ b/WaveSabreStandAlonePlayer/CMakeLists.txt @@ -1,10 +1,10 @@ add_executable(WaveSabreStandAlonePlayer main.cpp) -target_link_libraries(WaveSabreStandAlonePlayer WaveSabrePlayerLib) +target_link_libraries(WaveSabreStandAlonePlayer PUBLIC WaveSabrePlayerLib) if(MSVC) target_compile_definitions(WaveSabreStandAlonePlayer PRIVATE _CRT_SECURE_NO_WARNINGS) - target_link_libraries(WaveSabreStandAlonePlayer $<$:msvcrt>) + target_link_libraries(WaveSabreStandAlonePlayer PUBLIC $<$:msvcrt>) set_property(TARGET WaveSabreStandAlonePlayer APPEND_STRING PROPERTY LINK_FLAGS_MINSIZEREL " /NODEFAULTLIB /SAFESEH:NO /MANIFEST:NO /LTCG /OPT:REF /OPT:ICF /DYNAMICBASE:NO") @@ -12,4 +12,32 @@ if(MSVC) target_compile_definitions(WaveSabreStandAlonePlayer PRIVATE $<$:_NO_CRT_STDIO_INLINE>) endif() +else() + # assuming GCC or clang for now + + if(ENABLE_PTHREADS) + set_property(TARGET WaveSabrePlayerLib PROPERTY CXX_STANDARD 11) # need a recent one for stdatomic.h + + message(STATUS "Trying to look for pthreads...") + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads) + if(CMAKE_USE_PTHREADS_INIT) + message(STATUS "Found pthreads, enabling pthreads for background rendering threads.") + #target_link_libraries(WaveSabrePlayerLib PRIVATE Threads::Threads) # we don't actually need it here + target_compile_definitions(WaveSabrePlayerLib PRIVATE HAVE_PTHREAD=1) + else() + message(WARNING "pthreads not found, using synchronous fallback.") + endif() + else() + message(STATUS "pthreads disabled explicitely.") + endif() + + if(CMAKE_BUILD_TYPE EQUAL Debug) + target_compile_options(WaveSabreStandAlonePlayer PUBLIC -g -Og) + else() + #set_property(TARGET WaveSabreStandAlonePlayer PROPERTY INTERPROCEDURAL_OPTIMIZATION ON) + target_compile_options(WaveSabreStandAlonePlayer + PUBLIC -O2 -fno-exceptions -fno-rtti -fno-stack-protector -fno-stack-check -fno-unwind-tables -fno-asynchronous-unwind-tables -fomit-frame-pointer -fno-threadsafe-statics + PRIVATE -ffast-math -march=nocona -ffunction-sections -fdata-sections -Wl,--gc-sections) + endif() endif() diff --git a/WaveSabreStandAlonePlayer/main.cpp b/WaveSabreStandAlonePlayer/main.cpp index e5676a22..6ccb62d4 100644 --- a/WaveSabreStandAlonePlayer/main.cpp +++ b/WaveSabreStandAlonePlayer/main.cpp @@ -1,9 +1,17 @@ #include +#include #include using namespace WaveSabrePlayerLib; +#include +#include #include +#if !defined(WIN32) && !defined(_WIN32) +#include +#include +#endif + WaveSabreCore::Device *SongFactory(SongRenderer::DeviceId id) { switch (id) @@ -22,6 +30,7 @@ WaveSabreCore::Device *SongFactory(SongRenderer::DeviceId id) case SongRenderer::DeviceId::Adultery: return new WaveSabreCore::Adultery(); case SongRenderer::DeviceId::Specimen: return new WaveSabreCore::Specimen(); } + printf("ack, unknown device %d!\n", id); return nullptr; } @@ -34,20 +43,137 @@ void progressCallback(double progress, void *data) printf("]"); } -int main(int argc, char **argv) +struct player_args +{ + const char* infile; + const char* outfile; + int nthreads; + bool prerender; + bool wavwriter; +}; + +static void print_usage(const char* prgm) { - bool writeWav = argc >= 3 && !strcmp(argv[2], "-w"); - bool preRender = argc == 3 && !strcmp(argv[2], "-p"); + printf("%s: WaveSabre standalone song player/renderer\n" + "Usage:\n" +#if defined(WIN32) || defined(_WIN32) + "\t%s [/p|/prerender] [/w|/wav [out.wav]] [/t|/threads 3] \n" +#else + "\t%s [-p|--prerender] [-w|--wav [out.wav]] [-t|--threads 3] \n" +#endif + "\n" + "Arguments:\n" + "\tprerender prerender the song instead of rendering while playing.\n" + "\twav instead of playing back the song, write a WAV file. By default,\n" + "\t the output file is called `out.wav', but another may be supplied\n" + "\t immediately after this argument.\n" + "\tthreads the amount of threads to use in parallel for rendering. By\n" + "\t default, this number is 3.\n" + "\tinput.bin the input song file, exported by the WaveSabre exporter.\n" + , prgm, prgm); + exit(0); +} +static void parse_args(struct player_args* args, int argc, char** argv) +{ + if (argc < 2) { + print_usage(argv[0]); + // unreachable + } + + args->infile = NULL; + args->outfile = NULL; + args->nthreads = 3; + args->prerender = false; + args->wavwriter = false; + + char argpfix; +#if defined(WIN32) || defined(_WIN32) + argpfix = '/'; +#else + argpfix = '-'; +#endif + + for (int i = 1; i < argc; ++i) { + if (argv[i][0] == argpfix) { + const char* as = &argv[i][1]; + + if (!strcmp(as, "h") || !strcmp(as, "-help") || !strcmp(as, "help")) { + print_usage(argv[0]); + // unreachable + } + // TODO: option for printing version info + if (!strcmp(as, "p") || !strcmp(as, "-prerender") || !strcmp(as, "prerender")) { + args->prerender = true; + continue; + } + if (!strcmp(as, "w") || !strcmp(as, "-wav") || !strcmp(as, "wav")) { + args->wavwriter = true; + + if (i+1 < argc && argv[i+1][0] != argpfix) { + args->outfile = argv[i+1]; + ++i; + } + continue; + } + if (!strcmp(as, "t") || !strcmp(as, "-threads") || !strcmp(as, "threads")) { + if (i+1 == argc) { + printf("Expecting thread amount\n"); + exit(2); + } + + ++i; + int res = sscanf(argv[i], "%d", &args->nthreads); + if (res != 1 || args->nthreads < 0) { + printf("Can't parse thread amount '%s'\n", argv[i]); + exit(2); + } + + continue; + } + + printf("Unrecognised option '%s', ignoring...\n", argv[i]); + } else { + if (args->infile != NULL) { + printf("Unexpected argument '%s'\n", argv[i]); + exit(2); + } + + args->infile = argv[i]; + } + } + + if (args->infile == NULL) { + if (args->outfile != NULL) { + // oops, was probably meant as input file path + args->infile = args->outfile; + args->outfile = NULL; + } + } + + if (args->outfile == NULL && args->wavwriter) { + args->outfile = "out.wav"; + } +} - const int numRenderThreads = 3; +/*#if HAVE_SDL2 +int SDLmain(int argc, char **argv) +#else*/ +int main(int argc, char **argv) +//#endif +{ + struct player_args args; + parse_args(&args, argc, argv); FILE * pFile; long lSize; unsigned char * buffer; size_t result; - pFile = fopen(argv[1], "rb"); - if (pFile == NULL) { printf("File error\n"); exit(1); } + pFile = fopen(args.infile, "rb"); + if (pFile == NULL) { + printf("Can't open input file '%s'\n", args.infile); + exit(1); + } // obtain file size: fseek(pFile, 0, SEEK_END); @@ -59,7 +185,10 @@ int main(int argc, char **argv) // copy the file into the buffer: result = fread(buffer, 1, lSize, pFile); - if (result != lSize) { printf("Reading error\n"); exit(3); } + if (result != lSize) { + printf("Can't read from input file '%s'\n", args.infile); + exit(3); + } // terminate fclose(pFile); @@ -68,39 +197,54 @@ int main(int argc, char **argv) song.blob = buffer; song.factory = SongFactory; - if (writeWav) + if (args.wavwriter) { - WavWriter wavWriter(&song, numRenderThreads); + FILE* outf = fopen(args.outfile, "wb"); + if (!outf) { + printf("Can't open output file '%s'\n", args.outfile); + exit(4); + } + fclose(outf); // close again because WavWriter wants a file path + + WavWriter wavWriter(&song, args.nthreads); printf("WAV writer activated.\n"); - auto fileName = argc >= 4 ? argv[3] : "out.wav"; printf("Rendering...\n"); - wavWriter.Write(fileName, progressCallback, nullptr); + wavWriter.Write(args.outfile, progressCallback, nullptr); - printf("\n\nWAV file written to \"%s\". Enjoy.\n", fileName); + printf("\n\nWAV file written to \"%s\". Enjoy.\n", args.outfile); } else { IPlayer *player; - if (preRender) + if (args.prerender) { printf("Prerender activated.\n"); printf("Rendering...\n"); - player = new PreRenderPlayer(&song, numRenderThreads, progressCallback, nullptr); + player = new PreRenderPlayer(&song, args.nthreads, progressCallback, nullptr); printf("\n\n"); } else { - player = new RealtimePlayer(&song, numRenderThreads); + player = new RealtimePlayer(&song, args.nthreads); } +#if defined(WIN32) || defined(_WIN32) printf("Realtime player activated. Press ESC to quit.\n"); +#else + printf("Realtime player activated. Press ^C to quit.\n"); +#endif + player->Play(); +#if defined(WIN32) || defined(_WIN32) while (!GetAsyncKeyState(VK_ESCAPE)) +#else + while (true) +#endif { auto songPos = player->GetSongPos(); if (songPos >= player->GetLength()) break; @@ -109,7 +253,20 @@ int main(int argc, char **argv) int hundredths = (int)(songPos * 100.0) % 100; printf("\r %.1i:%.2i.%.2i", minutes, seconds, hundredths); + player->DoForegroundWork(); +#if defined(WIN32) || defined(_WIN32) Sleep(10); +#else + fflush(stdout); + #if HAVE_PTHREAD + sleep( 1); + #else + struct timespec to; + to.tv_sec = 0; + to.tv_nsec = 50*1000*1000; // 50 ms + nanosleep(&to, NULL); + #endif +#endif } printf("\n"); @@ -118,4 +275,4 @@ int main(int argc, char **argv) free(buffer); return 0; -} \ No newline at end of file +}