diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml new file mode 100644 index 0000000..daed23d --- /dev/null +++ b/.github/workflows/build-examples.yml @@ -0,0 +1,36 @@ +name: Build Tests + +on: + pull_request: + branches: + - main + +jobs: + build_feature: + name: Build Examples + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + include: + - os: ubuntu-latest + name: linux + + - os: macos-latest + name: mac + + - os: windows-latest + name: win + + steps: + + - name: Checkout code + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Build Example Harness + run: | + cmake -S . -B ./build -DCMAKE_BUILD_TYPE=Debug -DSST_EFFECTS_BUILD_EXAMPLES=TRUE -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" + cmake --build ./build --config Debug --target voice-effect-example + diff --git a/.github/workflows/code-checks.yml b/.github/workflows/code-checks.yml index ced2850..d88f2a6 100644 --- a/.github/workflows/code-checks.yml +++ b/.github/workflows/code-checks.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - path: [ 'tests', 'include' ] + path: [ 'tests', 'include', 'examples'' ] steps: - name: Checkout code uses: actions/checkout@v3 diff --git a/CMakeLists.txt b/CMakeLists.txt index 47af390..6849f13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.10) project(sst-effects VERSION 0.5 LANGUAGES C CXX) +option(SST_EFFECTS_BUILD_EXAMPLES "Build the example drivers (which will also acivate tests)" OFF) +option(SST_EFFECTS_BUILD_TESTS "Build the test harness" OFF) + set(CMAKE_CXX_STANDARD 17) add_library(${PROJECT_NAME} INTERFACE) @@ -12,6 +15,7 @@ if (${SST_EFFECTS_BUILD_EXAMPLES}) endif() if (${SST_EFFECTS_BUILD_TESTS}) + message(STATUS "Building tests") include(cmake/CPM.cmake) if (NOT TARGET sst-basic-blocks) @@ -81,6 +85,7 @@ if (${SST_EFFECTS_BUILD_TESTS}) if(${SST_EFFECTS_BUILD_EXAMPLES}) + message(STATUS "Building Examples / CLI Driver") if (NOT TARGET dr_libs) CPMAddPackage(NAME dr_libs GITHUB_REPOSITORY mackron/dr_libs @@ -88,13 +93,18 @@ if (${SST_EFFECTS_BUILD_TESTS}) ) add_library(dr_libs INTERFACE) target_include_directories(dr_libs INTERFACE ${dr_libs_SOURCE_DIR}) + + CPMAddPackage(NAME CL11 + GITHUB_REPOSITORY CLIUtils/CLI11 + GIT_TAG main) + endif () - add_executable(voice-effet-example + add_executable(voice-effect-example examples/voice-effect-example.cpp ) - target_link_libraries(voice-effet-example PUBLIC dr_libs simde sst-basic-blocks sst-filters sst-waveshapers fmt ${PROJECT_NAME}) - target_compile_definitions(voice-effet-example PUBLIC _USE_MATH_DEFINES=1) + target_link_libraries(voice-effect-example PUBLIC dr_libs CLI11::CLI11 simde sst-basic-blocks sst-filters sst-waveshapers fmt ${PROJECT_NAME}) + target_compile_definitions(voice-effect-example PUBLIC _USE_MATH_DEFINES=1) endif() endif () diff --git a/examples/voice-effect-example.cpp b/examples/voice-effect-example.cpp index 88bdba0..6b7ad21 100644 --- a/examples/voice-effect-example.cpp +++ b/examples/voice-effect-example.cpp @@ -1,4 +1,31 @@ +/* + * sst-effects - an open source library of audio effects + * built by Surge Synth Team. + * + * Copyright 2018-2023, various authors, as described in the GitHub + * transaction log. + * + * sst-effects is released under the GNU General Public Licence v3 + * or later (GPL-3.0-or-later). The license is found in the "LICENSE" + * file in the root of this repository, or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * The majority of these effects at initiation were factored from + * Surge XT, and so git history prior to April 2023 is found in the + * surge repo, https://github.com/surge-synthesizer/surge + * + * All source in sst-effects available at + * https://github.com/surge-synthesizer/sst-effects + */ + #include +#include +#include +#include +#include +#include +#include +#include #define DR_WAV_IMPLEMENTATION #include "dr_wav.h" @@ -9,153 +36,240 @@ struct DbToLinearProvider { - static constexpr size_t nPoints{512}; - float table_dB[nPoints]; - void init() { for (auto i = 0U; i < nPoints; i++) table_dB[i] = powf(10.f, 0.05f * ((float)i - 384.f)); } - float dbToLinear(float db) const { db += 384;int e = (int)db;float a = db - (float)e; - return (1.f - a) * table_dB[e & (nPoints - 1)] + a * table_dB[(e + 1) & (nPoints - 1)]; - } + static constexpr size_t nPoints{512}; + float table_dB[nPoints]; + void init() + { + for (auto i = 0U; i < nPoints; i++) + table_dB[i] = powf(10.f, 0.05f * ((float)i - 384.f)); + } + float dbToLinear(float db) const + { + db += 384; + int e = (int)db; + float a = db - (float)e; + return (1.f - a) * table_dB[e & (nPoints - 1)] + a * table_dB[(e + 1) & (nPoints - 1)]; + } }; +struct SSTFX +{ + std::array fb{}; + std::array ib{}; + float sampleRate; + DbToLinearProvider dbtlp; + + struct FxConfig + { + using BaseClass = SSTFX; + static constexpr int blockSize{16}; + static void setFloatParam(BaseClass *b, int i, float f) { b->fb[i] = f; } + static float getFloatParam(const BaseClass *b, int i) { return b->fb[i]; } + + static void setIntParam(BaseClass *b, int i, int v) { b->ib[i] = v; } + static int getIntParam(const BaseClass *b, int i) { return b->ib[i]; } + + static float dbToLinear(const BaseClass *b, float f) { return b->dbtlp.dbToLinear(f); } + static float equalNoteToPitch(const BaseClass *, float f) + { + return pow(2.f, (f + 69) / 12.f); + } + static float getSampleRate(const BaseClass *b) { return b->sampleRate; } + static float getSampleRateInv(const BaseClass *b) { return 1.0 / b->sampleRate; } + + static void preReservePool(BaseClass *, size_t) {} + static void preReserveSingleInstancePool(BaseClass *, size_t) {} + static uint8_t *checkoutBlock(BaseClass *, size_t n) + { + printf("checkoutBlock %zu\n", n); + uint8_t *ptr = (uint8_t *)malloc(n); + return ptr; + } + static void returnBlock(BaseClass *, uint8_t *ptr, size_t n) + { + printf("returnBlock %zu\n", n); + free(ptr); + } + }; + + std::unique_ptr> fx; + + // std::unique_ptr> fx; + + // std::unique_ptr> fx; + + SSTFX() { dbtlp.init(); } -struct SSTFX { - std::array fb{}; - std::array ib{}; - float sampleRate; - DbToLinearProvider dbtlp; - - struct FxConfig - { - using BaseClass = SSTFX; - static constexpr int blockSize{16}; - static void setFloatParam(BaseClass *b, int i, float f) { b->fb[i] = f; } - static float getFloatParam(const BaseClass *b, int i) { return b->fb[i]; } - - static void setIntParam(BaseClass *b, int i, int v) { b->ib[i] = v; } - static int getIntParam(const BaseClass *b, int i) { return b->ib[i]; } - - static float dbToLinear(const BaseClass *b, float f) { return b->dbtlp.dbToLinear(f); } - static float equalNoteToPitch(const BaseClass *, float f) { return pow(2.f, (f + 69) / 12.f); } - static float getSampleRate(const BaseClass *b) { return b->sampleRate; } - static float getSampleRateInv(const BaseClass *b) { return 1.0 / b->sampleRate; } - - static void preReservePool(BaseClass *, size_t) {} - static void preReserveSingleInstancePool(BaseClass *, size_t) {} - static uint8_t *checkoutBlock(BaseClass *, size_t n) { - printf("checkoutBlock %d\n", n); - uint8_t* ptr = (uint8_t*)malloc(n); - return ptr; + void init(float sampleRate) + { + sampleRate = sampleRate; + dbtlp.init(); + + // fx = std::make_unique>(); + // fx->initVoiceEffect(); + // fx->initVoiceEffectParams(); + // fx->setFloatParam(sst::voice_effects::dynamics::Compressor::fpThreshold, -10); + // fx->setFloatParam(sst::voice_effects::dynamics::Compressor::fpRatio, 7); + // fx->setFloatParam(sst::voice_effects::dynamics::Compressor::fpMakeUp, 3); + + // fx = std::make_unique>(); + // fx->initVoiceEffectParams(); + // fx->setFloatParam(sst::voice_effects::distortion::BitCrusher::fpBitdepth, 0.3); + // fx->setFloatParam(sst::voice_effects::distortion::BitCrusher::fpSamplerate, + // 0.0); + + fx = std::make_unique>(); + fx->initVoiceEffectParams(); + fx->setFloatParam(sst::voice_effects::utilities::VolumeAndPan::fpVolume, 8); + fx->setFloatParam(sst::voice_effects::utilities::VolumeAndPan::fpPan, -0.4); } - static void returnBlock(BaseClass *, uint8_t * ptr, size_t n) { - printf("returnBlock %d\n", n); - free(ptr); + + void process(const float *const datainL, const float *const datainR, float *dataoutL, + float *dataoutR, float pitch) + { + fx->processStereo(datainL, datainR, dataoutL, dataoutR, pitch); } - }; - - std::unique_ptr> fx; - - // std::unique_ptr> fx; - - // std::unique_ptr> fx; - - SSTFX() - { - dbtlp.init(); - } - - void init(float sampleRate) { - sampleRate = sampleRate; - dbtlp.init(); - - // fx = std::make_unique>(); - // fx->initVoiceEffect(); - // fx->initVoiceEffectParams(); - // fx->setFloatParam(sst::voice_effects::dynamics::Compressor::fpThreshold, -10); - // fx->setFloatParam(sst::voice_effects::dynamics::Compressor::fpRatio, 7); - // fx->setFloatParam(sst::voice_effects::dynamics::Compressor::fpMakeUp, 3); - - // fx = std::make_unique>(); - // fx->initVoiceEffectParams(); - // fx->setFloatParam(sst::voice_effects::distortion::BitCrusher::fpBitdepth, 0.3); - // fx->setFloatParam(sst::voice_effects::distortion::BitCrusher::fpSamplerate, 0.0); - - fx = std::make_unique>(); - fx->initVoiceEffectParams(); - fx->setFloatParam(sst::voice_effects::utilities::VolumeAndPan::fpVolume, 8); - fx->setFloatParam(sst::voice_effects::utilities::VolumeAndPan::fpPan, -0.4); - } - - void process(const float *const datainL, const float *const datainR, float *dataoutL, - float *dataoutR, float pitch) - { - fx->processStereo(datainL,datainR,dataoutL,dataoutR,pitch); - } - }; -int main(int argc, char const *argv[]) { - unsigned int channels; - unsigned int sampleRate; - drwav_uint64 totalPCMFrameCount; - float* pSampleData = drwav_open_file_and_read_pcm_frames_f32(argv[1], &channels, &sampleRate, &totalPCMFrameCount, NULL); - - printf("sampleRate: %d channels: %d, totalPCMFrameCount: %d\n", sampleRate, channels, totalPCMFrameCount); - - if (channels > 2) { - printf("Only 1 or 2 channels wav files supported, exiting.\n"); - exit(0); - } - - SSTFX fx; - fx.init(sampleRate); - auto blockSize = SSTFX::FxConfig::blockSize; - - uint32_t total_blocks = totalPCMFrameCount / blockSize; - - uint32_t sample_count = 0; - - float outputSamples[totalPCMFrameCount * 2]; - - FILE* datFile = fopen("/tmp/voice-effect-example.dat", "w" ); - for (size_t block = 0; block < total_blocks; block++) { - float inputL[blockSize]; - float inputR[blockSize]; - float outputL[blockSize]; - float outputR[blockSize]; - - for (size_t s = 0; s < blockSize; s++) { - if (channels == 2) { - inputL[s] = pSampleData[(block * (blockSize * 2)) + (s * 2)]; - inputR[s] = pSampleData[(block * (blockSize * 2)) + (s * 2) + 1]; - } else { - inputL[s] = pSampleData[(block * blockSize) + s]; - inputR[s] = inputL[s]; - } +int main(int argc, char const *argv[]) +{ + /* + * Set up command line arguments + */ + CLI::App app("..:: Voice Effects Example - Command Line player for SST Voice Effects ::.."); + + std::string infileName; + app.add_option("-i,--infile", infileName, "Input wav file for session")->required(); + + std::string outfileName; + app.add_option("-o,--outfile", outfileName, "Output wav file for session")->required(); + + std::string datfileName; + app.add_option("-d,--datfile", datfileName, "Optional plain text dat file"); + + bool launchGnuplot; + app.add_option("--gnuplot", launchGnuplot, "Attempt to launch gnuplot on datfile"); + + // TODO + // 1. Add a vec option (https://cliutils.github.io/CLI11/book/chapters/options.html) + // for float and int params + // 2. templatize the runner by type and allow you to select types with command line + // 3. RTAudio rather than file output + + CLI11_PARSE(app, argc, argv); + + if (launchGnuplot && datfileName.empty()) + { + std::cout << "To launch gnuplot you need to specify a datfile with -d" << std::endl; + exit(2); } - - fx.process((const float *)&inputL[0], (const float *)&inputR[0], &outputL[0], &outputR[0], 1); - - for (size_t sample_index = 0; sample_index < blockSize; sample_index++) { - outputSamples[(block * (blockSize * 2)) + (sample_index * 2)] = outputL[sample_index]; - outputSamples[(block * (blockSize * 2)) + (sample_index * 2) + 1] = outputR[sample_index]; - - fprintf(datFile, "%d %f %f\n", sample_count, inputL[sample_index], outputL[sample_index]); - sample_count++; + + unsigned int channels; + unsigned int sampleRate; + drwav_uint64 totalPCMFrameCount; + float *pSampleData = drwav_open_file_and_read_pcm_frames_f32( + infileName.c_str(), &channels, &sampleRate, &totalPCMFrameCount, NULL); + + // TODO - how does this report errors? + if (totalPCMFrameCount <= 0 || pSampleData == nullptr) + { + std::cout << "No samples in file. Exiting" << std::endl; + exit(2); + } + printf("sampleRate: %d channels: %d, totalPCMFrameCount: %llu\n", sampleRate, channels, + totalPCMFrameCount); + + if (channels > 2) + { + printf("Only 1 or 2 channels wav files supported, exiting.\n"); + exit(3); + } + + SSTFX fx; + fx.init(sampleRate); + static constexpr auto blockSize = SSTFX::FxConfig::blockSize; + + uint32_t total_blocks = totalPCMFrameCount / blockSize; + + uint32_t sample_count = 0; + + // FIXME - if we can block this we probably should + // FIXME - there are tails on effects and we need a way to specify how many sapmle tails + auto outputSamples = new float[totalPCMFrameCount * 2]; + + FILE *datFile{nullptr}; + if (!datfileName.empty()) + { + datFile = fopen(datfileName.c_str(), "w"); + + if (!datFile) + { + std::cout << "Datfile not open at '" << datfileName << "'" << std::endl; + exit(4); + } } - - } - fclose(datFile); - - drwav wav; - drwav_data_format format; - format.container = drwav_container_riff; - format.format = DR_WAVE_FORMAT_IEEE_FLOAT; - format.channels = 2; - format.sampleRate = 44100; - format.bitsPerSample = 32; - drwav_init_file_write(&wav, "/tmp/voice-effect-example.wav", &format, NULL); - drwav_uint64 framesWritten = drwav_write_pcm_frames(&wav, sample_count, outputSamples); - - system("gnuplot -p -e \"plot '/tmp/voice-effect-example.dat' using 1:2 with lines, '' using 1:3 with lines\""); - return 0; + + for (size_t block = 0; block < total_blocks; block++) + { + float inputL[blockSize]; + float inputR[blockSize]; + float outputL[blockSize]; + float outputR[blockSize]; + + for (size_t s = 0; s < blockSize; s++) + { + if (channels == 2) + { + inputL[s] = pSampleData[(block * (blockSize * 2)) + (s * 2)]; + inputR[s] = pSampleData[(block * (blockSize * 2)) + (s * 2) + 1]; + } + else + { + inputL[s] = pSampleData[(block * blockSize) + s]; + inputR[s] = inputL[s]; + } + } + + fx.process((const float *)&inputL[0], (const float *)&inputR[0], &outputL[0], &outputR[0], + 1); + + for (size_t sample_index = 0; sample_index < blockSize; sample_index++) + { + outputSamples[(block * (blockSize * 2)) + (sample_index * 2)] = outputL[sample_index]; + outputSamples[(block * (blockSize * 2)) + (sample_index * 2) + 1] = + outputR[sample_index]; + + if (datFile) + { + fprintf(datFile, "%d %f %f\n", sample_count, inputL[sample_index], + outputL[sample_index]); + } + sample_count++; + } + } + fclose(datFile); + + drwav wav; + drwav_data_format format; + format.container = drwav_container_riff; + format.format = DR_WAVE_FORMAT_IEEE_FLOAT; + format.channels = 2; + format.sampleRate = 44100; + format.bitsPerSample = 32; + drwav_init_file_write(&wav, outfileName.c_str(), &format, NULL); + drwav_uint64 framesWritten = drwav_write_pcm_frames(&wav, sample_count, outputSamples); + + delete[] outputSamples; + + if (launchGnuplot) + { + auto cmd = fmt::format("gnuplot -p -e \"plot '{}' using 1:2 with lines, '' using " + "1:3 with lines\"", + datfileName); + std::cout << "Launching " << cmd << std::endl; + system(cmd.c_str()); + } + + + return 0; } \ No newline at end of file diff --git a/scripts/fix_file_comments.pl b/scripts/fix_file_comments.pl index 9fa058b..d84bd8d 100644 --- a/scripts/fix_file_comments.pl +++ b/scripts/fix_file_comments.pl @@ -21,6 +21,15 @@ +find( + { + wanted => \&findfiles, + }, + 'examples' +); + + + sub findfiles { diff --git a/scripts/fix_header_guards.pl b/scripts/fix_header_guards.pl index 25ad12b..0603107 100644 --- a/scripts/fix_header_guards.pl +++ b/scripts/fix_header_guards.pl @@ -20,6 +20,14 @@ ); +find( + { + wanted => \&findfiles, + }, + 'examples' +); + + sub findfiles { $f = $File::Find::name;