diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6aa7910 --- /dev/null +++ b/.clang-format @@ -0,0 +1,111 @@ +--- +# BasedOnStyle: WebKit +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: No +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: All +BreakBeforeBraces: WebKit +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 0 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: true +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeCategories: + - Regex: '^"config\.h"' + Priority: -1 + # The main header for a source file automatically gets category 0 + - Regex: '^<.*SoftLink.h>' + Priority: 4 + - Regex: '^".*SoftLink.h"' + Priority: 3 + - Regex: '^<.*>' + Priority: 2 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 8 +UseTab: Never +--- +Language: Cpp +PointerAlignment: Left diff --git a/.github/workflows/ci.build.yml b/.github/workflows/ci.build.yml index a9931c0..a022668 100644 --- a/.github/workflows/ci.build.yml +++ b/.github/workflows/ci.build.yml @@ -3,30 +3,41 @@ name: Build on: pull_request: push: - branches: ['main', 'develop'] + branches: [ 'main', 'develop' ] + +defaults: + run: + shell: bash jobs: CMake: runs-on: ${{matrix.os}} strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ ubuntu-latest, macos-latest, windows-latest ] steps: - - name: "Checkout" + - name: Checkout Repository uses: actions/checkout@v4 - - name: "Prepare" - working-directory: ${{runner.workspace}}/neuron - run: cmake -E make_directory ${{runner.workspace}}/neuron/target + - name: Set Environment Variables + run: | + TARGET_PATH=target/release + echo "TARGET_PATH=$TARGET_PATH" >> $GITHUB_ENV + + - name: Prepare + run: | + mkdir -p "${{ env.TARGET_PATH }}" - - name: "Configure" - working-directory: ${{runner.workspace}}/neuron/target - run: cmake ${{runner.workspace}}/neuron + - name: Configure + run: | + cd "${{ env.TARGET_PATH }}" + cmake ../../ - - name: "Build" - working-directory: ${{runner.workspace}}/neuron/target - run: cmake --build . + - name: Build + run: | + cd "${{ env.TARGET_PATH }}" + cmake --build . --parallel 4 Make: runs-on: ${{matrix.os}} @@ -35,7 +46,7 @@ jobs: os: [ ubuntu-latest, macos-latest, windows-latest ] steps: - - name: "Checkout" + - name: Checkout uses: actions/checkout@v4 - name: "Install ARM Toolchain" @@ -43,5 +54,5 @@ jobs: with: release: '10-2020-q4' - - name: "Build" + - name: Build run: make diff --git a/.github/workflows/ci.test.yml b/.github/workflows/ci.test.yml index 6deed37..b50efbd 100644 --- a/.github/workflows/ci.test.yml +++ b/.github/workflows/ci.test.yml @@ -1,31 +1,70 @@ name: Test on: - pull_request: - push: - branches: ['main', 'develop'] + pull_request: + push: + branches: [ 'main', 'develop' ] + +defaults: + run: + shell: bash jobs: - GoogleTest: - runs-on: ubuntu-latest - steps: - - name: "Checkout" - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: "Prepare" - working-directory: ${{runner.workspace}}/neuron - run: mkdir -p target/test - - - name: "Generate Makefile" - working-directory: ${{runner.workspace}}/neuron/target/test - run: cmake -DBUILD_TESTS=ON ../../ - - - name: "Build" - working-directory: ${{runner.workspace}}/neuron/target/test - run: make - - - name: "Test" - working-directory: ${{runner.workspace}}/neuron/target/test - run: ctest + Plugin: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set Environment Variables + run: | + TARGET_PATH=${{ runner.workspace }}/neuron/target/test + echo "TARGET_PATH=$TARGET_PATH" >> $GITHUB_ENV + + - name: Prepare + run: | + mkdir -p "${{ env.TARGET_PATH }}" + + - name: Generate Makefile + working-directory: "${{ env.TARGET_PATH }}" + run: cmake -DCMAKE_BUILD_TYPE=Release -DNEO_BUILD_TESTS=ON -DNEO_PLUGIN_SUPPORT=ON ../../ + + - name: Build + working-directory: "${{ env.TARGET_PATH }}" + run: make + + - name: Test + working-directory: "${{ env.TARGET_PATH }}" + run: ctest + + + Standard: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set Environment Variables + run: | + TARGET_PATH=${{ runner.workspace }}/neuron/target/test + echo "TARGET_PATH=$TARGET_PATH" >> $GITHUB_ENV + + - name: Prepare + run: | + mkdir -p "${{ env.TARGET_PATH }}" + + - name: Generate Makefile + working-directory: "${{ env.TARGET_PATH }}" + run: cmake -DCMAKE_BUILD_TYPE=Release -DNEO_BUILD_TESTS=ON ../../ + + - name: Build + working-directory: "${{ env.TARGET_PATH }}" + run: make + + - name: Test + working-directory: "${{ env.TARGET_PATH }}" + run: ctest diff --git a/CMakeLists.txt b/CMakeLists.txt index b82e9d5..cf71623 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,58 +1,60 @@ cmake_minimum_required(VERSION 3.14) -option(BUILD_TESTS "Build unit tests" OFF) +option(NEO_PLUGIN_SUPPORT "Enable plugin support (e.g. atomic variables)" OFF) +option(NEO_BUILD_TESTS "Build unit tests" OFF) project(neuron VERSION 0.1.0 LANGUAGES CXX) -list(APPEND SOURCE - src/generators/oscillator.cpp - src/modulators/adsr.cpp - src/processors/effects/saturator.cpp - src/processors/effects/wavefolder.cpp - src/processors/filters/filter.cpp - src/utilities/logger.cpp -) - -list(APPEND INCLUDE_DIR - "src" - "src/audio" - "src/generators" - "src/modulators" - "src/processors/effects" - "src/processors/filters" - "src/utilities" -) - -add_library(neuron STATIC ${SOURCE}) - -set_target_properties(neuron PROPERTIES PUBLIC - CXX_STANDARD 14 - CXX_STANDARD_REQUIRED -) - -if(BUILD_TESTS) +file(GLOB_RECURSE SRC_FILES "src/*.cpp") + +add_library(neuron STATIC ${SRC_FILES}) + +if (NEO_PLUGIN_SUPPORT) + set_target_properties(neuron PROPERTIES + PUBLIC + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED + OSX_ARCHITECTURES "x86_64;arm64") + target_compile_definitions(neuron PUBLIC NEO_PLUGIN_SUPPORT) +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") + set_target_properties(neuron PROPERTIES + PUBLIC + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED) +endif () + +target_include_directories(neuron + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src) + +if (NEO_BUILD_TESTS) function(add_neuron_test name src) add_executable("${name}_test_exe" ${src}) - target_include_directories("${name}_test_exe" PRIVATE ${INCLUDE_DIR}) - target_link_libraries("${name}_test_exe" neuron) - target_link_libraries("${name}_test_exe" gtest gtest_main) + target_link_libraries("${name}_test_exe" PRIVATE neuron gtest gtest_main gmock gmock_main) add_test(NAME "${name}_test" COMMAND "${name}_test_exe") endfunction() add_subdirectory(vendor/googletest) + enable_testing() - add_neuron_test(waveform tests/audio/waveform_test.cpp) - add_neuron_test(oscillator tests/generators/oscillator_test.cpp) - add_neuron_test(adsr tests/modulators/adsr_test.cpp) - add_neuron_test(saturator tests/processors/effects/saturator_test.cpp) - add_neuron_test(wavefolder tests/processors/effects/wavefolder_test.cpp) - add_neuron_test(filter tests/processors/filters/filter_test.cpp) - add_neuron_test(arithmetic tests/utilities/arithmetic_test.cpp) - add_neuron_test(midi tests/utilities/midi_test.cpp) + if (NEO_PLUGIN_SUPPORT) + add_neuron_test(parameter_atomic tests/core/parameter_test_atomic.cpp) + endif () - enable_testing() -endif() + add_neuron_test(parameter tests/core/parameter_test.cpp) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") + add_neuron_test(oscillator tests/dsp/generators/oscillator_test.cpp) + add_neuron_test(adsr tests/dsp/modulators/adsr_test.cpp) + add_neuron_test(saturator tests/dsp/processors/saturator_test.cpp) + add_neuron_test(wavefolder tests/dsp/processors/wavefolder_test.cpp) + add_neuron_test(filter tests/dsp/processors/filter_test.cpp) -target_include_directories(neuron PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src PRIVATE ${INCLUDE_DIR}) + add_neuron_test(arithmetic tests/utils/arithmetic_test.cpp) + add_neuron_test(midi tests/utils/midi_test.cpp) + add_neuron_test(smoothed_value tests/utils/smoothed_value_test.cpp) + add_neuron_test(waveform tests/utils/waveform_test.cpp) +endif () diff --git a/Makefile b/Makefile index c08e05b..94630dd 100644 --- a/Makefile +++ b/Makefile @@ -1,44 +1,33 @@ TARGET = libneuron -MODULE_DIR = src +INCLUDE_DIR = include +SRC_DIR = src # Each Module Directory is listed below with it's modules. # Header only modules are listed commented out # below the others. -AUDIO_MOD_DIR = audio -AUDIO_MODULES = \ - -GENERATOR_MOD_DIR = generators +GENERATOR_MOD_DIR = dsp/generators GENERATOR_MODULES = \ oscillator \ -MODULATOR_MOD_DIR = modulators +MODULATOR_MOD_DIR = dsp/modulators MODULATOR_MODULES = \ adsr \ -PROCESSOR_EFFECTS_MOD_DIR = processors/effects -PROCESSOR_EFFECTS_MODULES = \ +PROCESSOR_MOD_DIR = dsp/processors +PROCESSOR_MODULES = \ +filter \ saturator \ wavefolder \ -PROCESSOR_FILTERS_MOD_DIR = processors/filters -PROCESS_FILTERS_MODULES = \ -filter \ - -UTILITY_MOD_DIR = utilities -UTILITY_MODULES = \ -logger - ###################################### # source ###################################### -CPP_SOURCES += $(addsuffix .cpp, $(MODULE_DIR)/$(GENERATOR_MOD_DIR)/$(GENERATOR_MODULES)) -CPP_SOURCES += $(addsuffix .cpp, $(MODULE_DIR)/$(MODULATOR_MOD_DIR)/$(MODULATOR_MODULES)) -CPP_SOURCES += $(addsuffix .cpp, $(MODULE_DIR)/$(PROCESSOR_EFFECTS_MOD_DIR)/$(PROCESSOR_EFFECTS_MODULES)) -CPP_SOURCES += $(addsuffix .cpp, $(MODULE_DIR)/$(PROCESSOR_FILTERS_MOD_DIR)/$(PROCESS_FILTERS_MODULES)) -CPP_SOURCES += $(addsuffix .cpp, $(MODULE_DIR)/$(UTILITY_MOD_DIR)/$(UTILITY_MODULES)) +CPP_SOURCES += $(addsuffix .cpp, $(SRC_DIR)/$(GENERATOR_MOD_DIR)/$(GENERATOR_MODULES)) +CPP_SOURCES += $(addsuffix .cpp, $(SRC_DIR)/$(MODULATOR_MOD_DIR)/$(MODULATOR_MODULES)) +CPP_SOURCES += $(addsuffix .cpp, $(SRC_DIR)/$(PROCESSOR_MOD_DIR)/$(PROCESSOR_MODULES)) ###################################### # building variables @@ -111,13 +100,8 @@ C_DEFS = \ -DSTM32H750xx C_INCLUDES = \ --I$(MODULE_DIR) \ --I$(MODULE_DIR)/$(AUDIO_MOD_DIR) \ --I$(MODULE_DIR)/$(GENERATOR_MOD_DIR) \ --I$(MODULE_DIR)/$(MODULATOR_MOD_DIR) \ --I$(MODULE_DIR)/$(PROCESSOR_EFFECTS_MOD_DIR) \ --I$(MODULE_DIR)/$(PROCESSOR_FILTERS_MOD_DIR) \ --I$(MODULE_DIR)/$(UTILITY_MOD_DIR) \ +-I$(INCLUDE_DIR) \ +-I$(SRC_DIR) \ # compile gcc flags ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections diff --git a/README.md b/README.md index 856bbb7..ea33563 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # `neuron` -[![Test](https://github.com/blackboxaudio/neuron/actions/workflows/ci.test.yml/badge.svg)](https://github.com/blackboxaudio/neuron/actions/workflows/ci.test.yml) [![Build](https://github.com/blackboxaudio/neuron/actions/workflows/ci.build.yml/badge.svg)](https://github.com/blackboxaudio/neuron/actions/workflows/ci.build.yml) +[![Test](https://github.com/blackboxaudio/neuron/actions/workflows/ci.test.yml/badge.svg)](https://github.com/blackboxaudio/neuron/actions/workflows/ci.test.yml) [![neuron: v0.1.0](https://img.shields.io/badge/Version-v0.1.0-blue.svg)](https://github.com/blackboxaudio/neuron) [![License](https://img.shields.io/badge/License-MIT-yellow)](https://github.com/blackboxaudio/neuron/blob/develop/LICENSE) -> Collection of C++ audio DSP components 🧠 +> Collection of C++ audio DSP components ⚡ ## Overview @@ -69,11 +69,11 @@ make ## Using the Library ```c++ -#include "neuron.h" +#include "neuron/neuron.h" // Create a DSP context (sample rate, // number of channels, buffer size). -static Context context { +static neuron::Context context { 44100, 1, 128, @@ -81,11 +81,20 @@ static Context context { // Create an oscillator with an initial // frequency of 440Hz. -static Oscillator osc(context, 440.0f); +static neuron::Oscillator osc(context, 440.0f); // Write to the buffer with samples // generated from the oscillator for(size_t idx = 0; idx < 128; idx++) { - buffer[idx] = (float)osc.Generate(); + buffer[idx] = osc.Generate(); } ``` + +Enable the `NEO_PLUGIN_SUPPORT` compile option in CMake to include necessary bits for JUCE integration: + +```cmake +# Set option for neuron to enable plugin support (e.g. std::atomic) +set(NEO_PLUGIN_SUPPORT ON) + +... +``` diff --git a/include/neuron/core/base.h b/include/neuron/core/base.h new file mode 100644 index 0000000..04f97fd --- /dev/null +++ b/include/neuron/core/base.h @@ -0,0 +1,32 @@ +#pragma once + +#ifdef NEO_PLUGIN_SUPPORT +#include +#endif + +namespace neuron { + + /** + * Describes a Neuron DSP component, capable of processing, or + * generating signals. + */ + template + class Neuron { + public: + /** + * Frees any memory allocated by the processor. + */ + ~Neuron() = default; + +#ifdef NEO_PLUGIN_SUPPORT + /** + * Attach a source via an atomic pointer to a given parameter. + */ + void AttachParameterToSource(const P parameter, std::atomic* source) + { + static_cast(this)->AttachParameterToSourceImpl(parameter, source); + } +#endif + }; + +} diff --git a/include/neuron/core/context.h b/include/neuron/core/context.h new file mode 100644 index 0000000..4b911fc --- /dev/null +++ b/include/neuron/core/context.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace neuron { + + /** + * Contains information about the context in which DSP operations + * will be executed, useful for some components such as oscillators + * that use the sample rate to calculate phase positions. + */ + struct Context { + size_t sampleRate; + size_t numChannels; + size_t blockSize; + }; + + /** + * The common default context, using a sample rate of 44.1kHz, stereo + * channel configuration, and a buffer size of 16 samples. + */ + static Context DEFAULT_CONTEXT = { 44100, 2, 16 }; + +} diff --git a/include/neuron/core/parameter.h b/include/neuron/core/parameter.h new file mode 100644 index 0000000..7a4a6b7 --- /dev/null +++ b/include/neuron/core/parameter.h @@ -0,0 +1,144 @@ +#pragma once + +#ifdef NEO_PLUGIN_SUPPORT +#include +#endif + +namespace neuron { + +#ifdef NEO_PLUGIN_SUPPORT + /** + * A read-only parameter used by a DSP component to allow more + * control and flexibility in shaping its sound. + */ + template + class Parameter { + public: + explicit Parameter(T value) + { + m_parameter->store(value); + } + + explicit Parameter(std::atomic* source) + { + m_parameter = source; + } + + ~Parameter() + { + m_parameter = nullptr; + } + + /** + * Attaches a new source for this parameter to read data from. + * + * CAUTION: If this method is called, the corresponding DSP component's + * setter method for this parameter will no longer update the variable. + * + * @param source The new pointer that this parameter will read from and write to. + */ + void AttachSource(std::atomic* source) + { + m_parameter = source; + } + + operator T() const + { + return m_parameter->load(); + } + + Parameter& operator=(T value) + { + m_parameter->store(value); + return *this; + } + + T operator+(T value) const + { + return m_parameter->load() + value; + } + + T operator-(T value) const + { + return m_parameter->load() - value; + } + + T operator*(T value) const + { + return m_parameter->load() * value; + } + + T operator/(T value) const + { + if (value == 0.0f) { + return value; + } else { + return m_parameter->load() / value; + } + } + + private: + /** + * CAUTION: This empty value is used as a safe initializer for the pointer, + * which is what is used by the JUCE library. + */ + std::atomic m_initial_source { 0.0f }; + std::atomic* m_parameter = &m_initial_source; + }; + +#else + /** + * An adjustable parameter used by a DSP component to allow more + * control and flexibility in shaping its sound. + */ + template + class Parameter { + public: + explicit Parameter(T value = 0.0f) + { + m_parameter = value; + } + + ~Parameter() = default; + + operator float() const + { + return m_parameter; + } + + Parameter& operator=(T value) + { + m_parameter = value; + return *this; + } + + T operator+(T value) const + { + return m_parameter + value; + } + + T operator-(T value) const + { + return m_parameter - value; + } + + T operator*(T value) const + { + return m_parameter * value; + } + + T operator/(T value) const + { + if (value == 0.0) { + return value; + } else { + return m_parameter / value; + } + } + + private: + T m_parameter; + }; +#endif + +} diff --git a/include/neuron/core/sample.h b/include/neuron/core/sample.h new file mode 100644 index 0000000..e6865de --- /dev/null +++ b/include/neuron/core/sample.h @@ -0,0 +1,21 @@ +#pragma once + +namespace neuron { + + /** + * The basic data type for DSP operations of + * an audio signal. + */ + using Sample = float; + + /** + * The minimum possible value for a sample. + */ + const Sample MIN = -1.0f; + + /** + * The maximum possible value for a sample. + */ + const Sample MAX = 1.0f; + +} diff --git a/include/neuron/dsp/generators/generator.h b/include/neuron/dsp/generators/generator.h new file mode 100644 index 0000000..aa6b9c7 --- /dev/null +++ b/include/neuron/dsp/generators/generator.h @@ -0,0 +1,31 @@ +#pragma once + +#include "neuron/core/sample.h" + +namespace neuron { + + /** + * Describes a DSP component that generates a signal without + * processing an input signal. + */ + template + class Generator { + public: + /** + * Frees any memory allocated by the generator. + */ + ~Generator() = default; + + /** + * Generates a sample of some audio signal, depending + * on the type of generator, G. + * + * @return Sample + */ + Sample Generate() + { + return static_cast(this)->GenerateImpl(); + } + }; + +} diff --git a/include/neuron/dsp/generators/oscillator.h b/include/neuron/dsp/generators/oscillator.h new file mode 100644 index 0000000..0307e26 --- /dev/null +++ b/include/neuron/dsp/generators/oscillator.h @@ -0,0 +1,99 @@ +#pragma once + +#include "neuron/core/base.h" +#include "neuron/core/context.h" +#include "neuron/core/parameter.h" +#include "neuron/dsp/generators/generator.h" +#include "neuron/utils/arithmetic.h" +#include "neuron/utils/waveform.h" + +namespace neuron { + + const size_t WAVETABLE_SIZE = 256; + + enum OscillatorParameter { + OSC_FREQUENCY, + }; + + /** + * The Oscillator class creates an audio signal + * with a basic waveform. + */ + class Oscillator : public Generator, public Neuron { + public: + /** + * Creates an oscillator generator. + * + * @param context The DSP context to be used by the oscillator. + * @param frequency The initial frequency of the oscillator. + * @return Oscillator + */ + explicit Oscillator(Context& context = DEFAULT_CONTEXT, float frequency = 440.0f, Waveform waveform = Waveform::SINE); + + /** + * Frees any memory allocated by the oscillator. + */ + ~Oscillator(); + + /** + * Resets the phase of the oscillator, starting it at the beginning + * waveform position. + * + * @param + */ + void Reset(float phase = 0.0f); + + /** + * Sets the frequency of the oscillator. + * + * @param frequency The new oscillator output frequency. + */ + void SetFrequency(float frequency); + + /** + * Sets the waveform of the oscillator. + * + * @param waveform The new oscillator output waveform. + */ + void SetWaveform(Waveform waveform); + + /** + * Attaches a follower oscillator to be synced to this one. + * + * @param oscillator The oscillator that will be synced to this one. + */ + void AttachFollower(Oscillator* oscillator); + + /** + * Detaches the follower oscillator from this one. + */ + void DetachFollower(); + + protected: + friend class Generator; + Sample GenerateImpl(); + +#ifdef NEO_PLUGIN_SUPPORT + friend class Neuron; + void AttachParameterToSourceImpl(OscillatorParameter parameter, std::atomic* source); +#endif + + private: + void PopulateWavetable(); + void IncrementPhase(); + Sample Lerp(); + + Context& m_context; + + Sample m_wavetable[WAVETABLE_SIZE]; + Waveform m_waveform; + + Parameter p_frequency; + + float m_phase = 0.0f; + float m_phaseIncrement = 0.0f; + + Oscillator* m_follower = nullptr; + }; + +} diff --git a/include/neuron/dsp/modulators/adsr.h b/include/neuron/dsp/modulators/adsr.h new file mode 100644 index 0000000..1cc7493 --- /dev/null +++ b/include/neuron/dsp/modulators/adsr.h @@ -0,0 +1,126 @@ +#pragma once + +#include "neuron/core/base.h" +#include "neuron/core/context.h" +#include "neuron/core/parameter.h" +#include "neuron/dsp/modulators/modulator.h" + +namespace neuron { + + /** + * The configuration of an ADSR envelope curve including durations + * for the attack, decay, and release stages as well as a sustain level. + */ + struct AdsrEnvelope { + float attack; + float decay; + float sustain; + float release; + }; + + enum AdsrParameter { + ADSR_ATTACK, + ADSR_DECAY, + ADSR_SUSTAIN, + ADSR_RELEASE, + }; + + /** + * The stages of an ADSR envelope, including an "idle" stage when not in use. + */ + enum AdsrStage { + IDLE, + ATTACK, + DECAY, + SUSTAIN, + RELEASE + }; + + const AdsrEnvelope DEFAULT_ADSR_ENVELOPE = { + 100.0f, 200.0f, 1.0f, 1000.0f + }; + + /** + * The AdsrEnvelopeModulator class is a modulation source + * that is based off of an ADSR envelope generator. + */ + class AdsrEnvelopeModulator : public Modulator, public Neuron { + public: + /** + * Creates an ADSR envelope modulator. + * + * @param context The DSP context to be used by the envelope. + * @param envelope The envelope configuration to initialize the class with. + * + * @return AdsrEnvelopeModulator + */ + explicit AdsrEnvelopeModulator(Context& context = DEFAULT_CONTEXT, AdsrEnvelope envelope = DEFAULT_ADSR_ENVELOPE); + + /** + * Starts the envelope from its attack phase. + */ + void Trigger(); + + /** + * Starts the envelope from its release phase. + */ + void Release(); + + /** + * Re-initializes the modulator, ready to be re-triggered. + */ + void Reset(); + + /** + * Sets the attack time of the envelope. + * + * @param attackTimeMs The new attack time for the envelope. + */ + void SetAttackTime(float attackTimeMs); + + /** + * Sets the decay time of the envelope. + * + * @param decayTimeMs The new decay time for the envelope. + */ + void SetDecayTime(float decayTimeMs); + + /** + * Sets the sustain level of the envelope. + * + * @param sustainLevel The new sustain level for the envelope. + */ + void SetSustainLevel(float sustainLevel); + + /** + * Sets the release time of the envelope. + * + * @param releaseTimeMs The new release time for the envelope. + */ + void SetReleaseTime(float releaseTimeMs); + + protected: + friend class Modulator; + float ModulateImpl(); + +#ifdef NEO_PLUGIN_SUPPORT + friend class Neuron; + void AttachParameterToSourceImpl(AdsrParameter parameter, std::atomic* source); +#endif + + private: + // Checks and updates the modulator's state if necessary + void Update(float stageDuration, AdsrStage nextStage, bool incrementSampleCount); + + Context& m_context; + + Parameter p_attack; + Parameter p_decay; + Parameter p_sustain; + Parameter p_release; + + AdsrStage m_stage = AdsrStage::IDLE; + size_t m_samplesSinceLastStage = 0; + }; + +} diff --git a/include/neuron/dsp/modulators/modulator.h b/include/neuron/dsp/modulators/modulator.h new file mode 100644 index 0000000..f26d175 --- /dev/null +++ b/include/neuron/dsp/modulators/modulator.h @@ -0,0 +1,28 @@ +#pragma once + +namespace neuron { + + /** + * Describes a DSP component that produces a stream of data + * that changes the parameter of another DSP component over time. + */ + template + class Modulator { + public: + /** + * Frees any memory allocated by the modulator. + */ + ~Modulator() = default; + + /** + * Creates a modulation value to be used elsewhere. + * + * @return float + */ + float Modulate() + { + return static_cast(this)->ModulateImpl(); + } + }; + +} diff --git a/include/neuron/dsp/processors/filter.h b/include/neuron/dsp/processors/filter.h new file mode 100644 index 0000000..ac61a77 --- /dev/null +++ b/include/neuron/dsp/processors/filter.h @@ -0,0 +1,61 @@ +#pragma once + +#include "neuron/core/base.h" +#include "neuron/core/context.h" +#include "neuron/core/parameter.h" +#include "neuron/core/sample.h" +#include "neuron/dsp/processors/processor.h" + +namespace neuron { + + const float FILTER_CUTOFF_FREQ_MIN = 20.0f; + const float FILTER_CUTOFF_FREQ_MAX = 20000.0f; + + enum FilterParameter { + FILTER_CUTOFF_FREQUENCY, + }; + + /** + * The Filter class applies a simple low-pass filter + * to audio signals. + */ + class Filter : public Processor, public Neuron { + public: + /** + * Creates a filter processor. + * + * @param context The DSP context to be used by the filter. + * @param cutoffFrequency The initial cutoff frequency of the filter. + * @return Filter + */ + explicit Filter(Context& context = DEFAULT_CONTEXT, + float cutoffFrequency = FILTER_CUTOFF_FREQ_MAX); + + /** + * Sets the filter's cutoff frequency. + * + * @param frequency The new cutoff frequency. + */ + void SetCutoffFrequency(float frequency); + + Parameter p_cutoffFrequency; + + protected: + friend class Processor; + Sample ProcessImpl(Sample input); + +#ifdef NEO_PLUGIN_SUPPORT + friend class Neuron; + void AttachParameterToSourceImpl(FilterParameter parameter, std::atomic* source); +#endif + + private: + void CalculateAlpha(); + + Context& m_context; + + float m_alpha; + Sample m_previousOutput; + }; + +} diff --git a/include/neuron/dsp/processors/processor.h b/include/neuron/dsp/processors/processor.h new file mode 100644 index 0000000..6596c24 --- /dev/null +++ b/include/neuron/dsp/processors/processor.h @@ -0,0 +1,30 @@ +#pragma once + +#include "neuron/core/sample.h" + +namespace neuron { + + /** + * Describes a DSP component that does some processing on + * an input signal to produce an output signal. + */ + template + class Processor { + public: + /** + * Frees any memory allocated by the processor. + */ + ~Processor() = default; + + /** + * Processes a sample of some audio signal. + * + * @return Sample + */ + Sample Process(Sample input) + { + return static_cast(this)->ProcessImpl(input); + } + }; + +} diff --git a/include/neuron/dsp/processors/saturator.h b/include/neuron/dsp/processors/saturator.h new file mode 100644 index 0000000..cd5f5fc --- /dev/null +++ b/include/neuron/dsp/processors/saturator.h @@ -0,0 +1,66 @@ +#pragma once + +#include "neuron/core/base.h" +#include "neuron/core/parameter.h" +#include "neuron/core/sample.h" +#include "neuron/dsp/processors/processor.h" + +namespace neuron { + + enum SaturatorParameter { + SATURATOR_SATURATION, + SATURATOR_SYMMETRY, + }; + + /** + * The Saturator class applies a tape saturation + * algorithm to audio signals. + */ + class Saturator : public Processor, public Neuron { + public: + /** + * Creates a default saturator processor. + * + * @return Saturator + */ + Saturator(); + + /** + * Frees any memory allocated by the saturator. + */ + ~Saturator() = default; + + /** + * Sets the saturation level, which boosts the signal before + * distortion is applied. This multiplier will always be greater than + * one. + * + * @param saturation The multiplier of the audio signal going into the + * distortion algorithm. + */ + void SetSaturation(float saturation); + + /** + * Sets the symmetry of the algorithm, determining how much + * distortion to apply to the positive and negative parts + * of the signal separately. + * + * @param symmetry A value between 0.0 and 1.0, ranging from asymmetrical + * (one-sided) to symmetrical respectively. + */ + void SetSymmetry(float symmetry); + + Parameter p_saturation; + Parameter p_symmetry; + + protected: + friend class Processor; + Sample ProcessImpl(Sample input); + +#ifdef NEO_PLUGIN_SUPPORT + friend class Neuron; + void AttachParameterToSourceImpl(SaturatorParameter parameter, std::atomic* source); +#endif + }; + +} \ No newline at end of file diff --git a/include/neuron/dsp/processors/wavefolder.h b/include/neuron/dsp/processors/wavefolder.h new file mode 100644 index 0000000..d9cb0bd --- /dev/null +++ b/include/neuron/dsp/processors/wavefolder.h @@ -0,0 +1,66 @@ +#pragma once + +#include "neuron/core/base.h" +#include "neuron/core/parameter.h" +#include "neuron/dsp/processors/processor.h" + +namespace neuron { + + enum WavefolderParameter { + WAVEFOLDER_INPUT_GAIN, + WAVEFOLDER_THRESHOLD, + WAVEFOLDER_SYMMETRY, + }; + + /** + * The Wavefolder class applies a wavefolding + * algorithm to audio signals. + */ + class Wavefolder : public Processor, public Neuron { + public: + /** + * Creates a default wavefolder processor. + */ + Wavefolder(); + + /** + * Sets the input gain level, which boosts the signal before + * being measured against the wavefolder threshold. + * + * @param gain The multiplier of the audio signal going into the + * wavefolding algorithm. + */ + void SetInputGain(float gain); + + /** + * Sets the threshold of the wavefolder, above which samples will + * be "folded" toawrds zero until they are within the threshold. + */ + void SetThreshold(float threshold); + + /** + * Sets the symmetry of the algorithm, determining how much + * wavefolding to apply to the positive and negative parts + * of the signal separately. + * + * @param symmetry A value between 0.0 and 1.0, ranging from asymmetrical + * (one-sided) to symmetrical respectively. + */ + void SetSymmetry(float symmetry); + + protected: + friend class Processor; + Sample ProcessImpl(Sample input); + +#ifdef NEO_PLUGIN_SUPPORT + friend class Neuron; + void AttachParameterToSourceImpl(const WavefolderParameter parameter, std::atomic* source); +#endif + + private: + Parameter p_inputGain; + Parameter p_threshold; + Parameter p_symmetry; + }; + +} diff --git a/include/neuron/neuron.h b/include/neuron/neuron.h new file mode 100644 index 0000000..5ba5bc7 --- /dev/null +++ b/include/neuron/neuron.h @@ -0,0 +1,34 @@ +/** +* Neuron is a lightweight audio DSP library intended for use + * in any relevant application e.g. Electrosmith Daisy patches, + * JUCE plugins, VCV Rack modules. + * + * Author: Matthew Maxwell, 2024 + */ +#pragma once + +#ifndef NEURON_LIB_H +#define NEURON_LIB_H + +// CORE +#include "neuron/core/base.h" +#include "neuron/core/context.h" +#include "neuron/core/parameter.h" +#include "neuron/core/sample.h" + +// DSP (Generators) +#include "neuron/dsp/generators/generator.h" + +// DSP (Modulators) +#include "neuron/dsp/modulators/modulator.h" + +// DSP (Processors) +#include "neuron/dsp/processors/processor.h" + +// UTILS +#include "neuron/utils/arithmetic.h" +#include "neuron/utils/midi.h" +#include "neuron/utils/smoothed_value.h" +#include "neuron/utils/waveform.h" + +#endif diff --git a/include/neuron/utils/arithmetic.h b/include/neuron/utils/arithmetic.h new file mode 100644 index 0000000..bb0cbb9 --- /dev/null +++ b/include/neuron/utils/arithmetic.h @@ -0,0 +1,147 @@ +#pragma once + +#include +#include + +namespace neuron { + + /** + * A value for the absence of quantity; nothing. + */ + const float ZERO = 0.0f; + + /** + * A value that leaves any other value unchanged + * when multiplied with it. + */ + const float IDENTITY = 1.0f; + + /** + * An irrational number that represents the ratio of the circumference of a + * circle to its diameter. + */ + const float PI = 3.14159265358979323846264338327950288f; + + /** + * An irrational number that is the base of the natural logarithm. + */ + const float EULER = 2.71828182845904523536028747135266250f; + + /** + * Depicts different mathematical curves, e.g. exponential, + * linear, logarithmic. + */ + enum Mapping { + EXP, + LOG, + LINEAR, + }; + + template + struct Epsilon { + static constexpr T value = T { 1e-5 }; + }; + + template<> + struct Epsilon { + static constexpr float value = 1e-5f; + }; + + template<> + struct Epsilon { + static constexpr double value = 1e-9; + }; + + template<> + struct Epsilon { + static constexpr int value = 0; + }; + + template<> + struct Epsilon { + static constexpr long value = 0; + }; + + /** + * Constricts a number between a lower and upper bound. + * + * @param n The number to the be clamped. + * @param min The lowest possible number n can be. + * @param max The highest possible number n can be. + * @return T + */ + template + inline T clamp(const T n, const T min, const T max) + { + return std::max(min, std::min(n, max)); + } + + /** + * Scales a number between 0 and 1 to a number within a range from min to max. + * + * @param n The number to scale. + * @param min The lower bound of the range to scale n. + * @param max The upper bound of the range to scale n. + * @param curve The curve in which to scale n (e.g. linear, exponential, + * logarithmic) + * @return T + */ + template + inline T map(const T n, const T min, const T max, + const Mapping curve = Mapping::LINEAR) + { + switch (curve) { + case Mapping::EXP: + return clamp(min + (n * n) * (max - min), min, max); + case Mapping::LOG: { + const float a = 1.f / log10f(max / min); + return clamp(min * powf(10, n / a), min, max); + } + case Mapping::LINEAR: + default: + return clamp(min + n * (max - min), min, max); + } + } + + /** + * A fast approximation of the hyperbolic tangent function, or `tanh`. + * + * NOTE: This function works best on a limited range from -5 to 5. + * + * @param n The function input. + * @return + */ + template + inline T tanh(T n) + { + auto n2 = n * n; + auto numerator = n * (135135 + n2 * (17325 + n2 * (378 + n2))); + auto denominator = 135135 + n2 * (62370 + n2 * (3150 + 28 * n2)); + return numerator / denominator; + } + + /** + * A fast approximation of the exponential function, or `exp`. + * + * NOTE: This function works best on a limited range from -6 to 4. + * + * @param n The function input. + * @return + */ + template + inline T exp(T n) + { + auto numerator = 1680 + n * (840 + n * (180 + n * (20 + n))); + auto denominator = 1680 + n * (-840 + n * (180 + n * (-20 + n))); + return numerator / denominator; + } + + template + inline bool isApproximatelyEqual(T a, T b, T relativeEpsilon = neuron::Epsilon::value) noexcept + { + T diff = std::abs(a - b); + T largest = std::max(std::abs(a), std::abs(b)); + return diff <= relativeEpsilon * largest; + } + +} diff --git a/include/neuron/utils/midi.h b/include/neuron/utils/midi.h new file mode 100644 index 0000000..82004c1 --- /dev/null +++ b/include/neuron/utils/midi.h @@ -0,0 +1,20 @@ +#pragma once + +#include "neuron/utils/arithmetic.h" + +namespace neuron { + + /** + * Converts a MIDI note number to a (floating-point) frequency. Any value outside + * of the proper note range [0, 127] will be clamped. + * + * @param n The MIDI note number to convert + * @return + */ + template + inline T midi_to_frequency(T n) + { + return powf(2, (clamp(n, 0.0f, 127.0f) - 69.0f) / 12.0f) * 440.0f; + } + +} diff --git a/include/neuron/utils/smoothed_value.h b/include/neuron/utils/smoothed_value.h new file mode 100644 index 0000000..2e9d55e --- /dev/null +++ b/include/neuron/utils/smoothed_value.h @@ -0,0 +1,189 @@ +#pragma once + +#include "neuron/utils/arithmetic.h" + +#include + +namespace neuron { + enum SmoothingType { + Linear, + Multiplicative, + }; + + template + class SmoothedValue; + + template<> + class SmoothedValue { + public: + SmoothedValue() + : m_currentValue(0.0f) + , m_targetValue(0.0f) + { + UpdateIncrement(); + } + + SmoothedValue(float initialValue) + : m_currentValue(initialValue) + , m_targetValue(initialValue) + { + UpdateIncrement(); + } + + void Reset(double sampleRate, double rampLengthMillis) + { + if (sampleRate > 0.0 && rampLengthMillis >= 0.0) { + m_sampleRate = sampleRate; + m_rampLengthMillis = rampLengthMillis; + UpdateIncrement(); + } + } + + void SetTargetValue(float value) noexcept + { + m_targetValue = value; + UpdateIncrement(); + } + + float GetNextValue() noexcept + { + if (isApproximatelyEqual(m_currentValue, m_targetValue)) { + m_currentValue = m_targetValue; + return m_currentValue; + } + + m_currentValue += m_increment; + if ((m_increment > 0.0f && m_currentValue > m_targetValue) || (m_increment < 0.0f && m_currentValue < m_targetValue)) { + m_currentValue = m_targetValue; + } + + return m_currentValue; + } + + void Skip(int numSamples) + { + if (isApproximatelyEqual(m_currentValue, m_targetValue)) { + m_currentValue = m_targetValue; + return; + } + + float newValue = m_currentValue + m_increment * numSamples; + if ((m_increment > 0.0f && newValue > m_targetValue) || (m_increment < 0.0f && newValue < m_targetValue)) { + m_currentValue = m_targetValue; + } else { + m_currentValue = newValue; + } + } + + private: + static constexpr float INV_1000 = 1.0f / 1000.0f; + + void UpdateIncrement() noexcept + { + if (m_rampLengthMillis <= 0.0) { + m_currentValue = m_targetValue; + m_increment = 0.0f; + return; + } + + float numSamplesBetweenValues = static_cast(m_rampLengthMillis * m_sampleRate) * INV_1000; + m_increment = (m_targetValue - m_currentValue) / numSamplesBetweenValues; + } + + double m_sampleRate = 44100.0; + double m_rampLengthMillis = 50; + + float m_currentValue; + float m_targetValue; + + float m_increment = 0.0f; + }; + + template<> + class SmoothedValue { + public: + SmoothedValue() + : m_currentValue(1.0f) + , m_targetValue(1.0f) + { + UpdateIncrement(); + } + + SmoothedValue(float initialValue) + : m_currentValue(initialValue) + , m_targetValue(initialValue) + { + UpdateIncrement(); + } + + void Reset(double sampleRate, double rampLengthMillis) + { + if (sampleRate > 0.0 && rampLengthMillis >= 0.0) { + m_sampleRate = sampleRate; + m_rampLengthMillis = rampLengthMillis; + UpdateIncrement(); + } + } + + void SetTargetValue(float value) noexcept + { + m_targetValue = value; + UpdateIncrement(); + } + + float GetNextValue() noexcept + { + if (isApproximatelyEqual(m_currentValue, m_targetValue)) { + m_currentValue = m_targetValue; + return m_currentValue; + } + + m_currentValue *= std::exp(m_increment); + if ((m_increment > 0.0f && m_currentValue > m_targetValue) || (m_increment < 0.0f && m_currentValue < m_targetValue)) { + m_currentValue = m_targetValue; + } + + return m_currentValue; + } + + void Skip(int numSamples) + { + if (isApproximatelyEqual(m_currentValue, m_targetValue)) { + m_currentValue = m_targetValue; + return; + } + + m_currentValue *= std::exp(m_increment * numSamples); + } + + private: + static constexpr float INV_1000 = 1.0f / 1000.0f; + + void UpdateIncrement() noexcept + { + if (m_rampLengthMillis <= 0.0) { + m_currentValue = m_targetValue; + m_increment = 0.0f; + return; + } + + float numSamplesBetweenValues = static_cast(m_rampLengthMillis * m_sampleRate) * INV_1000; + if (m_currentValue > 0.0f && m_targetValue > 0.0f) { + m_increment = std::log(m_targetValue / m_currentValue) / numSamplesBetweenValues; + } else { + m_increment = 0.0f; + } + } + + double m_sampleRate = 44100.0; + double m_rampLengthMillis = 50; + + float m_currentValue; + float m_targetValue; + + float m_increment = 0.0f; + }; + + using LinearSmoothedValue = SmoothedValue; + using MultiplicativeSmoothedValue = SmoothedValue; +} diff --git a/include/neuron/utils/waveform.h b/include/neuron/utils/waveform.h new file mode 100644 index 0000000..5d76e3d --- /dev/null +++ b/include/neuron/utils/waveform.h @@ -0,0 +1,41 @@ +#pragma once + +#include "neuron/core/sample.h" +#include "neuron/utils/arithmetic.h" + +namespace neuron { + + /** + * The basic waveform variants, i.e. sine, triangle, + * sawtooth, and square. + */ + enum class Waveform { + SINE, + TRIANGLE, + SAWTOOTH, + SQUARE, + }; + + /** + * Converts the value of a sine wave to a basic waveform. + * + * @param sample The sample value to convert. + * @param waveform The waveform to convert the sample to. + * @return Sample + */ + inline Sample SineToWaveform(Sample sample, Waveform waveform) + { + switch (waveform) { + default: + case Waveform::SINE: + return sample; + case Waveform::TRIANGLE: + return sample >= 0.0f ? (2.0f * sample - 1.0f) : (2.0f * -sample - 1.0f); + case Waveform::SAWTOOTH: + return (2.0f * PI * sample) - floorf((2.0f * PI * sample) + 0.5f); + case Waveform::SQUARE: + return sample >= 0.0f ? 1.0f : -1.0f; + } + } + +} \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh index 88c1770..c6e1588 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,7 +8,7 @@ convertsecs() { START_TIME=$(date +%s) -CONFIG=${1:-debug} +CONFIG=${1:-release} if [ $CONFIG != "debug" ] && [ $CONFIG != "release" ] && [ $CONFIG != "test" ]; then echo "Invalid build configuration" exit 1 diff --git a/scripts/format.sh b/scripts/format.sh index a11304c..244c151 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,13 +1,20 @@ #!/bin/bash printf "Formatting code...\n" -find src/ -iname '*.h' -o -iname '*.cpp' | xargs clang-format -i -style=WebKit + +find include/ -iname '*.h' | xargs clang-format -i -style=file +if [ $? -ne 0 ]; then + printf "Failed to format source code\n" + exit 1 +fi + +find src/ -iname '*.h' -o -iname '*.cpp' | xargs clang-format -i -style=file if [ $? -ne 0 ]; then printf "Failed to format source code\n" exit 1 fi -find tests/ -iname '*.h' -o -iname '*.cpp' | xargs clang-format -i -style=WebKit +find tests/ -iname '*.h' -o -iname '*.cpp' | xargs clang-format -i -style=file if [ $? -ne 0 ]; then printf "Failed to format test code\n" exit 1 diff --git a/scripts/test.sh b/scripts/test.sh index e8ecd6d..cedc4a2 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -5,6 +5,7 @@ TARGET_DIR=$PWD/target/test BUILD_TESTS=false OUTPUT_FAILURE=false +PLUGIN_SUPPORT=false for i in "$@"; do case $i in @@ -16,18 +17,31 @@ for i in "$@"; do OUTPUT_FAILURE=true shift ;; + -p|--plugin-support) + PLUGIN_SUPPORT=true + shift + ;; esac done +CMAKE_FLAGS="-DCMAKE_BUILD_TYPE=Release -DNEO_BUILD_TESTS=ON" +if [ "$PLUGIN_SUPPORT" == "true" ]; then + CMAKE_FLAGS+=" -DNEO_PLUGIN_SUPPORT=ON" +fi + if [ $BUILD_TESTS == "true" ]; then rm -rf "$TARGET_DIR" mkdir -p "$TARGET_DIR" cd "$TARGET_DIR" || exit 1 - cmake -DBUILD_TESTS=ON ../../ + cmake $CMAKE_FLAGS ../../ make else cd "$TARGET_DIR" || exit 1 fi -CTEST_FLAGS=$([ $OUTPUT_FAILURE == "true" ] && echo "--rerun-failed --output-on-failure") -ctest $CTEST_FLAGS +CTEST_CMD="ctest" +if [ "$OUTPUT_FAILURE" == "true" ]; then + CTEST_CMD="$CTEST_CMD --rerun-failed --output-on-failure" +fi + +eval "$CTEST_CMD" diff --git a/src/audio/context.h b/src/audio/context.h deleted file mode 100644 index 3fdb3de..0000000 --- a/src/audio/context.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -namespace neuron { - -/** - * Contains information about the context in which DSP operations - * will be executed, useful for some components such as oscillators - * that use the sample rate to calculate phase positions. - */ -struct Context { - size_t sampleRate; - size_t numChannels; - size_t blockSize; -}; - -/** - * The common default context, using a sample rate of 44.1kHz, stereo - * channel configuration, and a buffer size of 16 samples. - */ -static Context DEFAULT_CONTEXT = { 44100, 2, 16 }; -} diff --git a/src/audio/sample.h b/src/audio/sample.h deleted file mode 100644 index a777997..0000000 --- a/src/audio/sample.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -namespace neuron { - -/** - * The basic data type for DSP operations of - * an audio signal. - */ -using Sample = float; - -/** - * The minimum possible value for a sample. - */ -const Sample MIN = -1.0f; - -/** - * The maximum possible value for a sample. - */ -const Sample MAX = 1.0f; - -} diff --git a/src/audio/waveform.h b/src/audio/waveform.h deleted file mode 100644 index d06f195..0000000 --- a/src/audio/waveform.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "audio/sample.h" -#include "utilities/arithmetic.h" - -namespace neuron { - -/** - * The basic waveform variants, i.e. sine, triangle, - * sawtooth, and square. - */ -enum class Waveform { - SINE, - TRIANGLE, - SAWTOOTH, - SQUARE, -}; - -/** - * Converts the value of a sine wave to a basic waveform. - * - * @param sample The sample value to convert. - * @param waveform The waveform to convert the sample to. - * @return Sample - */ -inline Sample SineToWaveform(Sample sample, Waveform waveform) -{ - switch (waveform) { - default: - case Waveform::SINE: - return sample; - case Waveform::TRIANGLE: - return sample >= 0.0f ? (2.0f * sample - 1.0f) : (2.0f * -sample - 1.0f); - case Waveform::SAWTOOTH: - return (2.0f * PI * sample) - floorf((2.0f * PI * sample) + 0.5f); - case Waveform::SQUARE: - return sample >= 0.0f ? 1.0f : -1.0f; - } -} - -} \ No newline at end of file diff --git a/src/generators/oscillator.cpp b/src/dsp/generators/oscillator.cpp similarity index 55% rename from src/generators/oscillator.cpp rename to src/dsp/generators/oscillator.cpp index 426950e..882b001 100644 --- a/src/generators/oscillator.cpp +++ b/src/dsp/generators/oscillator.cpp @@ -1,10 +1,13 @@ -#include "generators/oscillator.h" +#include "neuron/dsp/generators/oscillator.h" + +#include using namespace neuron; Oscillator::Oscillator(Context& context, float frequency, Waveform waveform) : m_context(context) , m_waveform(waveform) + , p_frequency(frequency) { PopulateWavetable(); SetFrequency(frequency); @@ -15,7 +18,7 @@ Oscillator::~Oscillator() m_follower = nullptr; } -Sample Oscillator::Generate() +Sample Oscillator::GenerateImpl() { Sample value = Lerp(); @@ -24,9 +27,22 @@ Sample Oscillator::Generate() return SineToWaveform(value, m_waveform); } +#ifdef NEO_PLUGIN_SUPPORT +void Oscillator::AttachParameterToSourceImpl(OscillatorParameter parameter, std::atomic* source) +{ + switch (parameter) { + case OscillatorParameter::OSC_FREQUENCY: + p_frequency.AttachSource(source); + break; + default: + break; + } +} +#endif + void Oscillator::Reset(float phase) { - float clampedPhase = clamp(phase, 0.0f, (float)WAVETABLE_SIZE); + float clampedPhase = clamp(phase, 0.0f, static_cast(WAVETABLE_SIZE)); m_phase = clampedPhase; if (m_follower != nullptr) { m_follower->Reset(clampedPhase); @@ -35,7 +51,8 @@ void Oscillator::Reset(float phase) void Oscillator::SetFrequency(float frequency) { - m_phaseIncrement = frequency * (float)WAVETABLE_SIZE / (float)m_context.sampleRate; + p_frequency = frequency; + m_phaseIncrement = p_frequency * static_cast(WAVETABLE_SIZE) / static_cast(m_context.sampleRate); } void Oscillator::SetWaveform(Waveform waveform) @@ -45,7 +62,7 @@ void Oscillator::SetWaveform(Waveform waveform) void Oscillator::AttachFollower(Oscillator* follower) { - if (follower != nullptr) { + if (follower != nullptr && follower != this) { m_follower = follower; } } @@ -58,16 +75,16 @@ void Oscillator::DetachFollower() void Oscillator::PopulateWavetable() { for (size_t idx = 0; idx < WAVETABLE_SIZE; idx++) { - float phase = (float)idx * PI * 2.0f / (float)WAVETABLE_SIZE; - m_wavetable[idx] = (Sample)sin(phase); + float phase = static_cast(idx) * PI * 2.0f / static_cast(WAVETABLE_SIZE); + m_wavetable[idx] = sin(phase); } } void Oscillator::IncrementPhase() { m_phase += m_phaseIncrement; - if (m_phase >= (float)WAVETABLE_SIZE) { - m_phase -= (float)WAVETABLE_SIZE; + if (m_phase >= static_cast(WAVETABLE_SIZE)) { + m_phase -= static_cast(WAVETABLE_SIZE); if (m_follower != nullptr) { m_follower->Reset(m_phase); } @@ -76,9 +93,9 @@ void Oscillator::IncrementPhase() Sample Oscillator::Lerp() { - size_t truncatedIdx = (size_t)m_phase; + size_t truncatedIdx = m_phase; size_t nextIdx = (truncatedIdx + 1) % WAVETABLE_SIZE; - float nextIdxWeight = m_phase - (float)truncatedIdx; + float nextIdxWeight = m_phase - static_cast(truncatedIdx); float truncatedIdxWeight = 1.0f - nextIdxWeight; return (m_wavetable[truncatedIdx] * truncatedIdxWeight) + (m_wavetable[nextIdx] * nextIdxWeight); diff --git a/src/dsp/modulators/adsr.cpp b/src/dsp/modulators/adsr.cpp new file mode 100644 index 0000000..390d84c --- /dev/null +++ b/src/dsp/modulators/adsr.cpp @@ -0,0 +1,125 @@ +#include "neuron/dsp/modulators/adsr.h" + +using namespace neuron; + +AdsrEnvelopeModulator::AdsrEnvelopeModulator(Context& context, AdsrEnvelope envelope) + : m_context(context) + , p_attack(envelope.attack) + , p_decay(envelope.decay) + , p_sustain(envelope.sustain) + , p_release(envelope.release) +{ +} + +float AdsrEnvelopeModulator::ModulateImpl() +{ + float position = static_cast(m_samplesSinceLastStage) * (1000.0f / static_cast(m_context.sampleRate)); + + /** + * NOTE: The modulation value is calculated based on the current stage of the modulator. + * Based on the stage of the envelope, the calculation of the modulation value is made from a + * particular linear equation. The x position as input for this linear equation is calculated + * from the number of samples generated since the stage was last updated multiplied by the number + * of milliseconds per sample. If the x position is greater than the stage's duration, then the + * modulator is updated to the next stage. + */ + float value; + switch (m_stage) { + case AdsrStage::ATTACK: + value = position / p_attack; + Update(p_attack, AdsrStage::DECAY, true); + break; + case AdsrStage::DECAY: + value = (((p_sustain - 1.0f) / p_decay) * position) + 1.0f; + Update(p_decay, AdsrStage::SUSTAIN, true); + break; + case AdsrStage::SUSTAIN: + value = p_sustain; + break; + case AdsrStage::RELEASE: + value = ((-p_sustain / p_release) * position) + p_sustain; + Update(p_release, AdsrStage::IDLE, true); + break; + case AdsrStage::IDLE: + default: + value = 0.0f; + } + + return value; +} + +#ifdef NEO_PLUGIN_SUPPORT +void AdsrEnvelopeModulator::AttachParameterToSourceImpl(AdsrParameter parameter, std::atomic* source) +{ + switch (parameter) { + case AdsrParameter::ADSR_ATTACK: + p_attack.AttachSource(source); + break; + case AdsrParameter::ADSR_DECAY: + p_decay.AttachSource(source); + break; + case AdsrParameter::ADSR_SUSTAIN: + p_sustain.AttachSource(source); + break; + case AdsrParameter::ADSR_RELEASE: + p_release.AttachSource(source); + break; + default: + break; + } +} +#endif + +void AdsrEnvelopeModulator::Trigger() +{ + m_stage = AdsrStage::ATTACK; + m_samplesSinceLastStage = 0; +} + +void AdsrEnvelopeModulator::Release() +{ + m_stage = AdsrStage::RELEASE; + m_samplesSinceLastStage = 0; +} + +void AdsrEnvelopeModulator::Reset() +{ + m_stage = AdsrStage::IDLE; + m_samplesSinceLastStage = 0; +} + +void AdsrEnvelopeModulator::SetAttackTime(float attackTimeMs) +{ + p_attack = attackTimeMs; + Update(p_attack, AdsrStage::DECAY, false); +} + +void AdsrEnvelopeModulator::SetDecayTime(float decayTimeMs) +{ + p_decay = decayTimeMs; + Update(p_decay, AdsrStage::SUSTAIN, false); +} + +void AdsrEnvelopeModulator::SetSustainLevel(float sustainLevel) +{ + p_sustain = sustainLevel; +} + +void AdsrEnvelopeModulator::SetReleaseTime(float releaseTimeMs) +{ + p_release = releaseTimeMs; + Update(p_release, AdsrStage::IDLE, false); +} + +void AdsrEnvelopeModulator::Update(float stageDuration, AdsrStage nextStage, bool incrementSampleCount) +{ + if (incrementSampleCount) { + m_samplesSinceLastStage++; + } + + float msPerSample = 1000.0f / static_cast(m_context.sampleRate); + if (static_cast(m_samplesSinceLastStage) * msPerSample >= stageDuration) { + m_samplesSinceLastStage = 0; + m_stage = nextStage; + } +} diff --git a/src/dsp/processors/filter.cpp b/src/dsp/processors/filter.cpp new file mode 100644 index 0000000..ee37c99 --- /dev/null +++ b/src/dsp/processors/filter.cpp @@ -0,0 +1,45 @@ +#include "neuron/dsp/processors/filter.h" +#include "neuron/utils/arithmetic.h" + +using namespace neuron; + +Filter::Filter(Context& context, float cutoffFrequency) + : p_cutoffFrequency(cutoffFrequency) + , m_context(context) + , m_previousOutput(0.0f) +{ + SetCutoffFrequency(cutoffFrequency); +} + +Sample Filter::ProcessImpl(Sample input) +{ + float output = m_alpha * input + (1.0f - m_alpha) * m_previousOutput; + m_previousOutput = output; + return output; +} + +#ifdef NEO_PLUGIN_SUPPORT +void Filter::AttachParameterToSourceImpl(FilterParameter parameter, std::atomic* source) +{ + switch (parameter) { + case FilterParameter::FILTER_CUTOFF_FREQUENCY: + p_cutoffFrequency.AttachSource(source); + break; + default: + break; + } +} +#endif + +void Filter::SetCutoffFrequency(float frequency) +{ + p_cutoffFrequency = clamp(frequency, FILTER_CUTOFF_FREQ_MIN, FILTER_CUTOFF_FREQ_MAX); + CalculateAlpha(); +} + +void Filter::CalculateAlpha() +{ + float cutoffResponse = 1.0f / (2.0f * PI * p_cutoffFrequency); + float deltaTime = 1.0f / static_cast(m_context.sampleRate); + m_alpha = deltaTime / (cutoffResponse + deltaTime); +} diff --git a/src/dsp/processors/saturator.cpp b/src/dsp/processors/saturator.cpp new file mode 100644 index 0000000..57ee3c1 --- /dev/null +++ b/src/dsp/processors/saturator.cpp @@ -0,0 +1,46 @@ +#include "neuron/dsp/processors/saturator.h" +#include "neuron/utils/arithmetic.h" + +using namespace neuron; + +Saturator::Saturator() + : p_saturation(1.0f) + , p_symmetry(1.0f) +{ +} + +Sample Saturator::ProcessImpl(Sample input) +{ + float output = tanh(input * p_saturation); + if (input < 0.0f) { + output = (input * (1.0f - p_symmetry)) + (output * p_symmetry); + } + + return clamp(output, -1.0f, 1.0f); +} + +#ifdef NEO_PLUGIN_SUPPORT +void Saturator::AttachParameterToSourceImpl(SaturatorParameter parameter, std::atomic* source) +{ + switch (parameter) { + case SaturatorParameter::SATURATOR_SATURATION: + p_saturation.AttachSource(source); + break; + case SaturatorParameter::SATURATOR_SYMMETRY: + p_symmetry.AttachSource(source); + break; + default: + break; + } +} +#endif + +void Saturator::SetSaturation(float saturation) +{ + p_saturation = saturation < 1.0f ? 1.0f : saturation; +} + +void Saturator::SetSymmetry(float symmetry) +{ + p_symmetry = clamp(symmetry, 0.0f, 1.0f); +} diff --git a/src/dsp/processors/wavefolder.cpp b/src/dsp/processors/wavefolder.cpp new file mode 100644 index 0000000..88ae1d7 --- /dev/null +++ b/src/dsp/processors/wavefolder.cpp @@ -0,0 +1,63 @@ +#include "neuron/dsp/processors/wavefolder.h" +#include "neuron/utils/arithmetic.h" + +using namespace neuron; + +Wavefolder::Wavefolder() + : p_inputGain(1.0f) + , p_threshold(1.0f) + , p_symmetry(1.0f) +{ +} + +Sample Wavefolder::ProcessImpl(Sample input) +{ + float output = input * p_inputGain; + while (output > p_threshold || output < -p_threshold) { + if (output > p_threshold) { + output = p_threshold - (output - p_threshold); + } else if (output < -p_threshold) { + output = -p_threshold - (output + p_threshold); + } + } + + if (input < 0.0f) { + output = input * (1.0f - p_symmetry) + output * p_symmetry; + } + + return clamp(output, -1.0f, 1.0f); +} + +#ifdef NEO_PLUGIN_SUPPORT +void Wavefolder::AttachParameterToSourceImpl(const WavefolderParameter parameter, std::atomic* source) +{ + switch (parameter) { + case WavefolderParameter::WAVEFOLDER_INPUT_GAIN: + p_inputGain.AttachSource(source); + break; + case WavefolderParameter::WAVEFOLDER_THRESHOLD: + p_threshold.AttachSource(source); + break; + case WavefolderParameter::WAVEFOLDER_SYMMETRY: + p_symmetry.AttachSource(source); + break; + default: + break; + } +} +#endif + +void Wavefolder::SetInputGain(float gain) +{ + p_inputGain = gain; +} + +void Wavefolder::SetThreshold(float threshold) +{ + p_threshold = threshold; +} + +void Wavefolder::SetSymmetry(float symmetry) +{ + p_symmetry = clamp(symmetry, 0.0f, 1.0f); +} diff --git a/src/generators/oscillator.h b/src/generators/oscillator.h deleted file mode 100644 index 8569e5b..0000000 --- a/src/generators/oscillator.h +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once - -#include - -#include "audio/context.h" -#include "audio/sample.h" -#include "audio/waveform.h" -#include "utilities/arithmetic.h" - -namespace neuron { - -const size_t WAVETABLE_SIZE = 256; - -/** - * The Oscillator class creates an audio signal - * with a basic waveform. - */ -class Oscillator { -public: - /** - * Creates an oscillator generator. - * - * @param context The DSP context to be used by the oscillator. - * @param frequency The initial frequency of the oscillator. - * @return Oscillator - */ - Oscillator(Context& context = DEFAULT_CONTEXT, float frequency = 440.0f, Waveform waveform = Waveform::SINE); - - /** - * Frees any memory allocated by the oscillator. - */ - ~Oscillator(); - - /** - * Generates a sample of an audio signal with a - * basic waveform. - * - * @return Sample - */ - Sample Generate(); - - /** - * Resets the phase of the oscillator, starting it at the beginning - * waveform position. - * - * @param - */ - void Reset(float phase = 0.0f); - - /** - * Sets the frequency of the oscillator. - * - * @param frequency The new oscillator output frequency. - */ - void SetFrequency(float frequency); - - /** - * Sets the waveform of the oscillator. - * - * @param waveform The new oscillator output waveform. - */ - void SetWaveform(Waveform waveform); - - /** - * Attaches a follower oscillator to be synced to this one. - * - * @param oscillator The oscillator that will be synced to this one. - */ - void AttachFollower(Oscillator* oscillator); - - /** - * Detaches the follower oscillator from this one. - */ - void DetachFollower(); - -private: - void PopulateWavetable(); - void IncrementPhase(); - Sample Lerp(); - - Context& m_context; - Sample m_wavetable[WAVETABLE_SIZE]; - Waveform m_waveform; - float m_phase = 0.0f; - float m_phaseIncrement = 0.0f; - - Oscillator* m_follower = nullptr; -}; - -} diff --git a/src/modulators/adsr.cpp b/src/modulators/adsr.cpp deleted file mode 100644 index 1b1e42d..0000000 --- a/src/modulators/adsr.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "modulators/adsr.h" - -using namespace neuron; - -AdsrEnvelopeModulator::AdsrEnvelopeModulator(Context& context, AdsrEnvelope envelope) - : m_context(context) - , m_envelope(envelope) -{ -} - -AdsrEnvelopeModulator::~AdsrEnvelopeModulator() -{ -} - -float AdsrEnvelopeModulator::Modulate() -{ - float position = (float)m_samplesSinceLastStage * (1000.0f / m_context.sampleRate); - - /** - * NOTE: The modulation value is calculated based on the current stage of the modulator. - * Based on the stage of the envelope, the calculation of the modulation value is made from a - * particular linear equation. The x position as input for this linear equation is calculated - * from the number of samples generated since the stage was last updated multiplied by the number - * of milliseconds per sample. If the x position is greater than the stage's duration, then the - * modulator is updated to the next stage. - */ - float value; - switch (m_stage) { - case AdsrStage::ATTACK: - value = position / m_envelope.attack; - Update(m_envelope.attack, AdsrStage::DECAY, true); - break; - case AdsrStage::DECAY: - value = (((m_envelope.sustain - 1.0f) / m_envelope.decay) * position) + 1.0f; - Update(m_envelope.decay, AdsrStage::SUSTAIN, true); - break; - case AdsrStage::SUSTAIN: - value = m_envelope.sustain; - break; - case AdsrStage::RELEASE: - value = ((-m_envelope.sustain / m_envelope.release) * position) + m_envelope.sustain; - Update(m_envelope.release, AdsrStage::IDLE, true); - break; - case AdsrStage::IDLE: - default: - value = 0.0f; - } - - return value; -} - -void AdsrEnvelopeModulator::Trigger() -{ - m_stage = AdsrStage::ATTACK; - m_samplesSinceLastStage = 0; -} - -void AdsrEnvelopeModulator::Release() -{ - m_stage = AdsrStage::RELEASE; - m_samplesSinceLastStage = 0; -} - -void AdsrEnvelopeModulator::Reset() -{ - m_stage = AdsrStage::IDLE; - m_samplesSinceLastStage = 0; -} - -void AdsrEnvelopeModulator::SetAttackTime(float attackTimeMs) -{ - m_envelope.attack = attackTimeMs; - Update(m_envelope.attack, AdsrStage::DECAY, false); -} - -void AdsrEnvelopeModulator::SetDecayTime(float decayTimeMs) -{ - m_envelope.decay = decayTimeMs; - Update(m_envelope.decay, AdsrStage::SUSTAIN, false); -} - -void AdsrEnvelopeModulator::SetSustainLevel(float sustainLevel) -{ - m_envelope.sustain = sustainLevel; -} - -void AdsrEnvelopeModulator::SetReleaseTime(float releaseTimeMs) -{ - m_envelope.release = releaseTimeMs; - Update(m_envelope.release, AdsrStage::IDLE, false); -} - -void AdsrEnvelopeModulator::Update(float stageDuration, AdsrStage nextStage, bool incrementSampleCount) -{ - if (incrementSampleCount) { - m_samplesSinceLastStage++; - } - - float msPerSample = 1000.0f / (float)m_context.sampleRate; - if ((float)m_samplesSinceLastStage * msPerSample >= stageDuration) { - m_samplesSinceLastStage = 0; - m_stage = nextStage; - } -} diff --git a/src/modulators/adsr.h b/src/modulators/adsr.h deleted file mode 100644 index cd331f4..0000000 --- a/src/modulators/adsr.h +++ /dev/null @@ -1,112 +0,0 @@ -#pragma once - -#include "audio/context.h" - -namespace neuron { - -/** - * The configuration of an ADSR envelope curve including durations - * for the attack, decay, and release stages as well as a sustain level. - */ -struct AdsrEnvelope { - float attack; - float decay; - float sustain; - float release; -}; - -/** - * The stages of an ADSR envelope, including an "idle" stage when not in use. - */ -enum class AdsrStage { - IDLE, - ATTACK, - DECAY, - SUSTAIN, - RELEASE -}; - -const AdsrEnvelope DEFAULT_ADSR_ENVELOPE = { - 100.0f, 200.0f, 1.0f, 1000.0f -}; - -/** - * The ADsrEnvelopeModulator class is a modulation source - * that is based off of an ADSR envelope generator. - */ -class AdsrEnvelopeModulator { -public: - /** - * Creates an ADSR envelope modulator. - * - * @param context The DSP context to be used by the envelope. - * @param envelope The envelope configuration to initialize the class with. - * @return AdsrEnvelopeModulator - */ - AdsrEnvelopeModulator(Context& context = DEFAULT_CONTEXT, AdsrEnvelope envelope = DEFAULT_ADSR_ENVELOPE); - - /** - * Frees any memory allocated by the modulator. - */ - ~AdsrEnvelopeModulator(); - - /** - * Calculates a modulation value to apply to some arbitrary variable. - */ - float Modulate(); - - /** - * Starts the envelope from its attack phase. - */ - void Trigger(); - - /** - * Starts the envelope from its release phase. - */ - void Release(); - - /** - * Re-initializes the modulator, ready to be re-triggered. - */ - void Reset(); - - /** - * Sets the attack time of the envelope. - * - * @param attackTimeMs The new attack time for the envelope. - */ - void SetAttackTime(float attackTimeMs); - - /** - * Sets the decay time of the envelope. - * - * @param decayTimeMs The new decay time for the envelope. - */ - void SetDecayTime(float decayTimeMs); - - /** - * Sets the sustain level of the envelope. - * - * @param sustainLevel The new sustain level for the envelope. - */ - void SetSustainLevel(float sustainLevel); - - /** - * Sets the release time of the envelope. - * - * @param releaseTimeMs The new release time for the envelope. - */ - void SetReleaseTime(float releaseTimeMs); - -private: - // Checks and updates the modulator's state if necessary - void Update(float stageDuration, AdsrStage nextStage, bool incrementSampleCount); - - Context& m_context; - - AdsrEnvelope m_envelope; - AdsrStage m_stage = AdsrStage::IDLE; - size_t m_samplesSinceLastStage = 0; -}; - -} diff --git a/src/neuron.h b/src/neuron.h deleted file mode 100644 index 603925e..0000000 --- a/src/neuron.h +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Neuron is a lightweight audio DSP library intended for use - * in any relevant application e.g. Electrosmith Daisy patches, - * JUCE plugins, VCV Rack modules. - * - * Author: Matthew Maxwell, 2024 - */ -#ifndef NEURON_LIB_H -#define NEURON_LIB_H - -// AUDIO -#include "audio/context.h" -#include "audio/sample.h" -#include "audio/waveform.h" - -// GENERATORS -#include "generators/oscillator.h" - -// MODULATORS -#include "modulators/adsr.h" - -// PROCESSORS -#include "processors/effects/saturator.h" -#include "processors/effects/wavefolder.h" -#include "processors/filters/filter.h" - -// UTILITIES -#include "utilities/arithmetic.h" -#include "utilities/logger.h" -#include "utilities/midi.h" -#include "utilities/timer.h" - -#endif diff --git a/src/processors/effects/saturator.cpp b/src/processors/effects/saturator.cpp deleted file mode 100644 index 8c6d8b3..0000000 --- a/src/processors/effects/saturator.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "processors/effects/saturator.h" - -using namespace neuron; - -Sample Saturator::Process(const Sample input) -{ - float output = tanh((float)input * m_saturation); - if (input < 0.0f) { - output = (Sample)(input * (1.0f - m_symmetry)) + (output * m_symmetry); - } - - return (Sample)clamp(output, -1.0f, 1.0f); -} - -void Saturator::SetSaturation(float saturation) -{ - m_saturation = saturation < 1.0f ? 1.0f : saturation; -} - -void Saturator::SetSymmetry(float symmetry) -{ - m_symmetry = clamp(symmetry, 0.0f, 1.0f); -} diff --git a/src/processors/effects/saturator.h b/src/processors/effects/saturator.h deleted file mode 100644 index 2d6ab32..0000000 --- a/src/processors/effects/saturator.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include "audio/sample.h" -#include "utilities/arithmetic.h" - -namespace neuron { - -/** - * The Saturator class applies a tape saturation - * algorithm to audio signals. - */ -class Saturator { -public: - /** - * Creates a default saturator processor. - * - * @return Saturator - */ - Saturator() {}; - - /** - * Frees any memory allocated by the saturator. - */ - ~Saturator() {}; - - /** - * Applies a saturation algorithm to an input sample. - * - * @param input The input sample to be processed. - * @return Sample - */ - Sample Process(const Sample input); - - /** - * Sets the saturation level, which boosts the signal before - * distortion is applied. This multiplier will always be greater than - * one. - * - * @param saturation The multiplier of the audio signal going into the - * distortion algorithm. - */ - void SetSaturation(float saturation); - - /** - * Sets the symmetry of the algorithm, determining how much - * distortion to apply to the positive and negative parts - * of the signal separately. - * - * @param symmetry A value between 0.0 and 1.0, ranging from asymmetrical - * (one-sided) to symmetrical respectively. - */ - void SetSymmetry(float symmetry); - -private: - float m_saturation = 1.0f; - float m_symmetry = 1.0f; -}; - -} \ No newline at end of file diff --git a/src/processors/effects/wavefolder.cpp b/src/processors/effects/wavefolder.cpp deleted file mode 100644 index cba4f22..0000000 --- a/src/processors/effects/wavefolder.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "processors/effects/wavefolder.h" - -using namespace neuron; - -Sample Wavefolder::Process(const Sample input) -{ - float output = (float)input * m_inputGain; - while (output > m_threshold || output < -m_threshold) { - if (output > m_threshold) { - output = m_threshold - (output - m_threshold); - } else if (output < -m_threshold) { - output = -m_threshold - (output + m_threshold); - } - } - - if (input < 0.0f) { - output = (Sample)(input * (1.0f - m_symmetry)) + (output * m_symmetry); - } - - return (Sample)clamp(output, -1.0f, 1.0f); -} - -void Wavefolder::SetInputGain(float gain) -{ - m_inputGain = gain; -} - -void Wavefolder::SetThreshold(float threshold) -{ - m_threshold = threshold; -} - -void Wavefolder::SetSymmetry(float symmetry) -{ - m_symmetry = clamp(symmetry, 0.0f, 1.0f); -} diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h deleted file mode 100644 index b13df38..0000000 --- a/src/processors/effects/wavefolder.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include "audio/sample.h" -#include "utilities/arithmetic.h" - -namespace neuron { - -/** - * The Wavefolder class applies a wavefolding - * algorithm to audio signals. - */ -class Wavefolder { -public: - /** - * Creates a default wavefolder processor. - */ - Wavefolder() {}; - - /** - * Frees any memory allocated by the wavefolder. - */ - ~Wavefolder() {}; - - /** - * Applies a wavefolding algorithm to an input sample. - * - * @param input The input sample to be processed. - * @return Sample - */ - Sample Process(const Sample input); - - /** - * Sets the input gain level, which boosts the signal before - * being measured against the wavefolder threshold. - * - * @param gain The multiplier of the audio signal going into the - * wavefolding algorithm. - */ - void SetInputGain(float gain); - - /** - * Sets the threshold of the wavefolder, above which samples will - * be "folded" toawrds zero until they are within the threshold. - */ - void SetThreshold(float threshold); - - /** - * Sets the symmetry of the algorithm, determining how much - * wavefolding to apply to the positive and negative parts - * of the signal separately. - * - * @param symmetry A value between 0.0 and 1.0, ranging from asymmetrical - * (one-sided) to symmetrical respectively. - */ - void SetSymmetry(float symmetry); - -private: - float m_inputGain = 1.0f; - float m_threshold = 1.0f; - float m_symmetry = 1.0f; -}; - -} \ No newline at end of file diff --git a/src/processors/filters/filter.cpp b/src/processors/filters/filter.cpp deleted file mode 100644 index a778b0b..0000000 --- a/src/processors/filters/filter.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "processors/filters/filter.h" - -using namespace neuron; - -Filter::Filter(neuron::Context& context, float cutoffFrequency) - : m_context(context) - , m_cutoffFrequency(cutoffFrequency) - , m_previousOutput(0.0f) -{ - SetCutoffFrequency(cutoffFrequency); -} - -Sample Filter::Process(const neuron::Sample input) -{ - float output = m_alpha * (float)input + (1.0f - m_alpha) * (float)m_previousOutput; - m_previousOutput = (Sample)output; - return (Sample)output; -} - -void Filter::SetCutoffFrequency(float frequency) -{ - m_cutoffFrequency = clamp(frequency, FILTER_CUTOFF_FREQ_MIN, FILTER_CUTOFF_FREQ_MAX); - CalculateAlpha(); -} - -void Filter::CalculateAlpha() -{ - float cutoffResponse = 1.0f / (2.0f * PI * m_cutoffFrequency); - float deltaTime = 1.0f / (float)m_context.sampleRate; - m_alpha = deltaTime / (cutoffResponse + deltaTime); -} diff --git a/src/processors/filters/filter.h b/src/processors/filters/filter.h deleted file mode 100644 index f6a8eac..0000000 --- a/src/processors/filters/filter.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include "audio/context.h" -#include "audio/sample.h" -#include "utilities/arithmetic.h" - -namespace neuron { -const float FILTER_CUTOFF_FREQ_MIN = 20.0f; -const float FILTER_CUTOFF_FREQ_MAX = 20000.0f; - -/** - * The Filter class applies a simple low-pass filter - * to audio signals. - */ -class Filter { -public: - /** - * Creates a filter processor. - * - * @param context The DSP context to be used by the filter. - * @param cutoffFrequency The initial cutoff frequency of the filter. - * @return Filter - */ - Filter(Context& context = DEFAULT_CONTEXT, - float cutoffFrequency = FILTER_CUTOFF_FREQ_MAX); - - /** - * Frees any memory allocated by the oscillator. - */ - ~Filter() { } - - /** - * Applies a low-pass filter to an input sample. - * - * @param input The input sample to be processed. - * @return Sample - */ - Sample Process(const Sample input); - - /** - * Sets the filter's cutoff frequency. - * - * @param frequency The new cutoff frequency. - */ - void SetCutoffFrequency(float frequency); - -private: - void CalculateAlpha(); - - Context& m_context; - - float m_cutoffFrequency; - float m_alpha; - Sample m_previousOutput; -}; - -} diff --git a/src/utilities/arithmetic.h b/src/utilities/arithmetic.h deleted file mode 100644 index 6d382c0..0000000 --- a/src/utilities/arithmetic.h +++ /dev/null @@ -1,114 +0,0 @@ -#pragma once - -#include -#include - -namespace neuron { - -/** - * A value for the absence of quantity; nothing. - */ -const float ZERO = 0.0f; - -/** - * A value that leaves any other value unchanged - * when multiplied with it. - */ -const float IDENTITY = 1.0f; - -/** - * An irrational number that represents the ratio of the circumference of a - * circle to its diameter. - */ -const float PI = 3.14159265358979323846264338327950288f; - -/** - * An irrational number that is the base of the natural logarithm. - */ -const float EULER = 2.71828182845904523536028747135266250; - -/** - * Depicts different mathematical curves, e.g. exponential, - * linear, logarithmic. - */ -enum class Mapping { - EXP, - LOG, - LINEAR, -}; - -/** - * Constricts a number between a lower and upper bound. - * - * @param n The number to the be clamped. - * @param min The lowest possible number n can be. - * @param max The highest possible number n can be. - * @return T - */ -template -inline T clamp(const T n, const T min, const T max) -{ - return std::max(min, std::min(n, max)); -} - -/** - * Scales a number between 0 and 1 to a number within a range from min to max. - * - * @param n The number to scale. - * @param min The lower bound of the range to scale n. - * @param max The upper bound of the range to scale n. - * @param curve The curve in which to scale n (e.g. linear, exponential, - * logarithmic) - * @return T - */ -template -inline T map(const T n, const T min, const T max, - const Mapping curve = Mapping::LINEAR) -{ - switch (curve) { - case Mapping::EXP: - return clamp(min + (n * n) * (max - min), min, max); - case Mapping::LOG: { - const float a = 1.f / log10f(max / min); - return clamp(min * powf(10, n / a), min, max); - } - case Mapping::LINEAR: - default: - return clamp(min + n * (max - min), min, max); - } -} - -/** - * A fast approximation of the hyperbolic tangent function, or `tanh`. - * - * NOTE: This function works best on a limited range from -5 to 5. - * - * @param n The function input. - * @return - */ -template -inline T tanh(T n) -{ - auto n2 = n * n; - auto numerator = n * (135135 + n2 * (17325 + n2 * (378 + n2))); - auto denominator = 135135 + n2 * (62370 + n2 * (3150 + 28 * n2)); - return numerator / denominator; -} - -/** - * A fast approximation of the exponential function, or `exp`. - * - * NOTE: This function works best on a limited range from -6 to 4. - * - * @param n The function input. - * @return - */ -template -inline T exp(T n) -{ - auto numerator = 1680 + n * (840 + n * (180 + n * (20 + n))); - auto denominator = 1680 + n * (-840 + n * (180 + n * (-20 + n))); - return numerator / denominator; -} - -} diff --git a/src/utilities/logger.cpp b/src/utilities/logger.cpp deleted file mode 100644 index 6c565bf..0000000 --- a/src/utilities/logger.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "logger.h" - -#include -#include - -using namespace neuron; - -void Logger::Log(std::string str) -{ - std::time_t now = std::time(nullptr); - std::tm* localTime = std::localtime(&now); - char buffer[80]; - std::strftime(buffer, sizeof(buffer), "Y-%m-%d %H:%M:%S", localTime); - - std::cout << "[" << buffer << "] " - << "NEURON: " << str << std::endl; -} diff --git a/src/utilities/logger.h b/src/utilities/logger.h deleted file mode 100644 index 77c6a63..0000000 --- a/src/utilities/logger.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -namespace neuron { - -class Logger { -public: - static void Log(std::string str); -}; - -} - -#define NEO_TRACE(...) ::neuron::Logger::Log(__VA_ARGS__) -#define NEO_INFO(...) ::neuron::Logger::Log(__VA_ARGS__) -#define NEO_WARN(...) ::neuron::Logger::Log(__VA_ARGS__) -#define NEO_ERROR(...) ::neuron::Logger::Log(__VA_ARGS__) diff --git a/src/utilities/midi.h b/src/utilities/midi.h deleted file mode 100644 index e2cf2f5..0000000 --- a/src/utilities/midi.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -#include "utilities/arithmetic.h" - -namespace neuron { - -/** - * Converts a MIDI note number to a (floating-point) frequency. Any value outside - * of the proper note range [0, 127] will be clamped. - * - * @param n The MIDI note number to convert - * @return - */ -template -inline T midi_to_frequency(T n) -{ - return powf(2, (clamp(n, 0.0f, 127.0f) - 69.0f) / 12.0f) * 440.0f; -} - -} diff --git a/src/utilities/timer.h b/src/utilities/timer.h deleted file mode 100644 index 01b2495..0000000 --- a/src/utilities/timer.h +++ /dev/null @@ -1,34 +0,0 @@ -#include - -#include "utilities/logger.h" - -namespace neuron { - -class Timer { -public: - Timer() - { - m_startTimepoint = std::chrono::high_resolution_clock::now(); - } - - ~Timer() - { - Stop(); - } - - void Stop() - { - auto endTimepoint = std::chrono::high_resolution_clock::now(); - - auto start = std::chrono::time_point_cast(m_startTimepoint).time_since_epoch().count(); - auto end = std::chrono::time_point_cast(endTimepoint).time_since_epoch().count(); - - auto durationMs = (end - start) * 0.001f; - NEO_INFO(std::to_string(durationMs) + "ms"); - } - -private: - std::chrono::high_resolution_clock::time_point m_startTimepoint; -}; - -} \ No newline at end of file diff --git a/tests/core/parameter_test.cpp b/tests/core/parameter_test.cpp new file mode 100644 index 0000000..834f3c9 --- /dev/null +++ b/tests/core/parameter_test.cpp @@ -0,0 +1,41 @@ +#include "neuron/core/parameter.h" + +#include + +using namespace neuron; + +TEST(parameter_suite, initialization_test) +{ + Parameter param(1.0f); + + EXPECT_FLOAT_EQ(static_cast(param), 1.0f); +} + +TEST(parameter_suite, arithmetic_operations_test) +{ + Parameter param(5.0f); + + EXPECT_FLOAT_EQ(param + 3.0f, 8.0f); + + EXPECT_FLOAT_EQ(param - 2.0f, 3.0f); + + EXPECT_FLOAT_EQ(param * 2.0f, 10.0f); + + EXPECT_FLOAT_EQ(param / 2.0f, 2.5f); +} + +TEST(parameter_suite, division_by_zero_test) +{ + Parameter param(5.0f); + + EXPECT_FLOAT_EQ(param / 0.0f, 0.0f); +} + +TEST(parameter_suite, operator_assignment_test) +{ + Parameter param(5.0f); + + param = 3.0f; + + EXPECT_FLOAT_EQ(static_cast(param), 3.0f); +} diff --git a/tests/core/parameter_test_atomic.cpp b/tests/core/parameter_test_atomic.cpp new file mode 100644 index 0000000..2836867 --- /dev/null +++ b/tests/core/parameter_test_atomic.cpp @@ -0,0 +1,54 @@ +#include "neuron/core/parameter.h" + +#include +#include +#include + +using namespace neuron; + +TEST(parameter_suite, attach_source_test) +{ + std::atomic externalSource(2.0f); + Parameter param(1.0f); + + param.AttachSource(&externalSource); + + EXPECT_FLOAT_EQ(static_cast(param), 2.0f); + + externalSource.store(3.5f); + EXPECT_FLOAT_EQ(static_cast(param), 3.5f); +} + +TEST(parameter_suite, attach_and_assignment_test) +{ + Parameter param(5.0f); + + param = 3.0f; + + EXPECT_THAT(static_cast(param), testing::Not(testing::FloatEq(5.0f))); +} + +TEST(parameter_suite, attach_source_and_arithmetic_test) +{ + std::atomic externalSource(10.0f); + Parameter param(5.0f); + + param.AttachSource(&externalSource); + + EXPECT_FLOAT_EQ(param + 5.0f, 15.0f); + EXPECT_FLOAT_EQ(param - 2.0f, 8.0f); + EXPECT_FLOAT_EQ(param * 2.0f, 20.0f); + EXPECT_FLOAT_EQ(param / 2.0f, 5.0f); +} + +TEST(parameter_suite, destructor_test) +{ + std::atomic* externalSource = new std::atomic(10.0f); + Parameter* param = new Parameter(externalSource); + + delete param; + + EXPECT_FLOAT_EQ(externalSource->load(), 10.0f); + + delete externalSource; +} diff --git a/tests/generators/oscillator_test.cpp b/tests/dsp/generators/oscillator_test.cpp similarity index 90% rename from tests/generators/oscillator_test.cpp rename to tests/dsp/generators/oscillator_test.cpp index a6fa43f..521a381 100644 --- a/tests/generators/oscillator_test.cpp +++ b/tests/dsp/generators/oscillator_test.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/dsp/generators/oscillator.h" -#include "neuron.h" +#include using namespace neuron; @@ -34,7 +34,7 @@ TEST(oscillator_suite, reset_test) EXPECT_FLOAT_EQ(osc.Generate(), 0.0f); EXPECT_FLOAT_EQ(osc.Generate(), 0.06264372f); - osc.Reset((float)WAVETABLE_SIZE / 2.0f); + osc.Reset(static_cast(WAVETABLE_SIZE) / 2.0f); EXPECT_NEAR(osc.Generate(), 0.0f, 1e-5f); EXPECT_NEAR(osc.Generate(), -0.06264372f, 1e-5f); } @@ -55,13 +55,10 @@ TEST(oscillator_suite, oscillator_sync) EXPECT_NEAR(leader.Generate(), 0.00783538f, 1e-5f); EXPECT_NEAR(follower.Generate(), 0.01174026f, 1e-5f); - { - Timer t; - int numSamples = context.sampleRate; - while (numSamples--) { - leader.Generate(); - follower.Generate(); - } + int numSamples = context.sampleRate; + while (numSamples--) { + leader.Generate(); + follower.Generate(); } EXPECT_NE(leader.Generate(), follower.Generate()); diff --git a/tests/modulators/adsr_test.cpp b/tests/dsp/modulators/adsr_test.cpp similarity index 98% rename from tests/modulators/adsr_test.cpp rename to tests/dsp/modulators/adsr_test.cpp index 9c29c99..89d86b0 100644 --- a/tests/modulators/adsr_test.cpp +++ b/tests/dsp/modulators/adsr_test.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/dsp/modulators/adsr.h" -#include "neuron.h" +#include using namespace neuron; diff --git a/tests/processors/filters/filter_test.cpp b/tests/dsp/processors/filter_test.cpp similarity index 80% rename from tests/processors/filters/filter_test.cpp rename to tests/dsp/processors/filter_test.cpp index 90289e9..cec58d1 100644 --- a/tests/processors/filters/filter_test.cpp +++ b/tests/dsp/processors/filter_test.cpp @@ -1,6 +1,7 @@ -#include +#include "neuron/dsp/generators/oscillator.h" +#include "neuron/dsp/processors/filter.h" -#include "neuron.h" +#include using namespace neuron; diff --git a/tests/processors/effects/saturator_test.cpp b/tests/dsp/processors/saturator_test.cpp similarity index 96% rename from tests/processors/effects/saturator_test.cpp rename to tests/dsp/processors/saturator_test.cpp index e25e2e1..68315bb 100644 --- a/tests/processors/effects/saturator_test.cpp +++ b/tests/dsp/processors/saturator_test.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/dsp/processors/saturator.h" -#include "neuron.h" +#include using namespace neuron; diff --git a/tests/processors/effects/wavefolder_test.cpp b/tests/dsp/processors/wavefolder_test.cpp similarity index 96% rename from tests/processors/effects/wavefolder_test.cpp rename to tests/dsp/processors/wavefolder_test.cpp index e0682a7..4497598 100644 --- a/tests/processors/effects/wavefolder_test.cpp +++ b/tests/dsp/processors/wavefolder_test.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/dsp/processors/wavefolder.h" -#include "neuron.h" +#include using namespace neuron; diff --git a/tests/utilities/arithmetic_test.cpp b/tests/utilities/arithmetic_test.cpp deleted file mode 100644 index ad8aa96..0000000 --- a/tests/utilities/arithmetic_test.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include - -#include "neuron.h" - -using namespace neuron; - -TEST(arithmetic_suite, clamp_test) -{ - EXPECT_EQ(0.0f, clamp(0.0f, -1.0f, 1.0f)); - EXPECT_EQ(1.0f, clamp(2.0f, -1.0f, 1.0f)); - EXPECT_EQ(-1.0f, clamp(-2.0f, -1.0f, 1.0f)); - - EXPECT_EQ(1, clamp(1, -1, 1)); - EXPECT_EQ(-1, clamp(-1, -1, 1)); - - EXPECT_EQ(1, clamp(1, 1, 1)); - EXPECT_EQ(1, clamp(0, 1, -1)); -} - -TEST(arithmetic_suite, map_test) -{ - EXPECT_EQ(5.5f, map(0.5f, 1.0f, 10.0f)); - - EXPECT_EQ(0.25f, map(0.5f, 0.0f, 1.0f, Mapping::EXP)); - EXPECT_EQ(3.1622777f, map(0.5f, 1.0f, 10.0f, Mapping::LOG)); - EXPECT_EQ(0.5f, map(0.5f, 0.0f, 1.0f, Mapping::LINEAR)); - - EXPECT_EQ(1, map(0, 1, 100, Mapping::LINEAR)); - EXPECT_EQ(100, map(1, 1, 100, Mapping::LINEAR)); - - EXPECT_EQ(1.0f, map(0.0f, 1.0f, 1.0f)); - - EXPECT_EQ(0.0f, map(0.5f, 0.0f, 10.0f, Mapping::LOG)); -} - -TEST(arithmetic_suite, tanh_test) -{ - EXPECT_NEAR(tanh(0.0f), 0.0f, 1e-5f); - - EXPECT_NEAR(tanh(1.0f), std::tanh(1.0f), 1e-1f); - EXPECT_NEAR(tanh(-1.0f), std::tanh(-1.0f), 1e-1f); - - EXPECT_NEAR(tanh(0.5f), std::tanh(0.5f), 1e-1f); - EXPECT_NEAR(tanh(-0.5f), std::tanh(-0.5f), 1e-1f); - - EXPECT_NEAR(tanh(0.001f), std::tanh(0.001f), 1e-5f); -} - -TEST(arithmetic_suite, exp_test) -{ - EXPECT_NEAR(exp(0.5f), std::exp(0.5f), 1e-5f); - EXPECT_NEAR(exp(-0.5f), std::exp(-0.5f), 1e-5f); - - EXPECT_NEAR(exp(0.0f), 1.0f, 1e-5f); - - EXPECT_NEAR(exp(4.0f), std::exp(4.0f), 1.0f); - EXPECT_NEAR(exp(-6.0f), std::exp(-6.0f), 1e-2f); - - EXPECT_NEAR(exp(1e-6), std::exp(1e-6f), 1e-5f); -} diff --git a/tests/utils/arithmetic_test.cpp b/tests/utils/arithmetic_test.cpp new file mode 100644 index 0000000..0b2f887 --- /dev/null +++ b/tests/utils/arithmetic_test.cpp @@ -0,0 +1,164 @@ +#include "neuron/utils/arithmetic.h" + +#include + +using namespace neuron; + +TEST(arithmetic_suite, clamp_test) +{ + EXPECT_EQ(0.0f, clamp(0.0f, -1.0f, 1.0f)); + EXPECT_EQ(1.0f, clamp(2.0f, -1.0f, 1.0f)); + EXPECT_EQ(-1.0f, clamp(-2.0f, -1.0f, 1.0f)); + + EXPECT_EQ(1, clamp(1, -1, 1)); + EXPECT_EQ(-1, clamp(-1, -1, 1)); + + EXPECT_EQ(1, clamp(1, 1, 1)); + EXPECT_EQ(1, clamp(0, 1, -1)); +} + +TEST(arithmetic_suite, map_test) +{ + EXPECT_EQ(5.5f, map(0.5f, 1.0f, 10.0f)); + + EXPECT_EQ(0.25f, map(0.5f, 0.0f, 1.0f, Mapping::EXP)); + EXPECT_EQ(3.1622777f, map(0.5f, 1.0f, 10.0f, Mapping::LOG)); + EXPECT_EQ(0.5f, map(0.5f, 0.0f, 1.0f, Mapping::LINEAR)); + + EXPECT_EQ(1, map(0, 1, 100, Mapping::LINEAR)); + EXPECT_EQ(100, map(1, 1, 100, Mapping::LINEAR)); + + EXPECT_EQ(1.0f, map(0.0f, 1.0f, 1.0f)); + + EXPECT_EQ(0.0f, map(0.5f, 0.0f, 10.0f, Mapping::LOG)); +} + +TEST(arithmetic_suite, tanh_test) +{ + EXPECT_NEAR(tanh(0.0f), 0.0f, 1e-5f); + + EXPECT_NEAR(tanh(1.0f), std::tanh(1.0f), 1e-1f); + EXPECT_NEAR(tanh(-1.0f), std::tanh(-1.0f), 1e-1f); + + EXPECT_NEAR(tanh(0.5f), std::tanh(0.5f), 1e-1f); + EXPECT_NEAR(tanh(-0.5f), std::tanh(-0.5f), 1e-1f); + + EXPECT_NEAR(tanh(0.001f), std::tanh(0.001f), 1e-5f); +} + +TEST(arithmetic_suite, exp_test) +{ + EXPECT_NEAR(exp(0.5f), std::exp(0.5f), 1e-5f); + EXPECT_NEAR(exp(-0.5f), std::exp(-0.5f), 1e-5f); + + EXPECT_NEAR(exp(0.0f), 1.0f, 1e-5f); + + EXPECT_NEAR(exp(4.0f), std::exp(4.0f), 1.0f); + EXPECT_NEAR(exp(-6.0f), std::exp(-6.0f), 1e-2f); + + EXPECT_NEAR(exp(1e-6), std::exp(1e-6f), 1e-5f); +} + +TEST(arithmetic_suite, isApproximatelyEqual_test) +{ + // Test identical values + EXPECT_TRUE(isApproximatelyEqual(0.0f, 0.0f)); + EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f)); + EXPECT_TRUE(isApproximatelyEqual(-1.0f, -1.0f)); + + // Test with double precision + EXPECT_TRUE(isApproximatelyEqual(0.0, 0.0)); + EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0)); + EXPECT_TRUE(isApproximatelyEqual(-1.0, -1.0)); + + // Test integer types (exact equality) + EXPECT_TRUE(isApproximatelyEqual(0, 0)); + EXPECT_TRUE(isApproximatelyEqual(5, 5)); + EXPECT_TRUE(isApproximatelyEqual(-3, -3)); + + // Test values well within default relative epsilon for float (1e-5f) + EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f + 1e-6f)); // Well within 1e-5f * 1.0f + EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f - 1e-6f)); + EXPECT_TRUE(isApproximatelyEqual(10.0f, 10.0f + 5e-5f)); // Well within 1e-5f * 10.0f = 1e-4f + EXPECT_TRUE(isApproximatelyEqual(-1.0f, -1.0f + 1e-6f)); + + // Test values well within default relative epsilon for double (1e-9) + EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0 + 1e-10)); // Well within 1e-9 * 1.0 + EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0 - 1e-10)); + EXPECT_TRUE(isApproximatelyEqual(100.0, 100.0 + 5e-8)); // Well within 1e-9 * 100.0 = 1e-7 + + // Test values outside default relative epsilon for float + EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f + 2e-5f)); // Exceeds 1e-5f * 1.0f + EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f - 2e-5f)); + EXPECT_FALSE(isApproximatelyEqual(10.0f, 10.0f + 2e-4f)); // Exceeds 1e-5f * 10.0f + + // Test values outside default relative epsilon for double + EXPECT_FALSE(isApproximatelyEqual(1.0, 1.0 + 2e-9)); // Exceeds 1e-9 * 1.0 + EXPECT_FALSE(isApproximatelyEqual(1.0, 1.0 - 2e-9)); + EXPECT_FALSE(isApproximatelyEqual(100.0, 100.0 + 2e-7)); // Exceeds 1e-9 * 100.0 + + // Test clearly different values + EXPECT_FALSE(isApproximatelyEqual(1.0f, 2.0f)); + EXPECT_FALSE(isApproximatelyEqual(-1.0f, 1.0f)); + EXPECT_FALSE(isApproximatelyEqual(0.0f, 1.0f)); + EXPECT_FALSE(isApproximatelyEqual(1.0f, 0.0f)); + + EXPECT_FALSE(isApproximatelyEqual(1.0, 2.0)); + EXPECT_FALSE(isApproximatelyEqual(-1.0, 1.0)); + EXPECT_FALSE(isApproximatelyEqual(0.0, 1.0)); + + // Test integer differences + EXPECT_FALSE(isApproximatelyEqual(1, 2)); + EXPECT_FALSE(isApproximatelyEqual(-1, 1)); + EXPECT_FALSE(isApproximatelyEqual(0, 1)); + + // Test with custom epsilon values + EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f + 1e-7f, 1e-6f)); // Custom epsilon + EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f + 1e-5f, 1e-6f)); // Exceeds custom epsilon + + // Test edge cases near zero - demonstrate relative epsilon limitations + EXPECT_TRUE(isApproximatelyEqual(0.0f, 0.0f)); + EXPECT_TRUE(isApproximatelyEqual(-0.0f, 0.0f)); + + // Demonstrate that relative epsilon correctly fails for near-zero comparisons + float tiny_val = 1e-10f; + EXPECT_TRUE(isApproximatelyEqual(tiny_val, tiny_val)); + EXPECT_FALSE(isApproximatelyEqual(0.0f, tiny_val, 1e-8f)); // Correctly fails - this is expected behavior! + + // For absolute comparisons near zero, use direct comparison: + EXPECT_TRUE(std::abs(0.0f - tiny_val) <= 1e-8f); // Direct absolute comparison works + + // Test with small values where relative epsilon works well + float small_val = 1e-3f; // 0.001f + EXPECT_TRUE(isApproximatelyEqual(small_val, small_val)); + EXPECT_TRUE(isApproximatelyEqual(small_val, small_val + 5e-9f)); // Well within 1e-5f * 1e-3f = 1e-8f + EXPECT_FALSE(isApproximatelyEqual(small_val, small_val + 2e-8f)); // Exceeds 1e-8f + + // Test with large values where relative epsilon scales appropriately + float large_val = 1e6f; // 1,000,000 + EXPECT_TRUE(isApproximatelyEqual(large_val, large_val)); + EXPECT_TRUE(isApproximatelyEqual(large_val, large_val + 5.0f)); // Well within 1e-5f * 1e6f = 10.0f + EXPECT_FALSE(isApproximatelyEqual(large_val, large_val + 20.0f)); // Exceeds 10.0f threshold + + // Test audio-relevant ranges + // Frequency range: 20Hz - 20kHz + EXPECT_TRUE(isApproximatelyEqual(440.0f, 440.0f + 0.002f)); // Well within 1e-5f * 440 ≈ 0.0044f + EXPECT_FALSE(isApproximatelyEqual(440.0f, 440.0f + 0.01f)); // Exceeds threshold + + // Gain range: 0.0 - 1.0 + EXPECT_TRUE(isApproximatelyEqual(0.5f, 0.5f + 1e-6f)); // Well within 1e-5f * 0.5 = 5e-6f + EXPECT_FALSE(isApproximatelyEqual(0.5f, 0.5f + 1e-5f)); // Exceeds 5e-6f + + // Test sign differences + EXPECT_FALSE(isApproximatelyEqual(1.0f, -1.0f)); + EXPECT_FALSE(isApproximatelyEqual(-0.5f, 0.5f)); + + // Test boundary behavior around typical audio values + // MIDI note velocity (0-127) + EXPECT_TRUE(isApproximatelyEqual(64.0f, 64.0f + 0.0005f)); // Well within tolerance + EXPECT_FALSE(isApproximatelyEqual(64.0f, 64.0f + 0.001f)); // Outside tolerance + + // Sample values (-1.0 to 1.0 range) + EXPECT_TRUE(isApproximatelyEqual(0.8f, 0.8f + 5e-6f)); // Well within tolerance + EXPECT_FALSE(isApproximatelyEqual(0.8f, 0.8f + 1e-5f)); // At/outside tolerance +} diff --git a/tests/utilities/midi_test.cpp b/tests/utils/midi_test.cpp similarity index 93% rename from tests/utilities/midi_test.cpp rename to tests/utils/midi_test.cpp index 916e92f..dccfeba 100644 --- a/tests/utilities/midi_test.cpp +++ b/tests/utils/midi_test.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/utils/midi.h" -#include "neuron.h" +#include using namespace neuron; diff --git a/tests/utils/smoothed_value_test.cpp b/tests/utils/smoothed_value_test.cpp new file mode 100644 index 0000000..6f88734 --- /dev/null +++ b/tests/utils/smoothed_value_test.cpp @@ -0,0 +1,508 @@ +#include "neuron/utils/smoothed_value.h" + +#include +#include + +using namespace neuron; + +class LinearSmoothedValueTest : public ::testing::Test { +protected: + void SetUp() override + { + // Default test parameters + sampleRate = 44100.0; + rampLengthMs = 100.0; // 100ms ramp + } + + // Helper function for floating point comparisons + bool IsApproximatelyEqual(float a, float b, float tolerance = 1e-6f) + { + return std::abs(a - b) < tolerance; + } + + double sampleRate; + double rampLengthMs; +}; + +class MultiplicativeSmoothedValueTest : public ::testing::Test { +protected: + void SetUp() override + { + // Default test parameters + sampleRate = 44100.0; + rampLengthMs = 100.0; // 100ms ramp + } + + // Helper function for floating point comparisons + bool IsApproximatelyEqual(float a, float b, float tolerance = 1e-6f) + { + return std::abs(a - b) < tolerance; + } + + double sampleRate; + double rampLengthMs; +}; + +// LINEAR ============================================================================================================= + +// Test default constructor +TEST_F(LinearSmoothedValueTest, DefaultConstructor) +{ + LinearSmoothedValue sv; + EXPECT_FLOAT_EQ(sv.GetNextValue(), 0.0f); +} + +// Test constructor with initial value +TEST_F(LinearSmoothedValueTest, ConstructorWithInitialValue) +{ + const float initialValue = 5.0f; + LinearSmoothedValue sv(initialValue); + EXPECT_FLOAT_EQ(sv.GetNextValue(), initialValue); +} + +// Test Reset with valid parameters +TEST_F(LinearSmoothedValueTest, ResetWithValidParameters) +{ + LinearSmoothedValue sv(1.0f); + sv.Reset(sampleRate, rampLengthMs); + + EXPECT_FLOAT_EQ(sv.GetNextValue(), 1.0f); + + sv.SetTargetValue(2.0f); + float nextValue = sv.GetNextValue(); + EXPECT_GT(nextValue, 1.0f); + EXPECT_LT(nextValue, 2.0f); +} + +// Test Reset with invalid parameters +TEST_F(LinearSmoothedValueTest, ResetWithInvalidParameters) +{ + LinearSmoothedValue sv(1.0f); + sv.SetTargetValue(2.0f); + + // Store the state before any reset attempts + float valueBefore = sv.GetNextValue(); + + // Invalid resets should be ignored, so behavior should remain consistent + sv.Reset(-1.0, rampLengthMs); + float valueAfter1 = sv.GetNextValue(); + + sv.Reset(sampleRate, -1.0); + float valueAfter2 = sv.GetNextValue(); + + // Values should continue progressing toward target + EXPECT_GT(valueAfter1, valueBefore); // Still progressing + EXPECT_GT(valueAfter2, valueAfter1); // Still progressing + EXPECT_LT(valueAfter2, 2.0f); // Haven't reached target yet +} + +// Test SetTargetValue +TEST_F(LinearSmoothedValueTest, SetTargetValue) +{ + LinearSmoothedValue sv(0.0f); + sv.Reset(sampleRate, rampLengthMs); + + sv.SetTargetValue(10.0f); + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + EXPECT_GT(value1, 0.0f); + EXPECT_GT(value2, value1); + EXPECT_LT(value2, 10.0f); +} + +// Test GetNextValue reaches target eventually +TEST_F(LinearSmoothedValueTest, GetNextValueReachesTarget) +{ + LinearSmoothedValue sv(0.0f); + sv.Reset(sampleRate, 50.0); + sv.SetTargetValue(1.0f); + + int expectedSamples = static_cast((50.0 / 1000.0) * sampleRate); + + float lastValue = 0.0f; + for (int i = 0; i < expectedSamples + 10; ++i) { + lastValue = sv.GetNextValue(); + } + + EXPECT_FLOAT_EQ(lastValue, 1.0f); +} + +// Test smoothing with negative values +TEST_F(LinearSmoothedValueTest, SmoothingWithNegativeValues) +{ + LinearSmoothedValue sv(5.0f); + sv.Reset(sampleRate, rampLengthMs); + sv.SetTargetValue(-5.0f); + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + EXPECT_LT(value1, 5.0f); + EXPECT_LT(value2, value1); + EXPECT_GT(value1, -5.0f); +} + +// Test Skip functionality +TEST_F(LinearSmoothedValueTest, SkipSamples) +{ + LinearSmoothedValue sv1(0.0f); + LinearSmoothedValue sv2(0.0f); + + sv1.Reset(sampleRate, rampLengthMs); + sv2.Reset(sampleRate, rampLengthMs); + + sv1.SetTargetValue(10.0f); + sv2.SetTargetValue(10.0f); + + for (int i = 0; i < 100; ++i) { + sv1.GetNextValue(); + } + + sv2.Skip(100); + + float value1 = sv1.GetNextValue(); + float value2 = sv2.GetNextValue(); + + EXPECT_TRUE(IsApproximatelyEqual(value1, value2, 1e-5f)); +} + +// Test when ramp length is zero +TEST_F(LinearSmoothedValueTest, ZeroRampLength) +{ + LinearSmoothedValue sv(1.0f); + sv.Reset(sampleRate, 0.0); + sv.SetTargetValue(5.0f); + + EXPECT_FLOAT_EQ(sv.GetNextValue(), 5.0f); +} + +// Test very short ramp length +TEST_F(LinearSmoothedValueTest, VeryShortRampLength) +{ + LinearSmoothedValue sv(0.0f); + sv.Reset(sampleRate, 0.1); + sv.SetTargetValue(1.0f); + + float value = 0.0f; + for (int i = 0; i < 10; ++i) { + value = sv.GetNextValue(); + } + + EXPECT_FLOAT_EQ(value, 1.0f); +} + +// Test multiple target changes +TEST_F(LinearSmoothedValueTest, MultipleTargetChanges) +{ + LinearSmoothedValue sv(0.0f); + sv.Reset(sampleRate, 50.0); + + sv.SetTargetValue(10.0f); + for (int i = 0; i < 100; ++i) { + sv.GetNextValue(); + } + + float valueBeforeChange = sv.GetNextValue(); + sv.SetTargetValue(-5.0f); + float valueAfterChange = sv.GetNextValue(); + + EXPECT_NE(valueBeforeChange, valueAfterChange); + + for (int i = 0; i < 3000; ++i) { + sv.GetNextValue(); + } + EXPECT_FLOAT_EQ(sv.GetNextValue(), -5.0f); +} + +// Test same target value +TEST_F(LinearSmoothedValueTest, SameTargetValue) +{ + LinearSmoothedValue sv(5.0f); + sv.Reset(sampleRate, rampLengthMs); + sv.SetTargetValue(5.0f); + + EXPECT_FLOAT_EQ(sv.GetNextValue(), 5.0f); + EXPECT_FLOAT_EQ(sv.GetNextValue(), 5.0f); +} + +// Test large sample rate +TEST_F(LinearSmoothedValueTest, LargeSampleRate) +{ + LinearSmoothedValue sv(0.0f); + sv.Reset(192000.0, 100.0); + sv.SetTargetValue(1.0f); + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + EXPECT_GT(value1, 0.0f); + EXPECT_GT(value2, value1); + EXPECT_LT(value1, 0.1f); +} + +// MULTIPLICATIVE ===================================================================================================== + +// Test constructor with initial value (multiplicative can't start from zero) +TEST_F(MultiplicativeSmoothedValueTest, ConstructorWithInitialValue) +{ + const float initialValue = 5.0f; + MultiplicativeSmoothedValue sv(initialValue); + EXPECT_FLOAT_EQ(sv.GetNextValue(), initialValue); +} + +// Test basic multiplicative behavior - exponential curve +TEST_F(MultiplicativeSmoothedValueTest, ExponentialCurve) +{ + MultiplicativeSmoothedValue sv(1.0f); + sv.Reset(sampleRate, 100.0); // 100ms ramp + sv.SetTargetValue(8.0f); // 3 octaves (2^3 = 8) + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + float value3 = sv.GetNextValue(); + + // For multiplicative smoothing, the ratio between consecutive values should be approximately constant + float ratio1 = value2 / value1; + float ratio2 = value3 / value2; + + EXPECT_GT(value1, 1.0f); + EXPECT_GT(value2, value1); + EXPECT_GT(value3, value2); + EXPECT_TRUE(IsApproximatelyEqual(ratio1, ratio2, 1e-4f)); +} + +// Test frequency doubling (octave) - with more realistic tolerance +TEST_F(MultiplicativeSmoothedValueTest, FrequencyOctave) +{ + MultiplicativeSmoothedValue sv(440.0f); // A4 + sv.Reset(sampleRate, 50.0); + sv.SetTargetValue(880.0f); // A5 (one octave up) + + // Check that we get exponential progression + float prevValue = 440.0f; + for (int i = 0; i < 10; ++i) { + float currentValue = sv.GetNextValue(); + EXPECT_GT(currentValue, prevValue); + EXPECT_LT(currentValue, 880.0f); + prevValue = currentValue; + } + + // Eventually gets close to target - use more relaxed tolerance + for (int i = 0; i < 5000; ++i) { // More samples to ensure convergence + sv.GetNextValue(); + } + float finalValue = sv.GetNextValue(); + EXPECT_TRUE(IsApproximatelyEqual(finalValue, 880.0f, 0.01f)); // 1% tolerance +} + +// Test dB conversion - another common audio use case +TEST_F(MultiplicativeSmoothedValueTest, DecibelConversion) +{ + MultiplicativeSmoothedValue sv(1.0f); // 0 dB + sv.Reset(sampleRate, 100.0); + sv.SetTargetValue(3.16228f); // Approximately 10 dB (10^(10/20)) + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + EXPECT_GT(value1, 1.0f); + EXPECT_GT(value2, value1); + EXPECT_LT(value2, 3.16228f); +} + +// Test zero handling - multiplicative can't reach zero +TEST_F(MultiplicativeSmoothedValueTest, ZeroHandling) +{ + MultiplicativeSmoothedValue sv(1.0f); + sv.Reset(sampleRate, rampLengthMs); + sv.SetTargetValue(0.0f); // Invalid target for multiplicative + + // Should not progress since target is zero + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + EXPECT_FLOAT_EQ(value1, 1.0f); // Should remain at current value + EXPECT_FLOAT_EQ(value2, 1.0f); +} + +// Test starting from zero - should handle gracefully +TEST_F(MultiplicativeSmoothedValueTest, StartingFromZero) +{ + MultiplicativeSmoothedValue sv(0.0f); + sv.Reset(sampleRate, rampLengthMs); + sv.SetTargetValue(10.0f); + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + // Should not progress since current value is zero + EXPECT_FLOAT_EQ(value1, 0.0f); + EXPECT_FLOAT_EQ(value2, 0.0f); +} + +// Test SetTargetValue +TEST_F(MultiplicativeSmoothedValueTest, SetTargetValue) +{ + MultiplicativeSmoothedValue sv(2.0f); + sv.Reset(sampleRate, rampLengthMs); + sv.SetTargetValue(32.0f); // 4 octaves (2^4 * 2 = 32) + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + EXPECT_GT(value1, 2.0f); + EXPECT_GT(value2, value1); + EXPECT_LT(value2, 32.0f); +} + +// Test GetNextValue gets close to target +TEST_F(MultiplicativeSmoothedValueTest, GetNextValueReachesTarget) +{ + MultiplicativeSmoothedValue sv(1.0f); + sv.Reset(sampleRate, 50.0); + sv.SetTargetValue(4.0f); + + float lastValue = 1.0f; + for (int i = 0; i < 5000; ++i) { // More samples + lastValue = sv.GetNextValue(); + } + + // Use percentage-based tolerance for multiplicative + EXPECT_TRUE(IsApproximatelyEqual(lastValue, 4.0f, 0.01f)); // 1% tolerance +} + +// Test Skip functionality +TEST_F(MultiplicativeSmoothedValueTest, SkipSamples) +{ + MultiplicativeSmoothedValue sv1(1.0f); + MultiplicativeSmoothedValue sv2(1.0f); + + sv1.Reset(sampleRate, rampLengthMs); + sv2.Reset(sampleRate, rampLengthMs); + + sv1.SetTargetValue(16.0f); + sv2.SetTargetValue(16.0f); + + for (int i = 0; i < 100; ++i) { + sv1.GetNextValue(); + } + + sv2.Skip(100); + + float value1 = sv1.GetNextValue(); + float value2 = sv2.GetNextValue(); + + EXPECT_TRUE(IsApproximatelyEqual(value1, value2, 1e-4f)); +} + +// Test when ramp length is zero +TEST_F(MultiplicativeSmoothedValueTest, ZeroRampLength) +{ + MultiplicativeSmoothedValue sv(2.0f); + sv.Reset(sampleRate, 0.0); + sv.SetTargetValue(8.0f); + + EXPECT_FLOAT_EQ(sv.GetNextValue(), 8.0f); +} + +// Test very short ramp length - adjust expectations +TEST_F(MultiplicativeSmoothedValueTest, VeryShortRampLength) +{ + MultiplicativeSmoothedValue sv(1.0f); + sv.Reset(sampleRate, 0.1); // Very short ramp + sv.SetTargetValue(2.0f); + + float value = 1.0f; + for (int i = 0; i < 20; ++i) { // More iterations for very short ramp + value = sv.GetNextValue(); + } + + // For very short ramps, expect to get reasonably close + EXPECT_TRUE(IsApproximatelyEqual(value, 2.0f, 0.05f)); // 5% tolerance +} + +// Test multiple target changes - with realistic tolerances +TEST_F(MultiplicativeSmoothedValueTest, MultipleTargetChanges) +{ + MultiplicativeSmoothedValue sv(1.0f); + sv.Reset(sampleRate, 50.0); + + sv.SetTargetValue(4.0f); + for (int i = 0; i < 100; ++i) { + sv.GetNextValue(); + } + + float valueBeforeChange = sv.GetNextValue(); + sv.SetTargetValue(0.5f); // Going down + float valueAfterChange = sv.GetNextValue(); + + EXPECT_LT(valueAfterChange, valueBeforeChange); // Should start decreasing + + for (int i = 0; i < 5000; ++i) { + sv.GetNextValue(); + } + float finalValue = sv.GetNextValue(); + EXPECT_TRUE(IsApproximatelyEqual(finalValue, 0.5f, 0.01f)); // 1% tolerance +} + +// Test same target value +TEST_F(MultiplicativeSmoothedValueTest, SameTargetValue) +{ + MultiplicativeSmoothedValue sv(5.0f); + sv.Reset(sampleRate, rampLengthMs); + sv.SetTargetValue(5.0f); + + EXPECT_FLOAT_EQ(sv.GetNextValue(), 5.0f); + EXPECT_FLOAT_EQ(sv.GetNextValue(), 5.0f); +} + +// Test decreasing values (important for multiplicative) +TEST_F(MultiplicativeSmoothedValueTest, DecreasingValues) +{ + MultiplicativeSmoothedValue sv(8.0f); + sv.Reset(sampleRate, rampLengthMs); + sv.SetTargetValue(2.0f); // Decreasing + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + EXPECT_LT(value1, 8.0f); + EXPECT_LT(value2, value1); + EXPECT_GT(value2, 2.0f); +} + +// Test large sample rate +TEST_F(MultiplicativeSmoothedValueTest, LargeSampleRate) +{ + MultiplicativeSmoothedValue sv(1.0f); + sv.Reset(192000.0, 100.0); + sv.SetTargetValue(2.0f); + + float value1 = sv.GetNextValue(); + float value2 = sv.GetNextValue(); + + EXPECT_GT(value1, 1.0f); + EXPECT_GT(value2, value1); + EXPECT_LT(value1, 1.01f); // Should be a very small increment +} + +// Test musical interval - with more realistic expectations +TEST_F(MultiplicativeSmoothedValueTest, MusicalInterval) +{ + MultiplicativeSmoothedValue sv(440.0f); // A4 + sv.Reset(sampleRate, 120.0); // 120ms for 12 steps + sv.SetTargetValue(880.0f); // A5 + + // Just run for the expected duration plus some buffer + int totalSamples = static_cast((150.0 / 1000.0) * sampleRate); // 150ms buffer + for (int i = 0; i < totalSamples; ++i) { + sv.GetNextValue(); + } + + float finalValue = sv.GetNextValue(); + + // Check that we get reasonably close to 880Hz (within 1%) + EXPECT_TRUE(IsApproximatelyEqual(finalValue, 880.0f, 8.8f)); // ~1% tolerance +} diff --git a/tests/audio/waveform_test.cpp b/tests/utils/waveform_test.cpp similarity index 97% rename from tests/audio/waveform_test.cpp rename to tests/utils/waveform_test.cpp index 5d66133..9d4548f 100644 --- a/tests/audio/waveform_test.cpp +++ b/tests/utils/waveform_test.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/utils/waveform.h" -#include "neuron.h" +#include using namespace neuron; diff --git a/vendor/googletest b/vendor/googletest index df1544b..35d0c36 160000 --- a/vendor/googletest +++ b/vendor/googletest @@ -1 +1 @@ -Subproject commit df1544bcee0c7ce35cd5ea0b3eb8cc81855a4140 +Subproject commit 35d0c365609296fa4730d62057c487e3cfa030ff