From 322c902d9342b5381e0fb9987ee9bf9ff6cbfff1 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Tue, 26 Nov 2024 14:18:27 -0600 Subject: [PATCH 01/32] Remove unnecessary semicolons --- src/processors/effects/saturator.h | 4 ++-- src/processors/effects/wavefolder.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/processors/effects/saturator.h b/src/processors/effects/saturator.h index 2d6ab32..8b371f8 100644 --- a/src/processors/effects/saturator.h +++ b/src/processors/effects/saturator.h @@ -16,12 +16,12 @@ class Saturator { * * @return Saturator */ - Saturator() {}; + Saturator() {} /** * Frees any memory allocated by the saturator. */ - ~Saturator() {}; + ~Saturator() {} /** * Applies a saturation algorithm to an input sample. diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h index b13df38..de38321 100644 --- a/src/processors/effects/wavefolder.h +++ b/src/processors/effects/wavefolder.h @@ -14,12 +14,12 @@ class Wavefolder { /** * Creates a default wavefolder processor. */ - Wavefolder() {}; + Wavefolder() {} /** * Frees any memory allocated by the wavefolder. */ - ~Wavefolder() {}; + ~Wavefolder() {} /** * Applies a wavefolding algorithm to an input sample. From 8dca46d124ad1c971bde68a7c2d7886f8ba8c3ab Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Wed, 27 Nov 2024 12:37:14 -0600 Subject: [PATCH 02/32] Update floating point constant --- src/utilities/arithmetic.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/arithmetic.h b/src/utilities/arithmetic.h index 6d382c0..1068c3d 100644 --- a/src/utilities/arithmetic.h +++ b/src/utilities/arithmetic.h @@ -25,7 +25,7 @@ const float PI = 3.14159265358979323846264338327950288f; /** * An irrational number that is the base of the natural logarithm. */ -const float EULER = 2.71828182845904523536028747135266250; +const float EULER = 2.71828182845904523536028747135266250f; /** * Depicts different mathematical curves, e.g. exponential, From 1d3d8b6682d8419ec96d1173e65c9a0b5b7d8a46 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Wed, 27 Nov 2024 13:36:05 -0600 Subject: [PATCH 03/32] Re-format code --- .clang-format | 111 +++++++++++++++ scripts/format.sh | 4 +- src/audio/context.h | 30 ++-- src/audio/sample.h | 26 ++-- src/audio/waveform.h | 60 ++++---- src/generators/oscillator.h | 148 ++++++++++---------- src/modulators/adsr.cpp | 36 ++--- src/modulators/adsr.h | 192 +++++++++++++------------- src/neuron.h | 1 + src/processors/effects/saturator.h | 96 ++++++------- src/processors/effects/wavefolder.cpp | 21 +++ src/processors/effects/wavefolder.h | 105 +++++++------- src/processors/filters/filter.h | 90 ++++++------ src/utilities/arithmetic.h | 192 +++++++++++++------------- src/utilities/logger.h | 8 +- src/utilities/midi.h | 24 ++-- src/utilities/parameterized.h | 13 ++ src/utilities/timer.h | 52 +++---- 18 files changed, 682 insertions(+), 527 deletions(-) create mode 100644 .clang-format create mode 100644 src/utilities/parameterized.h 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/scripts/format.sh b/scripts/format.sh index a11304c..d5b4338 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,13 +1,13 @@ #!/bin/bash printf "Formatting code...\n" -find src/ -iname '*.h' -o -iname '*.cpp' | xargs clang-format -i -style=WebKit +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/src/audio/context.h b/src/audio/context.h index 3fdb3de..53096a6 100644 --- a/src/audio/context.h +++ b/src/audio/context.h @@ -4,20 +4,20 @@ 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; -}; + /** + * 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 }; + /** + * 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 index a777997..e6865de 100644 --- a/src/audio/sample.h +++ b/src/audio/sample.h @@ -2,20 +2,20 @@ namespace neuron { -/** - * The basic data type for DSP operations of - * an audio signal. - */ -using Sample = float; + /** + * 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 minimum possible value for a sample. + */ + const Sample MIN = -1.0f; -/** - * The maximum possible value for a sample. - */ -const Sample MAX = 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 index d06f195..f0cf428 100644 --- a/src/audio/waveform.h +++ b/src/audio/waveform.h @@ -5,37 +5,37 @@ namespace neuron { -/** - * The basic waveform variants, i.e. sine, triangle, - * sawtooth, and square. - */ -enum class Waveform { - SINE, - TRIANGLE, - SAWTOOTH, - SQUARE, -}; + /** + * 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; + /** + * 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.h b/src/generators/oscillator.h index 8569e5b..92c4ac5 100644 --- a/src/generators/oscillator.h +++ b/src/generators/oscillator.h @@ -9,82 +9,82 @@ 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); + const size_t WAVETABLE_SIZE = 256; /** - * Sets the frequency of the oscillator. - * - * @param frequency The new oscillator output frequency. + * The Oscillator class creates an audio signal + * with a basic waveform. */ - 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; -}; + 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 index 1b1e42d..120d62d 100644 --- a/src/modulators/adsr.cpp +++ b/src/modulators/adsr.cpp @@ -26,24 +26,24 @@ float AdsrEnvelopeModulator::Modulate() */ 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; + 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; diff --git a/src/modulators/adsr.h b/src/modulators/adsr.h index cd331f4..60c5ee4 100644 --- a/src/modulators/adsr.h +++ b/src/modulators/adsr.h @@ -4,109 +4,109 @@ 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 + * The configuration of an ADSR envelope curve including durations + * for the attack, decay, and release stages as well as a sustain level. */ - AdsrEnvelopeModulator(Context& context = DEFAULT_CONTEXT, AdsrEnvelope envelope = DEFAULT_ADSR_ENVELOPE); + struct AdsrEnvelope { + float attack; + float decay; + float sustain; + float release; + }; /** - * Frees any memory allocated by the modulator. + * The stages of an ADSR envelope, including an "idle" stage when not in use. */ - ~AdsrEnvelopeModulator(); + enum class AdsrStage { + IDLE, + ATTACK, + DECAY, + SUSTAIN, + RELEASE + }; + + const AdsrEnvelope DEFAULT_ADSR_ENVELOPE = { + 100.0f, 200.0f, 1.0f, 1000.0f + }; /** - * Calculates a modulation value to apply to some arbitrary variable. + * The ADsrEnvelopeModulator class is a modulation source + * that is based off of an ADSR envelope generator. */ - 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; -}; + 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 index 603925e..9dc2e46 100644 --- a/src/neuron.h +++ b/src/neuron.h @@ -28,6 +28,7 @@ #include "utilities/arithmetic.h" #include "utilities/logger.h" #include "utilities/midi.h" +#include "utilities/parameterized.h" #include "utilities/timer.h" #endif diff --git a/src/processors/effects/saturator.h b/src/processors/effects/saturator.h index 8b371f8..e485e11 100644 --- a/src/processors/effects/saturator.h +++ b/src/processors/effects/saturator.h @@ -5,55 +5,55 @@ namespace neuron { -/** - * The Saturator class applies a tape saturation - * algorithm to audio signals. - */ -class Saturator { -public: /** - * Creates a default saturator processor. - * - * @return Saturator + * The Saturator class applies a tape saturation + * algorithm to audio signals. */ - 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; -}; + 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 index cba4f22..b5df4b9 100644 --- a/src/processors/effects/wavefolder.cpp +++ b/src/processors/effects/wavefolder.cpp @@ -2,8 +2,17 @@ using namespace neuron; +Wavefolder::~Wavefolder() +{ + a_inputGain = nullptr; +} + Sample Wavefolder::Process(const Sample input) { + if (a_inputGain != nullptr) { + m_inputGain = a_inputGain->load(); + } + float output = (float)input * m_inputGain; while (output > m_threshold || output < -m_threshold) { if (output > m_threshold) { @@ -34,3 +43,15 @@ void Wavefolder::SetSymmetry(float symmetry) { m_symmetry = clamp(symmetry, 0.0f, 1.0f); } + +void Wavefolder::AttachParameter(int parameterId, std::atomic* parameter) +{ + switch ((WavefolderParameter)parameterId) { + case InputGain: + a_inputGain = parameter; + break; + default: + // Do nothing. + break; + } +} diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h index de38321..a284a8c 100644 --- a/src/processors/effects/wavefolder.h +++ b/src/processors/effects/wavefolder.h @@ -2,62 +2,71 @@ #include "audio/sample.h" #include "utilities/arithmetic.h" +#include "utilities/parameterized.h" namespace neuron { - -/** - * The Wavefolder class applies a wavefolding - * algorithm to audio signals. - */ -class Wavefolder { -public: /** - * Creates a default wavefolder processor. + * The Wavefolder class applies a wavefolding + * algorithm to audio signals. */ - Wavefolder() {} + class Wavefolder : public Parameterized { + public: + enum WavefolderParameter { + InputGain = 0 + }; - /** - * Frees any memory allocated by the wavefolder. - */ - ~Wavefolder() {} + /** + * Creates a default wavefolder processor. + */ + Wavefolder() + { + } - /** - * Applies a wavefolding algorithm to an input sample. - * - * @param input The input sample to be processed. - * @return Sample - */ - Sample Process(const Sample input); + /** + * Frees any memory allocated by the wavefolder. + */ + ~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); + /** + * 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 threshold of the wavefolder, above which samples will - * be "folded" toawrds zero until they are within the threshold. - */ - void SetThreshold(float threshold); + /** + * 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 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); + /** + * 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); + + void AttachParameter(int parameterId, std::atomic* parameter) override; -private: - float m_inputGain = 1.0f; - float m_threshold = 1.0f; - float m_symmetry = 1.0f; -}; + private: + std::atomic* a_inputGain = nullptr; -} \ No newline at end of file + float m_inputGain = 1.0f; + float m_threshold = 1.0f; + float m_symmetry = 1.0f; + }; +} diff --git a/src/processors/filters/filter.h b/src/processors/filters/filter.h index f6a8eac..b68a112 100644 --- a/src/processors/filters/filter.h +++ b/src/processors/filters/filter.h @@ -5,53 +5,53 @@ #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() { } + const float FILTER_CUTOFF_FREQ_MIN = 20.0f; + const float FILTER_CUTOFF_FREQ_MAX = 20000.0f; /** - * Applies a low-pass filter to an input sample. - * - * @param input The input sample to be processed. - * @return Sample + * The Filter class applies a simple low-pass filter + * to audio signals. */ - 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; -}; + 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 index 1068c3d..0dd9534 100644 --- a/src/utilities/arithmetic.h +++ b/src/utilities/arithmetic.h @@ -5,110 +5,110 @@ namespace neuron { -/** - * A value for the absence of quantity; nothing. - */ -const float ZERO = 0.0f; + /** + * 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; + /** + * 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 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; + /** + * 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 class Mapping { - EXP, - LOG, - LINEAR, -}; + /** + * 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); + /** + * 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)); } - case Mapping::LINEAR: - default: - return clamp(min + n * (max - min), min, 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 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; -} + /** + * 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.h b/src/utilities/logger.h index 77c6a63..41a6698 100644 --- a/src/utilities/logger.h +++ b/src/utilities/logger.h @@ -4,10 +4,10 @@ namespace neuron { -class Logger { -public: - static void Log(std::string str); -}; + class Logger { + public: + static void Log(std::string str); + }; } diff --git a/src/utilities/midi.h b/src/utilities/midi.h index e2cf2f5..5ad6845 100644 --- a/src/utilities/midi.h +++ b/src/utilities/midi.h @@ -6,17 +6,17 @@ 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; -} + /** + * 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/parameterized.h b/src/utilities/parameterized.h new file mode 100644 index 0000000..28830d3 --- /dev/null +++ b/src/utilities/parameterized.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace neuron { + class Parameterized { + public: + virtual ~Parameterized() = default; + + // Attach a parameter (e.g. via an atomic float pointer) + virtual void AttachParameter(int parameterId, std::atomic* parameter) = 0; + }; +} diff --git a/src/utilities/timer.h b/src/utilities/timer.h index 01b2495..3b719f9 100644 --- a/src/utilities/timer.h +++ b/src/utilities/timer.h @@ -4,31 +4,31 @@ 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; -}; + 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 From be7339971f3024ec829f2d14c41a2d0628ffec8f Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Wed, 27 Nov 2024 14:56:27 -0600 Subject: [PATCH 04/32] Add basic support for plugin building --- CMakeLists.txt | 9 +++++++-- scripts/test.sh | 2 +- src/processors/effects/wavefolder.cpp | 19 ++++++++++++------ src/processors/effects/wavefolder.h | 28 +++++++++++++++++++-------- src/utilities/parameterized.h | 8 +++++++- 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b82e9d5..9037f13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.14) -option(BUILD_TESTS "Build unit tests" OFF) +option(NEO_ENABLE_PLUGIN_SUPPORT "Enable plugin build support" OFF) +option(NEO_BUILD_TESTS "Build unit tests" OFF) project(neuron VERSION 0.1.0 LANGUAGES CXX) @@ -30,7 +31,11 @@ set_target_properties(neuron PROPERTIES PUBLIC CXX_STANDARD_REQUIRED ) -if(BUILD_TESTS) +if(NEO_ENABLE_PLUGIN_SUPPORT) + target_compile_definitions(neuron PUBLIC NEO_ENABLE_PLUGIN_SUPPORT) +endif() + +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}) diff --git a/scripts/test.sh b/scripts/test.sh index e8ecd6d..589e6fb 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -23,7 +23,7 @@ if [ $BUILD_TESTS == "true" ]; then rm -rf "$TARGET_DIR" mkdir -p "$TARGET_DIR" cd "$TARGET_DIR" || exit 1 - cmake -DBUILD_TESTS=ON ../../ + cmake -DNEO_BUILD_TESTS=ON ../../ make else cd "$TARGET_DIR" || exit 1 diff --git a/src/processors/effects/wavefolder.cpp b/src/processors/effects/wavefolder.cpp index b5df4b9..708234d 100644 --- a/src/processors/effects/wavefolder.cpp +++ b/src/processors/effects/wavefolder.cpp @@ -4,14 +4,16 @@ using namespace neuron; Wavefolder::~Wavefolder() { +#ifdef NEO_ENABLE_PLUGIN_SUPPORT a_inputGain = nullptr; +#endif } Sample Wavefolder::Process(const Sample input) { - if (a_inputGain != nullptr) { - m_inputGain = a_inputGain->load(); - } +#ifdef NEO_ENABLE_PLUGIN_SUPPORT + m_inputGain = a_inputGain->load(); +#endif float output = (float)input * m_inputGain; while (output > m_threshold || output < -m_threshold) { @@ -31,6 +33,9 @@ Sample Wavefolder::Process(const Sample input) void Wavefolder::SetInputGain(float gain) { +#ifdef NEO_ENABLE_PLUGIN_SUPPORT + a_inputGain->store(gain); +#endif m_inputGain = gain; } @@ -44,14 +49,16 @@ void Wavefolder::SetSymmetry(float symmetry) m_symmetry = clamp(symmetry, 0.0f, 1.0f); } -void Wavefolder::AttachParameter(int parameterId, std::atomic* parameter) +#ifdef NEO_ENABLE_PLUGIN_SUPPORT +void Wavefolder::AttachParameter(int parameter, std::atomic* value) { - switch ((WavefolderParameter)parameterId) { + switch ((WavefolderParameter)parameter) { case InputGain: - a_inputGain = parameter; + a_inputGain = value; break; default: // Do nothing. break; } } +#endif diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h index a284a8c..487a904 100644 --- a/src/processors/effects/wavefolder.h +++ b/src/processors/effects/wavefolder.h @@ -9,12 +9,9 @@ namespace neuron { * The Wavefolder class applies a wavefolding * algorithm to audio signals. */ - class Wavefolder : public Parameterized { + class Wavefolder + : public Parameterized { public: - enum WavefolderParameter { - InputGain = 0 - }; - /** * Creates a default wavefolder processor. */ @@ -60,13 +57,28 @@ namespace neuron { */ void SetSymmetry(float symmetry); - void AttachParameter(int parameterId, std::atomic* parameter) override; +#ifdef NEO_ENABLE_PLUGIN_SUPPORT + /** + * The wavefolder parameters that support automation by e.g. a DAW. + */ + enum WavefolderParameter { + InputGain = 0 + }; + + /** + * Attaches a parameter from a `AudioProcessorValueTreeState` parameter (from + * the JUCE library). + */ + void AttachParameter(int parameter, std::atomic* value) override; +#endif private: - std::atomic* a_inputGain = nullptr; - float m_inputGain = 1.0f; float m_threshold = 1.0f; float m_symmetry = 1.0f; + +#ifdef NEO_ENABLE_PLUGIN_SUPPORT + std::atomic* a_inputGain = nullptr; +#endif }; } diff --git a/src/utilities/parameterized.h b/src/utilities/parameterized.h index 28830d3..fe7bee5 100644 --- a/src/utilities/parameterized.h +++ b/src/utilities/parameterized.h @@ -1,13 +1,19 @@ #pragma once +#ifdef NEO_ENABLE_PLUGIN_SUPPORT #include +#endif namespace neuron { +#ifdef NEO_ENABLE_PLUGIN_SUPPORT class Parameterized { public: virtual ~Parameterized() = default; // Attach a parameter (e.g. via an atomic float pointer) - virtual void AttachParameter(int parameterId, std::atomic* parameter) = 0; + virtual void AttachParameter(int parameter, std::atomic* value) = 0; }; +#else + class Parameterized {}; +#endif } From fe8ccb077ddcef6fd862cf383125fc0d80ecb12b Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Wed, 27 Nov 2024 15:03:25 -0600 Subject: [PATCH 05/32] Format code --- src/processors/effects/wavefolder.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h index 487a904..bd68159 100644 --- a/src/processors/effects/wavefolder.h +++ b/src/processors/effects/wavefolder.h @@ -9,8 +9,7 @@ namespace neuron { * The Wavefolder class applies a wavefolding * algorithm to audio signals. */ - class Wavefolder - : public Parameterized { + class Wavefolder : public Parameterized { public: /** * Creates a default wavefolder processor. From d7a2c1ba93b3e0d1cb22fe8253e00b37732845c4 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Wed, 27 Nov 2024 17:02:19 -0600 Subject: [PATCH 06/32] Add parameter class --- src/processors/effects/wavefolder.cpp | 32 +------- src/processors/effects/wavefolder.h | 31 ++------ src/utilities/parameter.h | 103 ++++++++++++++++++++++++++ src/utilities/parameterized.h | 19 ----- 4 files changed, 111 insertions(+), 74 deletions(-) create mode 100644 src/utilities/parameter.h delete mode 100644 src/utilities/parameterized.h diff --git a/src/processors/effects/wavefolder.cpp b/src/processors/effects/wavefolder.cpp index 708234d..e04ebb3 100644 --- a/src/processors/effects/wavefolder.cpp +++ b/src/processors/effects/wavefolder.cpp @@ -2,20 +2,9 @@ using namespace neuron; -Wavefolder::~Wavefolder() -{ -#ifdef NEO_ENABLE_PLUGIN_SUPPORT - a_inputGain = nullptr; -#endif -} - Sample Wavefolder::Process(const Sample input) { -#ifdef NEO_ENABLE_PLUGIN_SUPPORT - m_inputGain = a_inputGain->load(); -#endif - - float output = (float)input * m_inputGain; + float output = (float)input * p_inputGain; while (output > m_threshold || output < -m_threshold) { if (output > m_threshold) { output = m_threshold - (output - m_threshold); @@ -33,10 +22,7 @@ Sample Wavefolder::Process(const Sample input) void Wavefolder::SetInputGain(float gain) { -#ifdef NEO_ENABLE_PLUGIN_SUPPORT - a_inputGain->store(gain); -#endif - m_inputGain = gain; + p_inputGain = gain; } void Wavefolder::SetThreshold(float threshold) @@ -48,17 +34,3 @@ void Wavefolder::SetSymmetry(float symmetry) { m_symmetry = clamp(symmetry, 0.0f, 1.0f); } - -#ifdef NEO_ENABLE_PLUGIN_SUPPORT -void Wavefolder::AttachParameter(int parameter, std::atomic* value) -{ - switch ((WavefolderParameter)parameter) { - case InputGain: - a_inputGain = value; - break; - default: - // Do nothing. - break; - } -} -#endif diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h index bd68159..e591cb7 100644 --- a/src/processors/effects/wavefolder.h +++ b/src/processors/effects/wavefolder.h @@ -2,26 +2,24 @@ #include "audio/sample.h" #include "utilities/arithmetic.h" -#include "utilities/parameterized.h" +#include "utilities/parameter.h" namespace neuron { /** * The Wavefolder class applies a wavefolding * algorithm to audio signals. */ - class Wavefolder : public Parameterized { + class Wavefolder { public: /** * Creates a default wavefolder processor. */ - Wavefolder() - { - } + Wavefolder() = default; /** * Frees any memory allocated by the wavefolder. */ - ~Wavefolder(); + ~Wavefolder() = default; /** * Applies a wavefolding algorithm to an input sample. @@ -56,28 +54,11 @@ namespace neuron { */ void SetSymmetry(float symmetry); -#ifdef NEO_ENABLE_PLUGIN_SUPPORT - /** - * The wavefolder parameters that support automation by e.g. a DAW. - */ - enum WavefolderParameter { - InputGain = 0 - }; - - /** - * Attaches a parameter from a `AudioProcessorValueTreeState` parameter (from - * the JUCE library). - */ - void AttachParameter(int parameter, std::atomic* value) override; -#endif + public: + Parameter p_inputGain; private: - float m_inputGain = 1.0f; float m_threshold = 1.0f; float m_symmetry = 1.0f; - -#ifdef NEO_ENABLE_PLUGIN_SUPPORT - std::atomic* a_inputGain = nullptr; -#endif }; } diff --git a/src/utilities/parameter.h b/src/utilities/parameter.h new file mode 100644 index 0000000..25bf7f3 --- /dev/null +++ b/src/utilities/parameter.h @@ -0,0 +1,103 @@ +#pragma once + +#ifdef NEO_ENABLE_PLUGIN_SUPPORT +#include +#endif + +namespace neuron { +#ifdef NEO_ENABLE_PLUGIN_SUPPORT + template + class Parameter { + public: + Parameter(std::atomic* ptr = nullptr) + { + m_parameter = ptr; + } + + ~Parameter() + { + m_parameter = nullptr; + } + + // Attach a parameter (e.g. via an atomic float pointer) + void AttachSource(std::atomic* ptr) + { + m_parameter = ptr; + } + + operator float() const { + return m_parameter->load(); + } + + T operator=(T value) const { + m_parameter->store(value); + return 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 { + return m_parameter->load() * value; + } + + T operator/(T value) const { + if (value == 0.0) { + return value; + } else { + return m_parameter->load() / value; + } + } + + private: + std::atomic* m_parameter = nullptr; + }; +#else + template + class Parameter { + public: + Parameter(T value = 0.0) + { + m_parameter = value; + } + + ~Parameter() = default; + + operator float() const { + return m_parameter; + } + + T operator=(T value) const { + 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 { + 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/src/utilities/parameterized.h b/src/utilities/parameterized.h deleted file mode 100644 index fe7bee5..0000000 --- a/src/utilities/parameterized.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#ifdef NEO_ENABLE_PLUGIN_SUPPORT -#include -#endif - -namespace neuron { -#ifdef NEO_ENABLE_PLUGIN_SUPPORT - class Parameterized { - public: - virtual ~Parameterized() = default; - - // Attach a parameter (e.g. via an atomic float pointer) - virtual void AttachParameter(int parameter, std::atomic* value) = 0; - }; -#else - class Parameterized {}; -#endif -} From a6ab300f43e9bf9619ccfd5b8abdc9ad3c1b47b1 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Wed, 27 Nov 2024 17:20:38 -0600 Subject: [PATCH 07/32] Format lib code --- src/neuron.h | 2 +- src/utilities/parameter.h | 44 +++++++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/neuron.h b/src/neuron.h index 9dc2e46..d1aad5d 100644 --- a/src/neuron.h +++ b/src/neuron.h @@ -28,7 +28,7 @@ #include "utilities/arithmetic.h" #include "utilities/logger.h" #include "utilities/midi.h" -#include "utilities/parameterized.h" +#include "utilities/parameter.h" #include "utilities/timer.h" #endif diff --git a/src/utilities/parameter.h b/src/utilities/parameter.h index 25bf7f3..8b204e6 100644 --- a/src/utilities/parameter.h +++ b/src/utilities/parameter.h @@ -6,7 +6,7 @@ namespace neuron { #ifdef NEO_ENABLE_PLUGIN_SUPPORT - template + template class Parameter { public: Parameter(std::atomic* ptr = nullptr) @@ -25,32 +25,38 @@ namespace neuron { m_parameter = ptr; } - operator float() const { + operator T() const + { return m_parameter->load(); } - T operator=(T value) const { + T operator=(T value) const + { m_parameter->store(value); return value; } - T operator+(T value) const { + T operator+(T value) const + { return m_parameter->load() + value; } - T operator-(T value) const { + T operator-(T value) const + { return m_parameter->load() - value; } - T operator*(T value) const { + T operator*(T value) const + { return m_parameter->load() * value; } - T operator/(T value) const { + T operator/(T value) const + { if (value == 0.0) { return value; } else { - return m_parameter->load() / value; + return m_parameter->load() / value; } } @@ -58,7 +64,7 @@ namespace neuron { std::atomic* m_parameter = nullptr; }; #else - template + template class Parameter { public: Parameter(T value = 0.0) @@ -68,31 +74,37 @@ namespace neuron { ~Parameter() = default; - operator float() const { + operator float() const + { return m_parameter; } - T operator=(T value) const { + T operator=(T value) const + { m_parameter = value; } - T operator+(T value) const { + T operator+(T value) const + { return m_parameter + value; } - T operator-(T value) const { + T operator-(T value) const + { return m_parameter - value; } - T operator*(T value) const { + T operator*(T value) const + { return m_parameter * value; } - T operator/(T value) const { + T operator/(T value) const + { if (value == 0.0) { return value; } else { - return m_parameter / value; + return m_parameter / value; } } From 10ab330e8bd14e33408518791fc78523ae4a11ba Mon Sep 17 00:00:00 2001 From: Matthew Maxwell <44885822+maxwellmattryan@users.noreply.github.com> Date: Sun, 1 Dec 2024 01:42:12 -0600 Subject: [PATCH 08/32] chore: add interfaces (#27) ## Summary This PR adds a lot of interfaces for guaranteeing consistency across DSP components. It also adds `Parameter` objects, which allow plugin support (i.e. supporting `std::atomic`). ## Changelog ``` - Adds support for ValueTreeState integration from JUCE - Adds new generator, processor, modulator, and neuron base classes ``` --- .github/workflows/ci.test.yml | 2 +- CMakeLists.txt | 53 +++++++---------- Makefile | 4 ++ scripts/test.sh | 20 ++++++- src/abstractions/generator.h | 31 ++++++++++ src/abstractions/modulator.h | 28 +++++++++ src/abstractions/neuron.h | 32 ++++++++++ src/{utilities => abstractions}/parameter.h | 56 +++++++++++++----- src/abstractions/processor.h | 30 ++++++++++ src/audio/context.h | 3 +- src/generators/oscillator.cpp | 27 +++++++-- src/generators/oscillator.h | 35 ++++++----- src/modulators/adsr.cpp | 61 +++++++++++++------- src/modulators/adsr.h | 42 +++++++++----- src/neuron.h | 8 ++- src/processors/effects/saturator.cpp | 35 +++++++++-- src/processors/effects/saturator.h | 37 +++++++----- src/processors/effects/wavefolder.cpp | 49 ++++++++++++---- src/processors/effects/wavefolder.h | 46 ++++++++------- src/processors/filters/filter.cpp | 30 +++++++--- src/processors/filters/filter.h | 38 ++++++------ src/utilities/midi.h | 2 - src/utilities/timer.h | 2 + tests/abstractions/parameter_test.cpp | 41 +++++++++++++ tests/abstractions/parameter_test_atomic.cpp | 53 +++++++++++++++++ vendor/googletest | 2 +- 26 files changed, 581 insertions(+), 186 deletions(-) create mode 100644 src/abstractions/generator.h create mode 100644 src/abstractions/modulator.h create mode 100644 src/abstractions/neuron.h rename src/{utilities => abstractions}/parameter.h (53%) create mode 100644 src/abstractions/processor.h create mode 100644 tests/abstractions/parameter_test.cpp create mode 100644 tests/abstractions/parameter_test_atomic.cpp diff --git a/.github/workflows/ci.test.yml b/.github/workflows/ci.test.yml index 6deed37..9e50d71 100644 --- a/.github/workflows/ci.test.yml +++ b/.github/workflows/ci.test.yml @@ -20,7 +20,7 @@ jobs: - name: "Generate Makefile" working-directory: ${{runner.workspace}}/neuron/target/test - run: cmake -DBUILD_TESTS=ON ../../ + run: cmake -DCMAKE_BUILD_TYPE=Release -DNEO_BUILD_TESTS=ON ../../ - name: "Build" working-directory: ${{runner.workspace}}/neuron/target/test diff --git a/CMakeLists.txt b/CMakeLists.txt index 9037f13..e348d9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,44 +1,28 @@ cmake_minimum_required(VERSION 3.14) -option(NEO_ENABLE_PLUGIN_SUPPORT "Enable plugin build support" OFF) +option(NEO_USE_STD_ATOMIC "Enable 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" -) +file(GLOB_RECURSE LIST_DIRECTORIES INCLUDE_DIRS "src/*") +file(GLOB_RECURSE SRC_FILES "src/*.cpp") -add_library(neuron STATIC ${SOURCE}) +add_library(neuron STATIC ${SRC_FILES}) set_target_properties(neuron PROPERTIES PUBLIC CXX_STANDARD 14 CXX_STANDARD_REQUIRED ) -if(NEO_ENABLE_PLUGIN_SUPPORT) - target_compile_definitions(neuron PUBLIC NEO_ENABLE_PLUGIN_SUPPORT) +if(NEO_USE_STD_ATOMIC) + target_compile_definitions(neuron PUBLIC NEO_USE_STD_ATOMIC) endif() 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_include_directories("${name}_test_exe" PRIVATE ${INCLUDE_DIRS}) target_link_libraries("${name}_test_exe" neuron) target_link_libraries("${name}_test_exe" gtest gtest_main) add_test(NAME "${name}_test" COMMAND "${name}_test_exe") @@ -46,18 +30,23 @@ if(NEO_BUILD_TESTS) add_subdirectory(vendor/googletest) - 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_USE_STD_ATOMIC) + add_neuron_test(parameter_atomic tests/abstractions/parameter_test_atomic.cpp) + else() + add_neuron_test(parameter tests/abstractions/parameter_test.cpp) + 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) + endif() enable_testing() endif() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") -target_include_directories(neuron PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src PRIVATE ${INCLUDE_DIR}) +target_include_directories(neuron PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src PRIVATE ${INCLUDE_DIRS}) diff --git a/Makefile b/Makefile index c08e05b..19613f5 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ MODULE_DIR = src # Header only modules are listed commented out # below the others. +ABSTRACTIONS_MOD_DIR = abstractions +ABSTRACTIONS_MODULES = \ + AUDIO_MOD_DIR = audio AUDIO_MODULES = \ @@ -112,6 +115,7 @@ C_DEFS = \ C_INCLUDES = \ -I$(MODULE_DIR) \ +-I$(MODULE_DIR)/$(ABSTRACTIONS_MOD_DIR) \ -I$(MODULE_DIR)/$(AUDIO_MOD_DIR) \ -I$(MODULE_DIR)/$(GENERATOR_MOD_DIR) \ -I$(MODULE_DIR)/$(MODULATOR_MOD_DIR) \ diff --git a/scripts/test.sh b/scripts/test.sh index 589e6fb..32e5ee2 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_USE_STD_ATOMIC=ON" +fi + if [ $BUILD_TESTS == "true" ]; then rm -rf "$TARGET_DIR" mkdir -p "$TARGET_DIR" cd "$TARGET_DIR" || exit 1 - cmake -DNEO_BUILD_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/abstractions/generator.h b/src/abstractions/generator.h new file mode 100644 index 0000000..e0629a3 --- /dev/null +++ b/src/abstractions/generator.h @@ -0,0 +1,31 @@ +#pragma once + +#include "audio/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/src/abstractions/modulator.h b/src/abstractions/modulator.h new file mode 100644 index 0000000..f26d175 --- /dev/null +++ b/src/abstractions/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/src/abstractions/neuron.h b/src/abstractions/neuron.h new file mode 100644 index 0000000..48bba6a --- /dev/null +++ b/src/abstractions/neuron.h @@ -0,0 +1,32 @@ +#pragma once + +#ifdef NEO_USE_STD_ATOMIC +#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_USE_STD_ATOMIC + /** + * 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/src/utilities/parameter.h b/src/abstractions/parameter.h similarity index 53% rename from src/utilities/parameter.h rename to src/abstractions/parameter.h index 8b204e6..ed0c120 100644 --- a/src/utilities/parameter.h +++ b/src/abstractions/parameter.h @@ -1,17 +1,27 @@ #pragma once -#ifdef NEO_ENABLE_PLUGIN_SUPPORT +#ifdef NEO_USE_STD_ATOMIC #include #endif namespace neuron { -#ifdef NEO_ENABLE_PLUGIN_SUPPORT + +#ifdef NEO_USE_STD_ATOMIC + /** + * A read-only parameter used by a DSP component to allow more + * control and flexibility in shaping its sound. + */ template class Parameter { public: - Parameter(std::atomic* ptr = nullptr) + explicit Parameter(T value) + { + std::atomic_init(m_parameter, value); + } + + explicit Parameter(std::atomic* source) { - m_parameter = ptr; + m_parameter = source; } ~Parameter() @@ -19,10 +29,17 @@ namespace neuron { m_parameter = nullptr; } - // Attach a parameter (e.g. via an atomic float pointer) - void AttachSource(std::atomic* ptr) + /** + * 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 = ptr; + m_parameter = source; } operator T() const @@ -30,10 +47,9 @@ namespace neuron { return m_parameter->load(); } - T operator=(T value) const + Parameter& operator=(T /* value */) { - m_parameter->store(value); - return value; + return *this; } T operator+(T value) const @@ -53,7 +69,7 @@ namespace neuron { T operator/(T value) const { - if (value == 0.0) { + if (value == 0.0f) { return value; } else { return m_parameter->load() / value; @@ -61,13 +77,23 @@ namespace neuron { } private: - std::atomic* m_parameter = nullptr; + /** + * 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: - Parameter(T value = 0.0) + explicit Parameter(T value = 0.0f) { m_parameter = value; } @@ -79,9 +105,10 @@ namespace neuron { return m_parameter; } - T operator=(T value) const + Parameter& operator=(T value) { m_parameter = value; + return *this; } T operator+(T value) const @@ -112,4 +139,5 @@ namespace neuron { T m_parameter; }; #endif + } diff --git a/src/abstractions/processor.h b/src/abstractions/processor.h new file mode 100644 index 0000000..45dab46 --- /dev/null +++ b/src/abstractions/processor.h @@ -0,0 +1,30 @@ +#pragma once + +#include "audio/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/src/audio/context.h b/src/audio/context.h index 53096a6..4b911fc 100644 --- a/src/audio/context.h +++ b/src/audio/context.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace neuron { @@ -20,4 +20,5 @@ namespace neuron { * channel configuration, and a buffer size of 16 samples. */ static Context DEFAULT_CONTEXT = { 44100, 2, 16 }; + } diff --git a/src/generators/oscillator.cpp b/src/generators/oscillator.cpp index 426950e..b86730b 100644 --- a/src/generators/oscillator.cpp +++ b/src/generators/oscillator.cpp @@ -1,3 +1,5 @@ +#include + #include "generators/oscillator.h" using namespace neuron; @@ -5,6 +7,7 @@ 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,6 +27,19 @@ Sample Oscillator::Generate() return SineToWaveform(value, m_waveform); } +#ifdef NEO_USE_STD_ATOMIC +void Oscillator::AttachParameterToSourceImpl(OscillatorParameter parameter, std::atomic* source) +{ + switch (parameter) { + case OscillatorParameter::FREQUENCY: + p_frequency.AttachSource(source); + break; + default: + break; + } +} +#endif + void Oscillator::Reset(float phase) { float clampedPhase = clamp(phase, 0.0f, (float)WAVETABLE_SIZE); @@ -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 * (float)WAVETABLE_SIZE / (float)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; } } @@ -59,7 +76,7 @@ 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); + m_wavetable[idx] = sin(phase); } } @@ -76,7 +93,7 @@ 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 truncatedIdxWeight = 1.0f - nextIdxWeight; diff --git a/src/generators/oscillator.h b/src/generators/oscillator.h index 92c4ac5..3e52e7c 100644 --- a/src/generators/oscillator.h +++ b/src/generators/oscillator.h @@ -1,9 +1,9 @@ #pragma once -#include - +#include "abstractions/generator.h" +#include "abstractions/neuron.h" +#include "abstractions/parameter.h" #include "audio/context.h" -#include "audio/sample.h" #include "audio/waveform.h" #include "utilities/arithmetic.h" @@ -11,11 +11,15 @@ namespace neuron { const size_t WAVETABLE_SIZE = 256; + enum class OscillatorParameter { + FREQUENCY, + }; + /** * The Oscillator class creates an audio signal * with a basic waveform. */ - class Oscillator { + class Oscillator : public Generator, public Neuron { public: /** * Creates an oscillator generator. @@ -24,21 +28,13 @@ namespace neuron { * @param frequency The initial frequency of the oscillator. * @return Oscillator */ - Oscillator(Context& context = DEFAULT_CONTEXT, float frequency = 440.0f, Waveform waveform = Waveform::SINE); + explicit 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. @@ -73,14 +69,27 @@ namespace neuron { */ void DetachFollower(); + protected: + friend class Generator; + Sample GenerateImpl(); + +#ifdef NEO_USE_STD_ATOMIC + 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; diff --git a/src/modulators/adsr.cpp b/src/modulators/adsr.cpp index 120d62d..abfc5f7 100644 --- a/src/modulators/adsr.cpp +++ b/src/modulators/adsr.cpp @@ -4,15 +4,14 @@ using namespace neuron; AdsrEnvelopeModulator::AdsrEnvelopeModulator(Context& context, AdsrEnvelope envelope) : m_context(context) - , m_envelope(envelope) + , p_attack(envelope.attack) + , p_decay(envelope.decay) + , p_sustain(envelope.sustain) + , p_release(envelope.release) { } -AdsrEnvelopeModulator::~AdsrEnvelopeModulator() -{ -} - -float AdsrEnvelopeModulator::Modulate() +float AdsrEnvelopeModulator::ModulateImpl() { float position = (float)m_samplesSinceLastStage * (1000.0f / m_context.sampleRate); @@ -27,19 +26,19 @@ float AdsrEnvelopeModulator::Modulate() float value; switch (m_stage) { case AdsrStage::ATTACK: - value = position / m_envelope.attack; - Update(m_envelope.attack, AdsrStage::DECAY, true); + value = position / p_attack; + Update(p_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); + value = (((p_sustain - 1.0f) / p_decay) * position) + 1.0f; + Update(p_decay, AdsrStage::SUSTAIN, true); break; case AdsrStage::SUSTAIN: - value = m_envelope.sustain; + value = p_sustain; break; case AdsrStage::RELEASE: - value = ((-m_envelope.sustain / m_envelope.release) * position) + m_envelope.sustain; - Update(m_envelope.release, AdsrStage::IDLE, true); + value = ((-p_sustain / p_release) * position) + p_sustain; + Update(p_release, AdsrStage::IDLE, true); break; case AdsrStage::IDLE: default: @@ -49,6 +48,28 @@ float AdsrEnvelopeModulator::Modulate() return value; } +#ifdef NEO_USE_STD_ATOMIC +void AdsrEnvelopeModulator::AttachParameterToSourceImpl(AdsrParameter parameter, std::atomic* source) +{ + switch (parameter) { + case AdsrParameter::ATTACK: + p_attack.AttachSource(source); + break; + case AdsrParameter::DECAY: + p_decay.AttachSource(source); + break; + case AdsrParameter::SUSTAIN: + p_sustain.AttachSource(source); + break; + case AdsrParameter::RELEASE: + p_release.AttachSource(source); + break; + default: + break; + } +} +#endif + void AdsrEnvelopeModulator::Trigger() { m_stage = AdsrStage::ATTACK; @@ -69,25 +90,25 @@ void AdsrEnvelopeModulator::Reset() void AdsrEnvelopeModulator::SetAttackTime(float attackTimeMs) { - m_envelope.attack = attackTimeMs; - Update(m_envelope.attack, AdsrStage::DECAY, false); + p_attack = attackTimeMs; + Update(p_attack, AdsrStage::DECAY, false); } void AdsrEnvelopeModulator::SetDecayTime(float decayTimeMs) { - m_envelope.decay = decayTimeMs; - Update(m_envelope.decay, AdsrStage::SUSTAIN, false); + p_decay = decayTimeMs; + Update(p_decay, AdsrStage::SUSTAIN, false); } void AdsrEnvelopeModulator::SetSustainLevel(float sustainLevel) { - m_envelope.sustain = sustainLevel; + p_sustain = sustainLevel; } void AdsrEnvelopeModulator::SetReleaseTime(float releaseTimeMs) { - m_envelope.release = releaseTimeMs; - Update(m_envelope.release, AdsrStage::IDLE, false); + p_release = releaseTimeMs; + Update(p_release, AdsrStage::IDLE, false); } void AdsrEnvelopeModulator::Update(float stageDuration, AdsrStage nextStage, bool incrementSampleCount) diff --git a/src/modulators/adsr.h b/src/modulators/adsr.h index 60c5ee4..fbd471f 100644 --- a/src/modulators/adsr.h +++ b/src/modulators/adsr.h @@ -1,5 +1,8 @@ #pragma once +#include "abstractions/modulator.h" +#include "abstractions/neuron.h" +#include "abstractions/parameter.h" #include "audio/context.h" namespace neuron { @@ -15,6 +18,13 @@ namespace neuron { float release; }; + enum class AdsrParameter { + ATTACK, + DECAY, + SUSTAIN, + RELEASE, + }; + /** * The stages of an ADSR envelope, including an "idle" stage when not in use. */ @@ -31,29 +41,20 @@ namespace neuron { }; /** - * The ADsrEnvelopeModulator class is a modulation source + * The AdsrEnvelopeModulator class is a modulation source * that is based off of an ADSR envelope generator. */ - class AdsrEnvelopeModulator { + 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 */ - 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(); + explicit AdsrEnvelopeModulator(Context& context = DEFAULT_CONTEXT, AdsrEnvelope envelope = DEFAULT_ADSR_ENVELOPE); /** * Starts the envelope from its attack phase. @@ -98,13 +99,26 @@ namespace neuron { */ void SetReleaseTime(float releaseTimeMs); + protected: + friend class Modulator; + float ModulateImpl(); + +#ifdef NEO_USE_STD_ATOMIC + 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; - AdsrEnvelope m_envelope; + 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/src/neuron.h b/src/neuron.h index d1aad5d..bf1ef32 100644 --- a/src/neuron.h +++ b/src/neuron.h @@ -8,6 +8,13 @@ #ifndef NEURON_LIB_H #define NEURON_LIB_H +// ABSTRACTIONS +#include "abstractions/generator.h" +#include "abstractions/modulator.h" +#include "abstractions/neuron.h" +#include "abstractions/parameter.h" +#include "abstractions/processor.h" + // AUDIO #include "audio/context.h" #include "audio/sample.h" @@ -28,7 +35,6 @@ #include "utilities/arithmetic.h" #include "utilities/logger.h" #include "utilities/midi.h" -#include "utilities/parameter.h" #include "utilities/timer.h" #endif diff --git a/src/processors/effects/saturator.cpp b/src/processors/effects/saturator.cpp index 8c6d8b3..606526b 100644 --- a/src/processors/effects/saturator.cpp +++ b/src/processors/effects/saturator.cpp @@ -1,23 +1,46 @@ #include "processors/effects/saturator.h" +#include "utilities/arithmetic.h" using namespace neuron; -Sample Saturator::Process(const Sample input) +Saturator::Saturator() + : p_saturation(1.0f) + , p_symmetry(1.0f) { - float output = tanh((float)input * m_saturation); +} + +Sample Saturator::ProcessImpl(Sample input) +{ + float output = tanh(input * p_saturation); if (input < 0.0f) { - output = (Sample)(input * (1.0f - m_symmetry)) + (output * m_symmetry); + output = (input * (1.0f - p_symmetry)) + (output * p_symmetry); } - return (Sample)clamp(output, -1.0f, 1.0f); + return clamp(output, -1.0f, 1.0f); +} + +#ifdef NEO_USE_STD_ATOMIC +void Saturator::AttachParameterToSourceImpl(SaturatorParameter parameter, std::atomic* source) +{ + switch (parameter) { + case SaturatorParameter::SATURATION: + p_saturation.AttachSource(source); + break; + case SaturatorParameter::SYMMETRY: + p_symmetry.AttachSource(source); + break; + default: + break; + } } +#endif void Saturator::SetSaturation(float saturation) { - m_saturation = saturation < 1.0f ? 1.0f : saturation; + p_saturation = saturation < 1.0f ? 1.0f : saturation; } void Saturator::SetSymmetry(float symmetry) { - m_symmetry = clamp(symmetry, 0.0f, 1.0f); + p_symmetry = clamp(symmetry, 0.0f, 1.0f); } diff --git a/src/processors/effects/saturator.h b/src/processors/effects/saturator.h index e485e11..c399d63 100644 --- a/src/processors/effects/saturator.h +++ b/src/processors/effects/saturator.h @@ -1,35 +1,34 @@ #pragma once +#include "abstractions/neuron.h" +#include "abstractions/parameter.h" +#include "abstractions/processor.h" #include "audio/sample.h" -#include "utilities/arithmetic.h" namespace neuron { + enum class SaturatorParameter { + SATURATION, + SYMMETRY, + }; + /** * The Saturator class applies a tape saturation * algorithm to audio signals. */ - class Saturator { + class Saturator : public Processor, public Neuron { public: /** * Creates a default saturator processor. * * @return Saturator */ - 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); + ~Saturator() = default; /** * Sets the saturation level, which boosts the signal before @@ -51,9 +50,17 @@ namespace neuron { */ void SetSymmetry(float symmetry); - private: - float m_saturation = 1.0f; - float m_symmetry = 1.0f; + Parameter p_saturation; + Parameter p_symmetry; + + protected: + friend class Processor; + Sample ProcessImpl(Sample input); + +#ifdef NEO_USE_STD_ATOMIC + friend class Neuron; + void AttachParameterToSourceImpl(SaturatorParameter parameter, std::atomic* source); +#endif }; } \ No newline at end of file diff --git a/src/processors/effects/wavefolder.cpp b/src/processors/effects/wavefolder.cpp index e04ebb3..2f86a43 100644 --- a/src/processors/effects/wavefolder.cpp +++ b/src/processors/effects/wavefolder.cpp @@ -1,24 +1,51 @@ #include "processors/effects/wavefolder.h" +#include "utilities/arithmetic.h" using namespace neuron; -Sample Wavefolder::Process(const Sample input) +Wavefolder::Wavefolder() + : p_inputGain(1.0f) + , p_threshold(1.0f) + , p_symmetry(1.0f) { - float output = (float)input * p_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); +} + +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 = (Sample)(input * (1.0f - m_symmetry)) + (output * m_symmetry); + output = input * (1.0f - p_symmetry) + output * p_symmetry; } - return (Sample)clamp(output, -1.0f, 1.0f); + return clamp(output, -1.0f, 1.0f); +} + +#ifdef NEO_USE_STD_ATOMIC +void Wavefolder::AttachParameterToSourceImpl(const WavefolderParameter parameter, std::atomic* source) +{ + switch (parameter) { + case WavefolderParameter::INPUT_GAIN: + p_inputGain.AttachSource(source); + break; + case WavefolderParameter::THRESHOLD: + p_threshold.AttachSource(source); + break; + case WavefolderParameter::SYMMETRY: + p_symmetry.AttachSource(source); + break; + default: + break; + } } +#endif void Wavefolder::SetInputGain(float gain) { @@ -27,10 +54,10 @@ void Wavefolder::SetInputGain(float gain) void Wavefolder::SetThreshold(float threshold) { - m_threshold = threshold; + p_threshold = threshold; } void Wavefolder::SetSymmetry(float symmetry) { - m_symmetry = clamp(symmetry, 0.0f, 1.0f); + p_symmetry = clamp(symmetry, 0.0f, 1.0f); } diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h index e591cb7..cf6d398 100644 --- a/src/processors/effects/wavefolder.h +++ b/src/processors/effects/wavefolder.h @@ -1,33 +1,27 @@ #pragma once -#include "audio/sample.h" -#include "utilities/arithmetic.h" -#include "utilities/parameter.h" +#include "abstractions/neuron.h" +#include "abstractions/parameter.h" +#include "abstractions/processor.h" namespace neuron { + + enum class WavefolderParameter { + INPUT_GAIN, + THRESHOLD, + SYMMETRY, + }; + /** * The Wavefolder class applies a wavefolding * algorithm to audio signals. */ - class Wavefolder { + class Wavefolder : public Processor, public Neuron { public: /** * Creates a default wavefolder processor. */ - Wavefolder() = default; - - /** - * Frees any memory allocated by the wavefolder. - */ - ~Wavefolder() = default; - - /** - * Applies a wavefolding algorithm to an input sample. - * - * @param input The input sample to be processed. - * @return Sample - */ - Sample Process(const Sample input); + Wavefolder(); /** * Sets the input gain level, which boosts the signal before @@ -54,11 +48,19 @@ namespace neuron { */ void SetSymmetry(float symmetry); - public: - Parameter p_inputGain; + protected: + friend class Processor; + Sample ProcessImpl(Sample input); + +#ifdef NEO_USE_STD_ATOMIC + friend class Neuron; + void AttachParameterToSourceImpl(const WavefolderParameter parameter, std::atomic* source); +#endif private: - float m_threshold = 1.0f; - float m_symmetry = 1.0f; + Parameter p_inputGain; + Parameter p_threshold; + Parameter p_symmetry; }; + } diff --git a/src/processors/filters/filter.cpp b/src/processors/filters/filter.cpp index a778b0b..3da596d 100644 --- a/src/processors/filters/filter.cpp +++ b/src/processors/filters/filter.cpp @@ -1,31 +1,45 @@ #include "processors/filters/filter.h" +#include "utilities/arithmetic.h" using namespace neuron; -Filter::Filter(neuron::Context& context, float cutoffFrequency) +Filter::Filter(Context& context, float cutoffFrequency) : m_context(context) - , m_cutoffFrequency(cutoffFrequency) , m_previousOutput(0.0f) + , p_cutoffFrequency(cutoffFrequency) { SetCutoffFrequency(cutoffFrequency); } -Sample Filter::Process(const neuron::Sample input) +Sample Filter::ProcessImpl(Sample input) { - float output = m_alpha * (float)input + (1.0f - m_alpha) * (float)m_previousOutput; - m_previousOutput = (Sample)output; - return (Sample)output; + float output = m_alpha * input + (1.0f - m_alpha) * m_previousOutput; + m_previousOutput = output; + return output; } +#ifdef NEO_USE_STD_ATOMIC +void Filter::AttachParameterToSourceImpl(FilterParameter parameter, std::atomic* source) +{ + switch (parameter) { + case FilterParameter::CUTOFF_FREQUENCY: + p_cutoffFrequency.AttachSource(source); + break; + default: + break; + } +} +#endif + void Filter::SetCutoffFrequency(float frequency) { - m_cutoffFrequency = clamp(frequency, FILTER_CUTOFF_FREQ_MIN, FILTER_CUTOFF_FREQ_MAX); + p_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 cutoffResponse = 1.0f / (2.0f * PI * p_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 index b68a112..4725fd1 100644 --- a/src/processors/filters/filter.h +++ b/src/processors/filters/filter.h @@ -1,18 +1,25 @@ #pragma once +#include "abstractions/neuron.h" +#include "abstractions/parameter.h" +#include "abstractions/processor.h" #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; + enum class FilterParameter { + CUTOFF_FREQUENCY, + }; + /** * The Filter class applies a simple low-pass filter * to audio signals. */ - class Filter { + class Filter : public Processor, public Neuron { public: /** * Creates a filter processor. @@ -21,22 +28,9 @@ namespace neuron { * @param cutoffFrequency The initial cutoff frequency of the filter. * @return Filter */ - Filter(Context& context = DEFAULT_CONTEXT, + explicit 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. * @@ -44,12 +38,22 @@ namespace neuron { */ void SetCutoffFrequency(float frequency); + Parameter p_cutoffFrequency; + + protected: + friend class Processor; + Sample ProcessImpl(Sample input); + +#ifdef NEO_USE_STD_ATOMIC + friend class Neuron; + void AttachParameterToSourceImpl(FilterParameter parameter, std::atomic* source); +#endif + private: void CalculateAlpha(); Context& m_context; - float m_cutoffFrequency; float m_alpha; Sample m_previousOutput; }; diff --git a/src/utilities/midi.h b/src/utilities/midi.h index 5ad6845..c798590 100644 --- a/src/utilities/midi.h +++ b/src/utilities/midi.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "utilities/arithmetic.h" namespace neuron { diff --git a/src/utilities/timer.h b/src/utilities/timer.h index 3b719f9..949b420 100644 --- a/src/utilities/timer.h +++ b/src/utilities/timer.h @@ -1,3 +1,5 @@ +#pragma once + #include #include "utilities/logger.h" diff --git a/tests/abstractions/parameter_test.cpp b/tests/abstractions/parameter_test.cpp new file mode 100644 index 0000000..74084ee --- /dev/null +++ b/tests/abstractions/parameter_test.cpp @@ -0,0 +1,41 @@ +#include + +#include "neuron.h" + +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/abstractions/parameter_test_atomic.cpp b/tests/abstractions/parameter_test_atomic.cpp new file mode 100644 index 0000000..b42bb97 --- /dev/null +++ b/tests/abstractions/parameter_test_atomic.cpp @@ -0,0 +1,53 @@ +#include +#include + +#include "neuron.h" + +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_FLOAT_EQ(static_cast(param), 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/vendor/googletest b/vendor/googletest index df1544b..35d0c36 160000 --- a/vendor/googletest +++ b/vendor/googletest @@ -1 +1 @@ -Subproject commit df1544bcee0c7ce35cd5ea0b3eb8cc81855a4140 +Subproject commit 35d0c365609296fa4730d62057c487e3cfa030ff From 41d22102c3304b37b6bec25f9c1e54eb8de89cb9 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 02:18:56 -0600 Subject: [PATCH 09/32] Change preprocessor var name --- CMakeLists.txt | 8 ++++---- scripts/test.sh | 2 +- src/abstractions/neuron.h | 4 ++-- src/abstractions/parameter.h | 4 ++-- src/generators/oscillator.cpp | 2 +- src/generators/oscillator.h | 2 +- src/modulators/adsr.cpp | 2 +- src/modulators/adsr.h | 2 +- src/processors/effects/saturator.cpp | 2 +- src/processors/effects/saturator.h | 2 +- src/processors/effects/wavefolder.cpp | 2 +- src/processors/effects/wavefolder.h | 2 +- src/processors/filters/filter.cpp | 2 +- src/processors/filters/filter.h | 2 +- 14 files changed, 19 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e348d9d..ebbcc9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.14) -option(NEO_USE_STD_ATOMIC "Enable atomic variables" 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) @@ -15,8 +15,8 @@ set_target_properties(neuron PROPERTIES PUBLIC CXX_STANDARD_REQUIRED ) -if(NEO_USE_STD_ATOMIC) - target_compile_definitions(neuron PUBLIC NEO_USE_STD_ATOMIC) +if(NEO_PLUGIN_SUPPORT) + target_compile_definitions(neuron PUBLIC NEO_PLUGIN_SUPPORT) endif() if(NEO_BUILD_TESTS) @@ -30,7 +30,7 @@ if(NEO_BUILD_TESTS) add_subdirectory(vendor/googletest) - if(NEO_USE_STD_ATOMIC) + if(NEO_PLUGIN_SUPPORT) add_neuron_test(parameter_atomic tests/abstractions/parameter_test_atomic.cpp) else() add_neuron_test(parameter tests/abstractions/parameter_test.cpp) diff --git a/scripts/test.sh b/scripts/test.sh index 32e5ee2..cedc4a2 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -26,7 +26,7 @@ done CMAKE_FLAGS="-DCMAKE_BUILD_TYPE=Release -DNEO_BUILD_TESTS=ON" if [ "$PLUGIN_SUPPORT" == "true" ]; then - CMAKE_FLAGS+=" -DNEO_USE_STD_ATOMIC=ON" + CMAKE_FLAGS+=" -DNEO_PLUGIN_SUPPORT=ON" fi if [ $BUILD_TESTS == "true" ]; then diff --git a/src/abstractions/neuron.h b/src/abstractions/neuron.h index 48bba6a..04f97fd 100644 --- a/src/abstractions/neuron.h +++ b/src/abstractions/neuron.h @@ -1,6 +1,6 @@ #pragma once -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT #include #endif @@ -18,7 +18,7 @@ namespace neuron { */ ~Neuron() = default; -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT /** * Attach a source via an atomic pointer to a given parameter. */ diff --git a/src/abstractions/parameter.h b/src/abstractions/parameter.h index ed0c120..5b6af01 100644 --- a/src/abstractions/parameter.h +++ b/src/abstractions/parameter.h @@ -1,12 +1,12 @@ #pragma once -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT #include #endif namespace neuron { -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT /** * A read-only parameter used by a DSP component to allow more * control and flexibility in shaping its sound. diff --git a/src/generators/oscillator.cpp b/src/generators/oscillator.cpp index b86730b..3f5624c 100644 --- a/src/generators/oscillator.cpp +++ b/src/generators/oscillator.cpp @@ -27,7 +27,7 @@ Sample Oscillator::GenerateImpl() return SineToWaveform(value, m_waveform); } -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT void Oscillator::AttachParameterToSourceImpl(OscillatorParameter parameter, std::atomic* source) { switch (parameter) { diff --git a/src/generators/oscillator.h b/src/generators/oscillator.h index 3e52e7c..680f264 100644 --- a/src/generators/oscillator.h +++ b/src/generators/oscillator.h @@ -73,7 +73,7 @@ namespace neuron { friend class Generator; Sample GenerateImpl(); -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT friend class Neuron; void AttachParameterToSourceImpl(OscillatorParameter parameter, std::atomic* source); #endif diff --git a/src/modulators/adsr.cpp b/src/modulators/adsr.cpp index abfc5f7..3d9a574 100644 --- a/src/modulators/adsr.cpp +++ b/src/modulators/adsr.cpp @@ -48,7 +48,7 @@ float AdsrEnvelopeModulator::ModulateImpl() return value; } -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT void AdsrEnvelopeModulator::AttachParameterToSourceImpl(AdsrParameter parameter, std::atomic* source) { switch (parameter) { diff --git a/src/modulators/adsr.h b/src/modulators/adsr.h index fbd471f..ed57523 100644 --- a/src/modulators/adsr.h +++ b/src/modulators/adsr.h @@ -103,7 +103,7 @@ namespace neuron { friend class Modulator; float ModulateImpl(); -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT friend class Neuron; void AttachParameterToSourceImpl(AdsrParameter parameter, std::atomic* source); #endif diff --git a/src/processors/effects/saturator.cpp b/src/processors/effects/saturator.cpp index 606526b..087b87e 100644 --- a/src/processors/effects/saturator.cpp +++ b/src/processors/effects/saturator.cpp @@ -19,7 +19,7 @@ Sample Saturator::ProcessImpl(Sample input) return clamp(output, -1.0f, 1.0f); } -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT void Saturator::AttachParameterToSourceImpl(SaturatorParameter parameter, std::atomic* source) { switch (parameter) { diff --git a/src/processors/effects/saturator.h b/src/processors/effects/saturator.h index c399d63..0e42d24 100644 --- a/src/processors/effects/saturator.h +++ b/src/processors/effects/saturator.h @@ -57,7 +57,7 @@ namespace neuron { friend class Processor; Sample ProcessImpl(Sample input); -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT friend class Neuron; void AttachParameterToSourceImpl(SaturatorParameter parameter, std::atomic* source); #endif diff --git a/src/processors/effects/wavefolder.cpp b/src/processors/effects/wavefolder.cpp index 2f86a43..f4201cf 100644 --- a/src/processors/effects/wavefolder.cpp +++ b/src/processors/effects/wavefolder.cpp @@ -28,7 +28,7 @@ Sample Wavefolder::ProcessImpl(Sample input) return clamp(output, -1.0f, 1.0f); } -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT void Wavefolder::AttachParameterToSourceImpl(const WavefolderParameter parameter, std::atomic* source) { switch (parameter) { diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h index cf6d398..64730b0 100644 --- a/src/processors/effects/wavefolder.h +++ b/src/processors/effects/wavefolder.h @@ -52,7 +52,7 @@ namespace neuron { friend class Processor; Sample ProcessImpl(Sample input); -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT friend class Neuron; void AttachParameterToSourceImpl(const WavefolderParameter parameter, std::atomic* source); #endif diff --git a/src/processors/filters/filter.cpp b/src/processors/filters/filter.cpp index 3da596d..37c50e4 100644 --- a/src/processors/filters/filter.cpp +++ b/src/processors/filters/filter.cpp @@ -18,7 +18,7 @@ Sample Filter::ProcessImpl(Sample input) return output; } -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT void Filter::AttachParameterToSourceImpl(FilterParameter parameter, std::atomic* source) { switch (parameter) { diff --git a/src/processors/filters/filter.h b/src/processors/filters/filter.h index 4725fd1..ae4f1e9 100644 --- a/src/processors/filters/filter.h +++ b/src/processors/filters/filter.h @@ -44,7 +44,7 @@ namespace neuron { friend class Processor; Sample ProcessImpl(Sample input); -#ifdef NEO_USE_STD_ATOMIC +#ifdef NEO_PLUGIN_SUPPORT friend class Neuron; void AttachParameterToSourceImpl(FilterParameter parameter, std::atomic* source); #endif From b93e0327451985c50dd47358b584140fe6f4e827 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 02:22:10 -0600 Subject: [PATCH 10/32] Format files --- .github/workflows/ci.build.yml | 8 ++++-- .github/workflows/ci.test.yml | 48 ++++++++++++++++++---------------- CMakeLists.txt | 14 +++++----- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.build.yml b/.github/workflows/ci.build.yml index a9931c0..f0250c5 100644 --- a/.github/workflows/ci.build.yml +++ b/.github/workflows/ci.build.yml @@ -3,14 +3,18 @@ name: Build on: pull_request: push: - branches: ['main', 'develop'] + branches: [ 'main', 'develop' ] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true 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" diff --git a/.github/workflows/ci.test.yml b/.github/workflows/ci.test.yml index 9e50d71..e579013 100644 --- a/.github/workflows/ci.test.yml +++ b/.github/workflows/ci.test.yml @@ -1,31 +1,35 @@ name: Test on: - pull_request: - push: - branches: ['main', 'develop'] + pull_request: + push: + branches: [ 'main', 'develop' ] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true jobs: - GoogleTest: - runs-on: ubuntu-latest - steps: - - name: "Checkout" - uses: actions/checkout@v4 - with: - submodules: recursive + 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: "Prepare" + working-directory: ${{runner.workspace}}/neuron + run: mkdir -p target/test - - name: "Generate Makefile" - working-directory: ${{runner.workspace}}/neuron/target/test - run: cmake -DCMAKE_BUILD_TYPE=Release -DNEO_BUILD_TESTS=ON ../../ + - name: "Generate Makefile" + working-directory: ${{runner.workspace}}/neuron/target/test + run: cmake -DCMAKE_BUILD_TYPE=Release -DNEO_BUILD_TESTS=ON ../../ - - name: "Build" - working-directory: ${{runner.workspace}}/neuron/target/test - run: make + - name: "Build" + working-directory: ${{runner.workspace}}/neuron/target/test + run: make - - name: "Test" - working-directory: ${{runner.workspace}}/neuron/target/test - run: ctest + - name: "Test" + working-directory: ${{runner.workspace}}/neuron/target/test + run: ctest diff --git a/CMakeLists.txt b/CMakeLists.txt index ebbcc9f..b2fc089 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,11 +15,11 @@ set_target_properties(neuron PROPERTIES PUBLIC CXX_STANDARD_REQUIRED ) -if(NEO_PLUGIN_SUPPORT) +if (NEO_PLUGIN_SUPPORT) target_compile_definitions(neuron PUBLIC NEO_PLUGIN_SUPPORT) -endif() +endif () -if(NEO_BUILD_TESTS) +if (NEO_BUILD_TESTS) function(add_neuron_test name src) add_executable("${name}_test_exe" ${src}) target_include_directories("${name}_test_exe" PRIVATE ${INCLUDE_DIRS}) @@ -30,9 +30,9 @@ if(NEO_BUILD_TESTS) add_subdirectory(vendor/googletest) - if(NEO_PLUGIN_SUPPORT) + if (NEO_PLUGIN_SUPPORT) add_neuron_test(parameter_atomic tests/abstractions/parameter_test_atomic.cpp) - else() + else () add_neuron_test(parameter tests/abstractions/parameter_test.cpp) add_neuron_test(waveform tests/audio/waveform_test.cpp) add_neuron_test(oscillator tests/generators/oscillator_test.cpp) @@ -42,10 +42,10 @@ if(NEO_BUILD_TESTS) 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) - endif() + endif () enable_testing() -endif() +endif () set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") From 8af73b702408663fd4cb4e6c816f66dddd6fc686 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 10:30:49 -0600 Subject: [PATCH 11/32] Add to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 856bbb7..69e5e05 100644 --- a/README.md +++ b/README.md @@ -89,3 +89,5 @@ for(size_t idx = 0; idx < 128; idx++) { buffer[idx] = (float)osc.Generate(); } ``` + +:information_source: Enable the `NEO_PLUGIN_SUPPORT` compile option in CMake to include necessary bits for JUCE integration. From 8f270794f8fbe87f488612c288f92530ffafbda4 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 12:00:56 -0600 Subject: [PATCH 12/32] Update build script --- scripts/build.sh | 2 +- src/neuron.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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/src/neuron.h b/src/neuron.h index bf1ef32..5c6a39f 100644 --- a/src/neuron.h +++ b/src/neuron.h @@ -5,6 +5,8 @@ * * Author: Matthew Maxwell, 2024 */ +#pragma once + #ifndef NEURON_LIB_H #define NEURON_LIB_H From e2df7d84c37d6b1c54b0bb5d6a875a69e68387ac Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 12:47:32 -0600 Subject: [PATCH 13/32] Update things --- .github/workflows/ci.build.yml | 29 +++++++++++++++++++---------- .github/workflows/ci.test.yml | 29 +++++++++++++++++++---------- README.md | 2 +- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.build.yml b/.github/workflows/ci.build.yml index f0250c5..0ac27c6 100644 --- a/.github/workflows/ci.build.yml +++ b/.github/workflows/ci.build.yml @@ -9,6 +9,10 @@ concurrency: group: ${{ github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash + jobs: CMake: runs-on: ${{matrix.os}} @@ -17,19 +21,24 @@ jobs: 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=${{ runner.workspace }}/neuron/target + echo "TARGET_PATH=$TARGET_PATH" >> $GITHUB_ENV + + - name: Prepare + run: | + mkdir -p "${{ env.TARGET_PATH }}" - - name: "Configure" - working-directory: ${{runner.workspace}}/neuron/target + - name: Configure + working-directory: "${{ env.TARGET_PATH }}" run: cmake ${{runner.workspace}}/neuron - - name: "Build" - working-directory: ${{runner.workspace}}/neuron/target + - name: Build + working-directory: "${{ env.TARGET_PATH }}" run: cmake --build . Make: @@ -39,7 +48,7 @@ jobs: os: [ ubuntu-latest, macos-latest, windows-latest ] steps: - - name: "Checkout" + - name: Checkout uses: actions/checkout@v4 - name: "Install ARM Toolchain" @@ -47,5 +56,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 e579013..55458c4 100644 --- a/.github/workflows/ci.test.yml +++ b/.github/workflows/ci.test.yml @@ -9,27 +9,36 @@ concurrency: group: ${{ github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash + jobs: GoogleTest: runs-on: ubuntu-latest steps: - - name: "Checkout" + - name: Checkout Repository uses: actions/checkout@v4 with: submodules: recursive - - name: "Prepare" - working-directory: ${{runner.workspace}}/neuron - run: mkdir -p target/test + - 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: ${{runner.workspace}}/neuron/target/test + - name: Generate Makefile + working-directory: "${{ env.TARGET_PATH }}" run: cmake -DCMAKE_BUILD_TYPE=Release -DNEO_BUILD_TESTS=ON ../../ - - name: "Build" - working-directory: ${{runner.workspace}}/neuron/target/test + - name: Build + working-directory: "${{ env.TARGET_PATH }}" run: make - - name: "Test" - working-directory: ${{runner.workspace}}/neuron/target/test + - name: Test + working-directory: "${{ env.TARGET_PATH }}" run: ctest diff --git a/README.md b/README.md index 69e5e05..699e855 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![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 From d023cd6bb82daf7a15a7da133de916002a11a0ac Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 12:58:05 -0600 Subject: [PATCH 14/32] Fix target paths' --- .github/workflows/ci.build.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.build.yml b/.github/workflows/ci.build.yml index 0ac27c6..423a83f 100644 --- a/.github/workflows/ci.build.yml +++ b/.github/workflows/ci.build.yml @@ -26,7 +26,7 @@ jobs: - name: Set Environment Variables run: | - TARGET_PATH=${{ runner.workspace }}/neuron/target + TARGET_PATH=${{ runner.workspace }}/neuron/target/release echo "TARGET_PATH=$TARGET_PATH" >> $GITHUB_ENV - name: Prepare @@ -34,12 +34,14 @@ jobs: mkdir -p "${{ env.TARGET_PATH }}" - name: Configure - working-directory: "${{ env.TARGET_PATH }}" - run: cmake ${{runner.workspace}}/neuron + run: | + cd "${{ env.TARGET_PATH }}" + cmake ../../ - name: Build - working-directory: "${{ env.TARGET_PATH }}" - run: cmake --build . + run: | + cd "${{ env.TARGET_PATH }}" + cmake --build . --parallel 4 Make: runs-on: ${{matrix.os}} From 2307fe618aafbf16f60c2b3b74fd03ef3eb25f33 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 12:59:39 -0600 Subject: [PATCH 15/32] Remove in-progress cancels' --- .github/workflows/ci.build.yml | 4 ---- .github/workflows/ci.test.yml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/.github/workflows/ci.build.yml b/.github/workflows/ci.build.yml index 423a83f..a314a02 100644 --- a/.github/workflows/ci.build.yml +++ b/.github/workflows/ci.build.yml @@ -5,10 +5,6 @@ on: push: branches: [ 'main', 'develop' ] -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - defaults: run: shell: bash diff --git a/.github/workflows/ci.test.yml b/.github/workflows/ci.test.yml index 55458c4..06932c7 100644 --- a/.github/workflows/ci.test.yml +++ b/.github/workflows/ci.test.yml @@ -5,10 +5,6 @@ on: push: branches: [ 'main', 'develop' ] -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - defaults: run: shell: bash From 44b7670ca3c40423bbaf97dd6ecba61b35b49fda Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 13:01:12 -0600 Subject: [PATCH 16/32] Remove extra path info --- .github/workflows/ci.build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.build.yml b/.github/workflows/ci.build.yml index a314a02..a022668 100644 --- a/.github/workflows/ci.build.yml +++ b/.github/workflows/ci.build.yml @@ -22,7 +22,7 @@ jobs: - name: Set Environment Variables run: | - TARGET_PATH=${{ runner.workspace }}/neuron/target/release + TARGET_PATH=target/release echo "TARGET_PATH=$TARGET_PATH" >> $GITHUB_ENV - name: Prepare From ded23a30ce2dbb88dbd6f20cfb8bdcee5255805b Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 13:08:18 -0600 Subject: [PATCH 17/32] Try different construction ordering --- src/processors/filters/filter.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/processors/filters/filter.cpp b/src/processors/filters/filter.cpp index 37c50e4..da9106d 100644 --- a/src/processors/filters/filter.cpp +++ b/src/processors/filters/filter.cpp @@ -5,8 +5,7 @@ using namespace neuron; Filter::Filter(Context& context, float cutoffFrequency) : m_context(context) - , m_previousOutput(0.0f) - , p_cutoffFrequency(cutoffFrequency) + , p_cutoffFrequency(cutoffFrequency), m_previousOutput(0.0f) { SetCutoffFrequency(cutoffFrequency); } From c200698e30a02723328e4f519b00c95025e5692d Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 13:11:23 -0600 Subject: [PATCH 18/32] Re-order again --- src/processors/filters/filter.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/processors/filters/filter.cpp b/src/processors/filters/filter.cpp index da9106d..c941c11 100644 --- a/src/processors/filters/filter.cpp +++ b/src/processors/filters/filter.cpp @@ -4,8 +4,7 @@ using namespace neuron; Filter::Filter(Context& context, float cutoffFrequency) - : m_context(context) - , p_cutoffFrequency(cutoffFrequency), m_previousOutput(0.0f) + : p_cutoffFrequency(cutoffFrequency), m_context(context), m_previousOutput(0.0f) { SetCutoffFrequency(cutoffFrequency); } From 20bffccab376ebdd0d782ef46c4bad9b2a376d5a Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 13:40:30 -0600 Subject: [PATCH 19/32] Remove C++ versioning for plugin builds --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b2fc089..8f896e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,8 @@ set_target_properties(neuron PROPERTIES PUBLIC if (NEO_PLUGIN_SUPPORT) target_compile_definitions(neuron PUBLIC NEO_PLUGIN_SUPPORT) +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") endif () if (NEO_BUILD_TESTS) @@ -47,6 +49,4 @@ if (NEO_BUILD_TESTS) enable_testing() endif () -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") - target_include_directories(neuron PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src PRIVATE ${INCLUDE_DIRS}) From b0b28bba617e52e7a13a749a79a0595215aa3d3f Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 13:42:53 -0600 Subject: [PATCH 20/32] Format code --- src/processors/filters/filter.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/processors/filters/filter.cpp b/src/processors/filters/filter.cpp index c941c11..7b946ea 100644 --- a/src/processors/filters/filter.cpp +++ b/src/processors/filters/filter.cpp @@ -4,7 +4,9 @@ using namespace neuron; Filter::Filter(Context& context, float cutoffFrequency) - : p_cutoffFrequency(cutoffFrequency), m_context(context), m_previousOutput(0.0f) + : p_cutoffFrequency(cutoffFrequency) + , m_context(context) + , m_previousOutput(0.0f) { SetCutoffFrequency(cutoffFrequency); } From 94e0f86785e9ee21b94eb2124aabc7b615e0d654 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 14:03:17 -0600 Subject: [PATCH 21/32] Don't set C++ standard for plugin builds --- CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f896e6..af9c7a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,15 +10,15 @@ file(GLOB_RECURSE SRC_FILES "src/*.cpp") add_library(neuron STATIC ${SRC_FILES}) -set_target_properties(neuron PROPERTIES PUBLIC - CXX_STANDARD 14 - CXX_STANDARD_REQUIRED -) - if (NEO_PLUGIN_SUPPORT) 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 () if (NEO_BUILD_TESTS) From e65e9fc3e46331c72b36f996036da7b100ed0250 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 15:20:01 -0600 Subject: [PATCH 22/32] Add c++ standard and osx arch --- CMakeLists.txt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index af9c7a5..7d35d8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,14 +11,18 @@ 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 + PUBLIC CXX_STANDARD 14 - CXX_STANDARD_REQUIRED - ) + CXX_STANDARD_REQUIRED) endif () if (NEO_BUILD_TESTS) From c725fd044f3529b20a9c9dbd28c8986b5d481db9 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 15:24:24 -0600 Subject: [PATCH 23/32] Update README --- CMakeLists.txt | 2 +- README.md | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d35d8c..2172e24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ add_library(neuron STATIC ${SRC_FILES}) if (NEO_PLUGIN_SUPPORT) set_target_properties(neuron PROPERTIES - PUBLIC + PUBLIC CXX_STANDARD 23 CXX_STANDARD_REQUIRED OSX_ARCHITECTURES "x86_64;arm64") diff --git a/README.md b/README.md index 699e855..7a45ea2 100644 --- a/README.md +++ b/README.md @@ -90,4 +90,11 @@ for(size_t idx = 0; idx < 128; idx++) { } ``` -:information_source: Enable the `NEO_PLUGIN_SUPPORT` compile option in CMake to include necessary bits for JUCE integration. +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) + +... +``` From 7de853ec2a89ac2c07f601210e11c9354cbbcfc3 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 15:31:32 -0600 Subject: [PATCH 24/32] Fix enums for MacOS --- src/generators/oscillator.cpp | 2 +- src/generators/oscillator.h | 4 ++-- src/modulators/adsr.cpp | 10 +++++----- src/modulators/adsr.h | 12 ++++++------ src/processors/effects/saturator.cpp | 4 ++-- src/processors/effects/saturator.h | 6 +++--- src/processors/effects/wavefolder.cpp | 6 +++--- src/processors/effects/wavefolder.h | 8 ++++---- src/processors/filters/filter.cpp | 2 +- src/processors/filters/filter.h | 4 ++-- src/utilities/arithmetic.h | 2 +- 11 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/generators/oscillator.cpp b/src/generators/oscillator.cpp index 3f5624c..d5769e2 100644 --- a/src/generators/oscillator.cpp +++ b/src/generators/oscillator.cpp @@ -31,7 +31,7 @@ Sample Oscillator::GenerateImpl() void Oscillator::AttachParameterToSourceImpl(OscillatorParameter parameter, std::atomic* source) { switch (parameter) { - case OscillatorParameter::FREQUENCY: + case OscillatorParameter::OSC_FREQUENCY: p_frequency.AttachSource(source); break; default: diff --git a/src/generators/oscillator.h b/src/generators/oscillator.h index 680f264..02d13c6 100644 --- a/src/generators/oscillator.h +++ b/src/generators/oscillator.h @@ -11,8 +11,8 @@ namespace neuron { const size_t WAVETABLE_SIZE = 256; - enum class OscillatorParameter { - FREQUENCY, + enum OscillatorParameter { + OSC_FREQUENCY, }; /** diff --git a/src/modulators/adsr.cpp b/src/modulators/adsr.cpp index 3d9a574..6adb3b5 100644 --- a/src/modulators/adsr.cpp +++ b/src/modulators/adsr.cpp @@ -13,7 +13,7 @@ AdsrEnvelopeModulator::AdsrEnvelopeModulator(Context& context, AdsrEnvelope enve float AdsrEnvelopeModulator::ModulateImpl() { - float position = (float)m_samplesSinceLastStage * (1000.0f / m_context.sampleRate); + float position = (float)m_samplesSinceLastStage * (1000.0f / (float)m_context.sampleRate); /** * NOTE: The modulation value is calculated based on the current stage of the modulator. @@ -52,16 +52,16 @@ float AdsrEnvelopeModulator::ModulateImpl() void AdsrEnvelopeModulator::AttachParameterToSourceImpl(AdsrParameter parameter, std::atomic* source) { switch (parameter) { - case AdsrParameter::ATTACK: + case AdsrParameter::ADSR_ATTACK: p_attack.AttachSource(source); break; - case AdsrParameter::DECAY: + case AdsrParameter::ADSR_DECAY: p_decay.AttachSource(source); break; - case AdsrParameter::SUSTAIN: + case AdsrParameter::ADSR_SUSTAIN: p_sustain.AttachSource(source); break; - case AdsrParameter::RELEASE: + case AdsrParameter::ADSR_RELEASE: p_release.AttachSource(source); break; default: diff --git a/src/modulators/adsr.h b/src/modulators/adsr.h index ed57523..9a52455 100644 --- a/src/modulators/adsr.h +++ b/src/modulators/adsr.h @@ -18,17 +18,17 @@ namespace neuron { float release; }; - enum class AdsrParameter { - ATTACK, - DECAY, - SUSTAIN, - 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 class AdsrStage { + enum AdsrStage { IDLE, ATTACK, DECAY, diff --git a/src/processors/effects/saturator.cpp b/src/processors/effects/saturator.cpp index 087b87e..a3b8ed3 100644 --- a/src/processors/effects/saturator.cpp +++ b/src/processors/effects/saturator.cpp @@ -23,10 +23,10 @@ Sample Saturator::ProcessImpl(Sample input) void Saturator::AttachParameterToSourceImpl(SaturatorParameter parameter, std::atomic* source) { switch (parameter) { - case SaturatorParameter::SATURATION: + case SaturatorParameter::SATURATOR_SATURATION: p_saturation.AttachSource(source); break; - case SaturatorParameter::SYMMETRY: + case SaturatorParameter::SATURATOR_SYMMETRY: p_symmetry.AttachSource(source); break; default: diff --git a/src/processors/effects/saturator.h b/src/processors/effects/saturator.h index 0e42d24..2adfb07 100644 --- a/src/processors/effects/saturator.h +++ b/src/processors/effects/saturator.h @@ -7,9 +7,9 @@ namespace neuron { - enum class SaturatorParameter { - SATURATION, - SYMMETRY, + enum SaturatorParameter { + SATURATOR_SATURATION, + SATURATOR_SYMMETRY, }; /** diff --git a/src/processors/effects/wavefolder.cpp b/src/processors/effects/wavefolder.cpp index f4201cf..1046417 100644 --- a/src/processors/effects/wavefolder.cpp +++ b/src/processors/effects/wavefolder.cpp @@ -32,13 +32,13 @@ Sample Wavefolder::ProcessImpl(Sample input) void Wavefolder::AttachParameterToSourceImpl(const WavefolderParameter parameter, std::atomic* source) { switch (parameter) { - case WavefolderParameter::INPUT_GAIN: + case WavefolderParameter::WAVEFOLDER_INPUT_GAIN: p_inputGain.AttachSource(source); break; - case WavefolderParameter::THRESHOLD: + case WavefolderParameter::WAVEFOLDER_THRESHOLD: p_threshold.AttachSource(source); break; - case WavefolderParameter::SYMMETRY: + case WavefolderParameter::WAVEFOLDER_SYMMETRY: p_symmetry.AttachSource(source); break; default: diff --git a/src/processors/effects/wavefolder.h b/src/processors/effects/wavefolder.h index 64730b0..5069728 100644 --- a/src/processors/effects/wavefolder.h +++ b/src/processors/effects/wavefolder.h @@ -6,10 +6,10 @@ namespace neuron { - enum class WavefolderParameter { - INPUT_GAIN, - THRESHOLD, - SYMMETRY, + enum WavefolderParameter { + WAVEFOLDER_INPUT_GAIN, + WAVEFOLDER_THRESHOLD, + WAVEFOLDER_SYMMETRY, }; /** diff --git a/src/processors/filters/filter.cpp b/src/processors/filters/filter.cpp index 7b946ea..4859c09 100644 --- a/src/processors/filters/filter.cpp +++ b/src/processors/filters/filter.cpp @@ -22,7 +22,7 @@ Sample Filter::ProcessImpl(Sample input) void Filter::AttachParameterToSourceImpl(FilterParameter parameter, std::atomic* source) { switch (parameter) { - case FilterParameter::CUTOFF_FREQUENCY: + case FilterParameter::FILTER_CUTOFF_FREQUENCY: p_cutoffFrequency.AttachSource(source); break; default: diff --git a/src/processors/filters/filter.h b/src/processors/filters/filter.h index ae4f1e9..6e8d00c 100644 --- a/src/processors/filters/filter.h +++ b/src/processors/filters/filter.h @@ -11,8 +11,8 @@ namespace neuron { const float FILTER_CUTOFF_FREQ_MIN = 20.0f; const float FILTER_CUTOFF_FREQ_MAX = 20000.0f; - enum class FilterParameter { - CUTOFF_FREQUENCY, + enum FilterParameter { + FILTER_CUTOFF_FREQUENCY, }; /** diff --git a/src/utilities/arithmetic.h b/src/utilities/arithmetic.h index 0dd9534..7b4da16 100644 --- a/src/utilities/arithmetic.h +++ b/src/utilities/arithmetic.h @@ -31,7 +31,7 @@ namespace neuron { * Depicts different mathematical curves, e.g. exponential, * linear, logarithmic. */ - enum class Mapping { + enum Mapping { EXP, LOG, LINEAR, From d564c0d6ef944cd8f61b0c27aca962ca24bfb5c6 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 1 Dec 2024 16:03:05 -0600 Subject: [PATCH 25/32] Remove atomic_init --- src/abstractions/parameter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abstractions/parameter.h b/src/abstractions/parameter.h index 5b6af01..2b70a6d 100644 --- a/src/abstractions/parameter.h +++ b/src/abstractions/parameter.h @@ -16,7 +16,7 @@ namespace neuron { public: explicit Parameter(T value) { - std::atomic_init(m_parameter, value); + m_parameter->store(value); } explicit Parameter(std::atomic* source) From 6d1490fd6aef65f12f40cf5aa67ba1a127818743 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Thu, 21 Aug 2025 22:54:41 -0500 Subject: [PATCH 26/32] Remove logger and timer --- src/neuron.h | 2 -- src/utilities/logger.cpp | 17 ----------------- src/utilities/logger.h | 17 ----------------- src/utilities/timer.h | 36 ------------------------------------ 4 files changed, 72 deletions(-) delete mode 100644 src/utilities/logger.cpp delete mode 100644 src/utilities/logger.h delete mode 100644 src/utilities/timer.h diff --git a/src/neuron.h b/src/neuron.h index 5c6a39f..ca5a9dd 100644 --- a/src/neuron.h +++ b/src/neuron.h @@ -35,8 +35,6 @@ // UTILITIES #include "utilities/arithmetic.h" -#include "utilities/logger.h" #include "utilities/midi.h" -#include "utilities/timer.h" #endif 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 41a6698..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/timer.h b/src/utilities/timer.h deleted file mode 100644 index 949b420..0000000 --- a/src/utilities/timer.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#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 From 1063044b88fb89e0870aa4a38aa1896a6386582a Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Thu, 18 Sep 2025 17:34:18 -0500 Subject: [PATCH 27/32] Implement with tests --- CMakeLists.txt | 1 + src/neuron.h | 1 + src/utilities/arithmetic.h | 6 + src/utilities/smoothed_value.cpp | 74 +++++++++ src/utilities/smoothed_value.h | 34 ++++ tests/generators/oscillator_test.cpp | 11 +- tests/utilities/arithmetic_test.cpp | 94 +++++++++++ tests/utilities/smoothed_value_test.cpp | 206 ++++++++++++++++++++++++ 8 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 src/utilities/smoothed_value.cpp create mode 100644 src/utilities/smoothed_value.h create mode 100644 tests/utilities/smoothed_value_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2172e24..4fb84f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ if (NEO_BUILD_TESTS) 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) + add_neuron_test(smoothed_value tests/utilities/smoothed_value_test.cpp) endif () enable_testing() diff --git a/src/neuron.h b/src/neuron.h index ca5a9dd..b53261c 100644 --- a/src/neuron.h +++ b/src/neuron.h @@ -36,5 +36,6 @@ // UTILITIES #include "utilities/arithmetic.h" #include "utilities/midi.h" +#include "utilities/smoothed_value.h" #endif diff --git a/src/utilities/arithmetic.h b/src/utilities/arithmetic.h index 7b4da16..086770d 100644 --- a/src/utilities/arithmetic.h +++ b/src/utilities/arithmetic.h @@ -111,4 +111,10 @@ namespace neuron { return numerator / denominator; } + template + inline bool isApproximatelyEqual(T a, T b) + { + return std::abs(a - b) <= std::numeric_limits::epsilon(); + } + } diff --git a/src/utilities/smoothed_value.cpp b/src/utilities/smoothed_value.cpp new file mode 100644 index 0000000..b9bd8d3 --- /dev/null +++ b/src/utilities/smoothed_value.cpp @@ -0,0 +1,74 @@ +#include "smoothed_value.h" +#include "arithmetic.h" + +using namespace neuron; + +SmoothedValue::SmoothedValue() + : m_currentValue(0.0f) + , m_targetValue(0.0f) +{ + UpdateIncrement(); +} + +SmoothedValue::SmoothedValue(float initialValue) +{ + m_currentValue = initialValue; + m_targetValue = initialValue; + UpdateIncrement(); +} + +void SmoothedValue::Reset(double sampleRate, double rampLengthMillis) +{ + if (sampleRate > 0.0 && rampLengthMillis >= 0.0) { + m_sampleRate = sampleRate; + m_rampLengthMillis = rampLengthMillis; + UpdateIncrement(); + } +} + +void SmoothedValue::SetTargetValue(float value) +{ + m_targetValue = value; + UpdateIncrement(); +} + +float SmoothedValue::GetNextValue() +{ + if (neuron::isApproximatelyEqual(m_currentValue, m_targetValue)) { + m_currentValue = m_targetValue; + return m_currentValue; + } else { + 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 SmoothedValue::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 = newValue; + } else { + m_currentValue = newValue; + } +} + +void SmoothedValue::UpdateIncrement() +{ + if (m_rampLengthMillis <= 0.0) { + m_currentValue = m_targetValue; + m_increment = 0.0f; + return; + } + + float numSamplesBetweenValues = static_cast(m_rampLengthMillis) * static_cast(m_sampleRate) / 1000.0f; + m_increment = (m_targetValue - m_currentValue) / numSamplesBetweenValues; +} diff --git a/src/utilities/smoothed_value.h b/src/utilities/smoothed_value.h new file mode 100644 index 0000000..2d29004 --- /dev/null +++ b/src/utilities/smoothed_value.h @@ -0,0 +1,34 @@ +#pragma once + +namespace neuron { + struct SmoothedValueConfig { + double sampleRate; + double rampLengthMillis; + }; + + class SmoothedValue { + public: + SmoothedValue(); + SmoothedValue(float initialValue); + ~SmoothedValue() = default; + + void Reset(double sampleRate, double rampLengthMillis); + + void SetTargetValue(float value); + float GetNextValue(); + + void Skip(int numSamples); + + private: + void UpdateIncrement(); + + double m_sampleRate = 44100.0; + double m_rampLengthMillis = 50; + + float m_currentValue; + float m_targetValue; + + float m_increment = 0.0f; + }; + +} diff --git a/tests/generators/oscillator_test.cpp b/tests/generators/oscillator_test.cpp index a6fa43f..18f4762 100644 --- a/tests/generators/oscillator_test.cpp +++ b/tests/generators/oscillator_test.cpp @@ -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/utilities/arithmetic_test.cpp b/tests/utilities/arithmetic_test.cpp index ad8aa96..8f04fb4 100644 --- a/tests/utilities/arithmetic_test.cpp +++ b/tests/utilities/arithmetic_test.cpp @@ -58,3 +58,97 @@ TEST(arithmetic_suite, exp_test) 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 + EXPECT_TRUE(isApproximatelyEqual(0, 0)); + EXPECT_TRUE(isApproximatelyEqual(5, 5)); + EXPECT_TRUE(isApproximatelyEqual(-3, -3)); + + // Test values within epsilon for float + float epsilon_f = std::numeric_limits::epsilon(); + EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f + epsilon_f)); + EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f - epsilon_f)); + EXPECT_TRUE(isApproximatelyEqual(0.0f, epsilon_f)); + EXPECT_TRUE(isApproximatelyEqual(0.0f, -epsilon_f)); + + // Test values within epsilon for double + double epsilon_d = std::numeric_limits::epsilon(); + EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0 + epsilon_d)); + EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0 - epsilon_d)); + EXPECT_TRUE(isApproximatelyEqual(0.0, epsilon_d)); + EXPECT_TRUE(isApproximatelyEqual(0.0, -epsilon_d)); + + // Test values exactly at epsilon boundary + EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f + epsilon_f)); + EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0 + epsilon_d)); + + // Test values outside epsilon for float - use larger multiples + EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f + epsilon_f * 10.0f)); + EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f - epsilon_f * 10.0f)); + EXPECT_FALSE(isApproximatelyEqual(0.0f, epsilon_f * 10.0f)); + + // Test values outside epsilon for double - use larger multiples + EXPECT_FALSE(isApproximatelyEqual(1.0, 1.0 + epsilon_d * 10.0)); + EXPECT_FALSE(isApproximatelyEqual(1.0, 1.0 - epsilon_d * 10.0)); + EXPECT_FALSE(isApproximatelyEqual(0.0, epsilon_d * 10.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 negative values + EXPECT_TRUE(isApproximatelyEqual(-1.0f, -1.0f + epsilon_f)); + EXPECT_TRUE(isApproximatelyEqual(-1.0f, -1.0f - epsilon_f)); + EXPECT_FALSE(isApproximatelyEqual(-1.0f, -1.0f + epsilon_f * 10.0f)); + + // Test with small positive values + float small_val = 1e-6f; + EXPECT_TRUE(isApproximatelyEqual(small_val, small_val)); + EXPECT_TRUE(isApproximatelyEqual(small_val, small_val + epsilon_f)); + EXPECT_FALSE(isApproximatelyEqual(small_val, small_val + epsilon_f * 100.0f)); + + // Test with large values + float large_val = 1e6f; + EXPECT_TRUE(isApproximatelyEqual(large_val, large_val)); + EXPECT_TRUE(isApproximatelyEqual(large_val, large_val + epsilon_f)); + EXPECT_FALSE(isApproximatelyEqual(large_val, large_val + 1.0f)); + + // Test special cases with zero + EXPECT_TRUE(isApproximatelyEqual(0.0f, 0.0f)); + EXPECT_TRUE(isApproximatelyEqual(-0.0f, 0.0f)); + EXPECT_TRUE(isApproximatelyEqual(0.0f, epsilon_f)); + + // Test values that should definitely be outside epsilon + EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f + 1e-6f)); // Much larger than epsilon + EXPECT_FALSE(isApproximatelyEqual(1.0, 1.0 + 1e-15)); // Much larger than double epsilon + + // Test with nextafter to get the actual next representable value + float next_float = std::nextafter(1.0f, 2.0f); + EXPECT_TRUE(isApproximatelyEqual(1.0f, next_float)); // Should be exactly epsilon apart + + float next_next_float = std::nextafter(next_float, 2.0f); + EXPECT_FALSE(isApproximatelyEqual(1.0f, next_next_float)); // Should be > epsilon apart +} diff --git a/tests/utilities/smoothed_value_test.cpp b/tests/utilities/smoothed_value_test.cpp new file mode 100644 index 0000000..e9624c8 --- /dev/null +++ b/tests/utilities/smoothed_value_test.cpp @@ -0,0 +1,206 @@ +#include +#include "neuron.h" +#include + +using namespace neuron; + +class SmoothedValueTest : 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; +}; + +// Test default constructor +TEST_F(SmoothedValueTest, DefaultConstructor) { + SmoothedValue sv; + EXPECT_FLOAT_EQ(sv.GetNextValue(), 0.0f); +} + +// Test constructor with initial value +TEST_F(SmoothedValueTest, ConstructorWithInitialValue) { + const float initialValue = 5.0f; + SmoothedValue sv(initialValue); + EXPECT_FLOAT_EQ(sv.GetNextValue(), initialValue); +} + +// Test Reset with valid parameters +TEST_F(SmoothedValueTest, ResetWithValidParameters) { + SmoothedValue 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(SmoothedValueTest, ResetWithInvalidParameters) { + SmoothedValue 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(SmoothedValueTest, SetTargetValue) { + SmoothedValue 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(SmoothedValueTest, GetNextValueReachesTarget) { + SmoothedValue 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(SmoothedValueTest, SmoothingWithNegativeValues) { + SmoothedValue 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(SmoothedValueTest, SkipSamples) { + SmoothedValue sv1(0.0f); + SmoothedValue 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(SmoothedValueTest, ZeroRampLength) { + SmoothedValue 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(SmoothedValueTest, VeryShortRampLength) { + SmoothedValue 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(SmoothedValueTest, MultipleTargetChanges) { + SmoothedValue 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(SmoothedValueTest, SameTargetValue) { + SmoothedValue 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(SmoothedValueTest, LargeSampleRate) { + SmoothedValue 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); +} \ No newline at end of file From 87ddd15fb60cd6d543d485b874f63e4b4b32a08f Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Thu, 18 Sep 2025 18:21:58 -0500 Subject: [PATCH 28/32] Fix impls and tests --- src/utilities/arithmetic.h | 31 +++++++- src/utilities/smoothed_value.cpp | 74 ----------------- src/utilities/smoothed_value.h | 93 +++++++++++++++++----- tests/utilities/arithmetic_test.cpp | 118 +++++++++++++++------------- 4 files changed, 166 insertions(+), 150 deletions(-) delete mode 100644 src/utilities/smoothed_value.cpp diff --git a/src/utilities/arithmetic.h b/src/utilities/arithmetic.h index 086770d..7415fc3 100644 --- a/src/utilities/arithmetic.h +++ b/src/utilities/arithmetic.h @@ -37,6 +37,31 @@ namespace neuron { 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. * @@ -112,9 +137,11 @@ namespace neuron { } template - inline bool isApproximatelyEqual(T a, T b) + inline bool isApproximatelyEqual(T a, T b, T relativeEpsilon = neuron::Epsilon::value) noexcept { - return std::abs(a - b) <= std::numeric_limits::epsilon(); + T diff = std::abs(a - b); + T largest = std::max(std::abs(a), std::abs(b)); + return diff <= relativeEpsilon * largest; } } diff --git a/src/utilities/smoothed_value.cpp b/src/utilities/smoothed_value.cpp deleted file mode 100644 index b9bd8d3..0000000 --- a/src/utilities/smoothed_value.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "smoothed_value.h" -#include "arithmetic.h" - -using namespace neuron; - -SmoothedValue::SmoothedValue() - : m_currentValue(0.0f) - , m_targetValue(0.0f) -{ - UpdateIncrement(); -} - -SmoothedValue::SmoothedValue(float initialValue) -{ - m_currentValue = initialValue; - m_targetValue = initialValue; - UpdateIncrement(); -} - -void SmoothedValue::Reset(double sampleRate, double rampLengthMillis) -{ - if (sampleRate > 0.0 && rampLengthMillis >= 0.0) { - m_sampleRate = sampleRate; - m_rampLengthMillis = rampLengthMillis; - UpdateIncrement(); - } -} - -void SmoothedValue::SetTargetValue(float value) -{ - m_targetValue = value; - UpdateIncrement(); -} - -float SmoothedValue::GetNextValue() -{ - if (neuron::isApproximatelyEqual(m_currentValue, m_targetValue)) { - m_currentValue = m_targetValue; - return m_currentValue; - } else { - 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 SmoothedValue::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 = newValue; - } else { - m_currentValue = newValue; - } -} - -void SmoothedValue::UpdateIncrement() -{ - if (m_rampLengthMillis <= 0.0) { - m_currentValue = m_targetValue; - m_increment = 0.0f; - return; - } - - float numSamplesBetweenValues = static_cast(m_rampLengthMillis) * static_cast(m_sampleRate) / 1000.0f; - m_increment = (m_targetValue - m_currentValue) / numSamplesBetweenValues; -} diff --git a/src/utilities/smoothed_value.h b/src/utilities/smoothed_value.h index 2d29004..bbc2b1b 100644 --- a/src/utilities/smoothed_value.h +++ b/src/utilities/smoothed_value.h @@ -1,34 +1,87 @@ #pragma once -namespace neuron { - struct SmoothedValueConfig { - double sampleRate; - double rampLengthMillis; - }; +#include "arithmetic.h" +namespace neuron { class SmoothedValue { - public: - SmoothedValue(); - SmoothedValue(float initialValue); - ~SmoothedValue() = default; + public: + SmoothedValue() : m_currentValue(0.0f), m_targetValue(0.0f) + { + UpdateIncrement(); + } + + SmoothedValue(float initialValue) : m_currentValue(initialValue), m_targetValue(initialValue) + { + UpdateIncrement(); + } + + ~SmoothedValue() = default; + + 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; + } else { + 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 Reset(double sampleRate, double rampLengthMillis); + void Skip(int numSamples) + { + if (isApproximatelyEqual(m_currentValue, m_targetValue)) { + m_currentValue = m_targetValue; + return; + } - void SetTargetValue(float value); - float GetNextValue(); + 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; + } + } - void Skip(int numSamples); + private: + static constexpr float INV_1000 = 1.0f / 1000.0f; - private: - void UpdateIncrement(); + void UpdateIncrement() noexcept + { + if (m_rampLengthMillis <= 0.0) { + m_currentValue = m_targetValue; + m_increment = 0.0f; + return; + } - double m_sampleRate = 44100.0; - double m_rampLengthMillis = 50; + float numSamplesBetweenValues = static_cast(m_rampLengthMillis * m_sampleRate) * INV_1000; + m_increment = (m_targetValue - m_currentValue) / numSamplesBetweenValues; + } - float m_currentValue; - float m_targetValue; + double m_sampleRate = 44100.0; + double m_rampLengthMillis = 50; - float m_increment = 0.0f; + float m_currentValue; + float m_targetValue; + float m_increment = 0.0f; }; } diff --git a/tests/utilities/arithmetic_test.cpp b/tests/utilities/arithmetic_test.cpp index 8f04fb4..24154db 100644 --- a/tests/utilities/arithmetic_test.cpp +++ b/tests/utilities/arithmetic_test.cpp @@ -71,38 +71,31 @@ TEST(arithmetic_suite, isApproximatelyEqual_test) EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0)); EXPECT_TRUE(isApproximatelyEqual(-1.0, -1.0)); - // Test integer types + // Test integer types (exact equality) EXPECT_TRUE(isApproximatelyEqual(0, 0)); EXPECT_TRUE(isApproximatelyEqual(5, 5)); EXPECT_TRUE(isApproximatelyEqual(-3, -3)); - // Test values within epsilon for float - float epsilon_f = std::numeric_limits::epsilon(); - EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f + epsilon_f)); - EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f - epsilon_f)); - EXPECT_TRUE(isApproximatelyEqual(0.0f, epsilon_f)); - EXPECT_TRUE(isApproximatelyEqual(0.0f, -epsilon_f)); - - // Test values within epsilon for double - double epsilon_d = std::numeric_limits::epsilon(); - EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0 + epsilon_d)); - EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0 - epsilon_d)); - EXPECT_TRUE(isApproximatelyEqual(0.0, epsilon_d)); - EXPECT_TRUE(isApproximatelyEqual(0.0, -epsilon_d)); - - // Test values exactly at epsilon boundary - EXPECT_TRUE(isApproximatelyEqual(1.0f, 1.0f + epsilon_f)); - EXPECT_TRUE(isApproximatelyEqual(1.0, 1.0 + epsilon_d)); - - // Test values outside epsilon for float - use larger multiples - EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f + epsilon_f * 10.0f)); - EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f - epsilon_f * 10.0f)); - EXPECT_FALSE(isApproximatelyEqual(0.0f, epsilon_f * 10.0f)); - - // Test values outside epsilon for double - use larger multiples - EXPECT_FALSE(isApproximatelyEqual(1.0, 1.0 + epsilon_d * 10.0)); - EXPECT_FALSE(isApproximatelyEqual(1.0, 1.0 - epsilon_d * 10.0)); - EXPECT_FALSE(isApproximatelyEqual(0.0, epsilon_d * 10.0)); + // 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)); @@ -119,36 +112,53 @@ TEST(arithmetic_suite, isApproximatelyEqual_test) EXPECT_FALSE(isApproximatelyEqual(-1, 1)); EXPECT_FALSE(isApproximatelyEqual(0, 1)); - // Test negative values - EXPECT_TRUE(isApproximatelyEqual(-1.0f, -1.0f + epsilon_f)); - EXPECT_TRUE(isApproximatelyEqual(-1.0f, -1.0f - epsilon_f)); - EXPECT_FALSE(isApproximatelyEqual(-1.0f, -1.0f + epsilon_f * 10.0f)); - - // Test with small positive values - float small_val = 1e-6f; - EXPECT_TRUE(isApproximatelyEqual(small_val, small_val)); - EXPECT_TRUE(isApproximatelyEqual(small_val, small_val + epsilon_f)); - EXPECT_FALSE(isApproximatelyEqual(small_val, small_val + epsilon_f * 100.0f)); - - // Test with large values - float large_val = 1e6f; - EXPECT_TRUE(isApproximatelyEqual(large_val, large_val)); - EXPECT_TRUE(isApproximatelyEqual(large_val, large_val + epsilon_f)); - EXPECT_FALSE(isApproximatelyEqual(large_val, large_val + 1.0f)); + // 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 special cases with zero + // Test edge cases near zero - demonstrate relative epsilon limitations EXPECT_TRUE(isApproximatelyEqual(0.0f, 0.0f)); EXPECT_TRUE(isApproximatelyEqual(-0.0f, 0.0f)); - EXPECT_TRUE(isApproximatelyEqual(0.0f, epsilon_f)); - // Test values that should definitely be outside epsilon - EXPECT_FALSE(isApproximatelyEqual(1.0f, 1.0f + 1e-6f)); // Much larger than epsilon - EXPECT_FALSE(isApproximatelyEqual(1.0, 1.0 + 1e-15)); // Much larger than double epsilon + // 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! - // Test with nextafter to get the actual next representable value - float next_float = std::nextafter(1.0f, 2.0f); - EXPECT_TRUE(isApproximatelyEqual(1.0f, next_float)); // Should be exactly epsilon apart + // For absolute comparisons near zero, use direct comparison: + EXPECT_TRUE(std::abs(0.0f - tiny_val) <= 1e-8f); // Direct absolute comparison works - float next_next_float = std::nextafter(next_float, 2.0f); - EXPECT_FALSE(isApproximatelyEqual(1.0f, next_next_float)); // Should be > epsilon apart + // 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 } From f44612d2de9fb7317273d095408e0d522054b957 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell <44885822+maxwellmattryan@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:09:04 -0500 Subject: [PATCH 29/32] feat: implement smoothed value class (#33) ## Summary Closes #32 --- CMakeLists.txt | 36 +- Makefile | 44 +- .../neuron.h => include/neuron/core/base.h | 0 {src/audio => include/neuron/core}/context.h | 0 .../neuron/core}/parameter.h | 0 {src/audio => include/neuron/core}/sample.h | 0 .../neuron/dsp/generators}/generator.h | 2 +- .../neuron/dsp}/generators/oscillator.h | 12 +- {src => include/neuron/dsp}/modulators/adsr.h | 8 +- .../neuron/dsp/modulators}/modulator.h | 0 .../neuron/dsp/processors}/filter.h | 10 +- .../neuron/dsp/processors}/processor.h | 2 +- .../neuron/dsp/processors}/saturator.h | 8 +- .../neuron/dsp/processors}/wavefolder.h | 6 +- include/neuron/neuron.h | 34 ++ .../neuron/utils}/arithmetic.h | 2 +- .../utilities => include/neuron/utils}/midi.h | 2 +- include/neuron/utils/smoothed_value.h | 189 +++++++ .../audio => include/neuron/utils}/waveform.h | 4 +- scripts/format.sh | 7 + src/{ => dsp}/generators/oscillator.cpp | 4 +- src/{ => dsp}/modulators/adsr.cpp | 2 +- .../filters => dsp/processors}/filter.cpp | 4 +- .../effects => dsp/processors}/saturator.cpp | 4 +- .../effects => dsp/processors}/wavefolder.cpp | 4 +- src/neuron.h | 41 -- src/utilities/smoothed_value.h | 87 --- .../{abstractions => core}/parameter_test.cpp | 4 +- .../parameter_test_atomic.cpp | 4 +- .../{ => dsp}/generators/oscillator_test.cpp | 4 +- tests/{ => dsp}/modulators/adsr_test.cpp | 4 +- .../processors}/filter_test.cpp | 5 +- .../processors}/saturator_test.cpp | 4 +- .../processors}/wavefolder_test.cpp | 4 +- tests/utilities/smoothed_value_test.cpp | 206 ------- .../{utilities => utils}/arithmetic_test.cpp | 44 +- tests/{utilities => utils}/midi_test.cpp | 4 +- tests/utils/smoothed_value_test.cpp | 508 ++++++++++++++++++ tests/{audio => utils}/waveform_test.cpp | 4 +- 39 files changed, 847 insertions(+), 460 deletions(-) rename src/abstractions/neuron.h => include/neuron/core/base.h (100%) rename {src/audio => include/neuron/core}/context.h (100%) rename {src/abstractions => include/neuron/core}/parameter.h (100%) rename {src/audio => include/neuron/core}/sample.h (100%) rename {src/abstractions => include/neuron/dsp/generators}/generator.h (94%) rename {src => include/neuron/dsp}/generators/oscillator.h (91%) rename {src => include/neuron/dsp}/modulators/adsr.h (95%) rename {src/abstractions => include/neuron/dsp/modulators}/modulator.h (100%) rename {src/processors/filters => include/neuron/dsp/processors}/filter.h (89%) rename {src/abstractions => include/neuron/dsp/processors}/processor.h (94%) rename {src/processors/effects => include/neuron/dsp/processors}/saturator.h (92%) rename {src/processors/effects => include/neuron/dsp/processors}/wavefolder.h (94%) create mode 100644 include/neuron/neuron.h rename {src/utilities => include/neuron/utils}/arithmetic.h (98%) rename {src/utilities => include/neuron/utils}/midi.h (91%) create mode 100644 include/neuron/utils/smoothed_value.h rename {src/audio => include/neuron/utils}/waveform.h (93%) rename src/{ => dsp}/generators/oscillator.cpp (98%) rename src/{ => dsp}/modulators/adsr.cpp (98%) rename src/{processors/filters => dsp/processors}/filter.cpp (93%) rename src/{processors/effects => dsp/processors}/saturator.cpp (92%) rename src/{processors/effects => dsp/processors}/wavefolder.cpp (94%) delete mode 100644 src/neuron.h delete mode 100644 src/utilities/smoothed_value.h rename tests/{abstractions => core}/parameter_test.cpp (95%) rename tests/{abstractions => core}/parameter_test_atomic.cpp (97%) rename tests/{ => dsp}/generators/oscillator_test.cpp (98%) rename tests/{ => dsp}/modulators/adsr_test.cpp (98%) rename tests/{processors/filters => dsp/processors}/filter_test.cpp (80%) rename tests/{processors/effects => dsp/processors}/saturator_test.cpp (96%) rename tests/{processors/effects => dsp/processors}/wavefolder_test.cpp (96%) delete mode 100644 tests/utilities/smoothed_value_test.cpp rename tests/{utilities => utils}/arithmetic_test.cpp (79%) rename tests/{utilities => utils}/midi_test.cpp (93%) create mode 100644 tests/utils/smoothed_value_test.cpp rename tests/{audio => utils}/waveform_test.cpp (97%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4fb84f5..ea11eeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,6 @@ option(NEO_BUILD_TESTS "Build unit tests" OFF) project(neuron VERSION 0.1.0 LANGUAGES CXX) -file(GLOB_RECURSE LIST_DIRECTORIES INCLUDE_DIRS "src/*") file(GLOB_RECURSE SRC_FILES "src/*.cpp") add_library(neuron STATIC ${SRC_FILES}) @@ -25,33 +24,36 @@ else() 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_DIRS}) - 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) add_test(NAME "${name}_test" COMMAND "${name}_test_exe") endfunction() add_subdirectory(vendor/googletest) if (NEO_PLUGIN_SUPPORT) - add_neuron_test(parameter_atomic tests/abstractions/parameter_test_atomic.cpp) + add_neuron_test(parameter_atomic tests/core/parameter_test_atomic.cpp) else () - add_neuron_test(parameter tests/abstractions/parameter_test.cpp) - 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) - add_neuron_test(smoothed_value tests/utilities/smoothed_value_test.cpp) + add_neuron_test(parameter tests/core/parameter_test.cpp) + add_neuron_test(waveform tests/utils/waveform_test.cpp) + 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) + 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) endif () enable_testing() endif () - -target_include_directories(neuron PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src PRIVATE ${INCLUDE_DIRS}) diff --git a/Makefile b/Makefile index 19613f5..94630dd 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +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. -ABSTRACTIONS_MOD_DIR = abstractions -ABSTRACTIONS_MODULES = \ - -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 @@ -114,14 +100,8 @@ C_DEFS = \ -DSTM32H750xx C_INCLUDES = \ --I$(MODULE_DIR) \ --I$(MODULE_DIR)/$(ABSTRACTIONS_MOD_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/src/abstractions/neuron.h b/include/neuron/core/base.h similarity index 100% rename from src/abstractions/neuron.h rename to include/neuron/core/base.h diff --git a/src/audio/context.h b/include/neuron/core/context.h similarity index 100% rename from src/audio/context.h rename to include/neuron/core/context.h diff --git a/src/abstractions/parameter.h b/include/neuron/core/parameter.h similarity index 100% rename from src/abstractions/parameter.h rename to include/neuron/core/parameter.h diff --git a/src/audio/sample.h b/include/neuron/core/sample.h similarity index 100% rename from src/audio/sample.h rename to include/neuron/core/sample.h diff --git a/src/abstractions/generator.h b/include/neuron/dsp/generators/generator.h similarity index 94% rename from src/abstractions/generator.h rename to include/neuron/dsp/generators/generator.h index e0629a3..aa6b9c7 100644 --- a/src/abstractions/generator.h +++ b/include/neuron/dsp/generators/generator.h @@ -1,6 +1,6 @@ #pragma once -#include "audio/sample.h" +#include "neuron/core/sample.h" namespace neuron { diff --git a/src/generators/oscillator.h b/include/neuron/dsp/generators/oscillator.h similarity index 91% rename from src/generators/oscillator.h rename to include/neuron/dsp/generators/oscillator.h index 02d13c6..0307e26 100644 --- a/src/generators/oscillator.h +++ b/include/neuron/dsp/generators/oscillator.h @@ -1,11 +1,11 @@ #pragma once -#include "abstractions/generator.h" -#include "abstractions/neuron.h" -#include "abstractions/parameter.h" -#include "audio/context.h" -#include "audio/waveform.h" -#include "utilities/arithmetic.h" +#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 { diff --git a/src/modulators/adsr.h b/include/neuron/dsp/modulators/adsr.h similarity index 95% rename from src/modulators/adsr.h rename to include/neuron/dsp/modulators/adsr.h index 9a52455..1cc7493 100644 --- a/src/modulators/adsr.h +++ b/include/neuron/dsp/modulators/adsr.h @@ -1,9 +1,9 @@ #pragma once -#include "abstractions/modulator.h" -#include "abstractions/neuron.h" -#include "abstractions/parameter.h" -#include "audio/context.h" +#include "neuron/core/base.h" +#include "neuron/core/context.h" +#include "neuron/core/parameter.h" +#include "neuron/dsp/modulators/modulator.h" namespace neuron { diff --git a/src/abstractions/modulator.h b/include/neuron/dsp/modulators/modulator.h similarity index 100% rename from src/abstractions/modulator.h rename to include/neuron/dsp/modulators/modulator.h diff --git a/src/processors/filters/filter.h b/include/neuron/dsp/processors/filter.h similarity index 89% rename from src/processors/filters/filter.h rename to include/neuron/dsp/processors/filter.h index 6e8d00c..ac61a77 100644 --- a/src/processors/filters/filter.h +++ b/include/neuron/dsp/processors/filter.h @@ -1,10 +1,10 @@ #pragma once -#include "abstractions/neuron.h" -#include "abstractions/parameter.h" -#include "abstractions/processor.h" -#include "audio/context.h" -#include "audio/sample.h" +#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 { diff --git a/src/abstractions/processor.h b/include/neuron/dsp/processors/processor.h similarity index 94% rename from src/abstractions/processor.h rename to include/neuron/dsp/processors/processor.h index 45dab46..6596c24 100644 --- a/src/abstractions/processor.h +++ b/include/neuron/dsp/processors/processor.h @@ -1,6 +1,6 @@ #pragma once -#include "audio/sample.h" +#include "neuron/core/sample.h" namespace neuron { diff --git a/src/processors/effects/saturator.h b/include/neuron/dsp/processors/saturator.h similarity index 92% rename from src/processors/effects/saturator.h rename to include/neuron/dsp/processors/saturator.h index 2adfb07..cd5f5fc 100644 --- a/src/processors/effects/saturator.h +++ b/include/neuron/dsp/processors/saturator.h @@ -1,9 +1,9 @@ #pragma once -#include "abstractions/neuron.h" -#include "abstractions/parameter.h" -#include "abstractions/processor.h" -#include "audio/sample.h" +#include "neuron/core/base.h" +#include "neuron/core/parameter.h" +#include "neuron/core/sample.h" +#include "neuron/dsp/processors/processor.h" namespace neuron { diff --git a/src/processors/effects/wavefolder.h b/include/neuron/dsp/processors/wavefolder.h similarity index 94% rename from src/processors/effects/wavefolder.h rename to include/neuron/dsp/processors/wavefolder.h index 5069728..d9cb0bd 100644 --- a/src/processors/effects/wavefolder.h +++ b/include/neuron/dsp/processors/wavefolder.h @@ -1,8 +1,8 @@ #pragma once -#include "abstractions/neuron.h" -#include "abstractions/parameter.h" -#include "abstractions/processor.h" +#include "neuron/core/base.h" +#include "neuron/core/parameter.h" +#include "neuron/dsp/processors/processor.h" namespace neuron { 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/src/utilities/arithmetic.h b/include/neuron/utils/arithmetic.h similarity index 98% rename from src/utilities/arithmetic.h rename to include/neuron/utils/arithmetic.h index 7415fc3..bb0cbb9 100644 --- a/src/utilities/arithmetic.h +++ b/include/neuron/utils/arithmetic.h @@ -39,7 +39,7 @@ namespace neuron { template struct Epsilon { - static constexpr T value = T{1e-5}; + static constexpr T value = T { 1e-5 }; }; template<> diff --git a/src/utilities/midi.h b/include/neuron/utils/midi.h similarity index 91% rename from src/utilities/midi.h rename to include/neuron/utils/midi.h index c798590..82004c1 100644 --- a/src/utilities/midi.h +++ b/include/neuron/utils/midi.h @@ -1,6 +1,6 @@ #pragma once -#include "utilities/arithmetic.h" +#include "neuron/utils/arithmetic.h" namespace neuron { 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/src/audio/waveform.h b/include/neuron/utils/waveform.h similarity index 93% rename from src/audio/waveform.h rename to include/neuron/utils/waveform.h index f0cf428..5d76e3d 100644 --- a/src/audio/waveform.h +++ b/include/neuron/utils/waveform.h @@ -1,7 +1,7 @@ #pragma once -#include "audio/sample.h" -#include "utilities/arithmetic.h" +#include "neuron/core/sample.h" +#include "neuron/utils/arithmetic.h" namespace neuron { diff --git a/scripts/format.sh b/scripts/format.sh index d5b4338..244c151 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,6 +1,13 @@ #!/bin/bash printf "Formatting code...\n" + +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" diff --git a/src/generators/oscillator.cpp b/src/dsp/generators/oscillator.cpp similarity index 98% rename from src/generators/oscillator.cpp rename to src/dsp/generators/oscillator.cpp index d5769e2..25f1d04 100644 --- a/src/generators/oscillator.cpp +++ b/src/dsp/generators/oscillator.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/dsp/generators/oscillator.h" -#include "generators/oscillator.h" +#include using namespace neuron; diff --git a/src/modulators/adsr.cpp b/src/dsp/modulators/adsr.cpp similarity index 98% rename from src/modulators/adsr.cpp rename to src/dsp/modulators/adsr.cpp index 6adb3b5..9921aab 100644 --- a/src/modulators/adsr.cpp +++ b/src/dsp/modulators/adsr.cpp @@ -1,4 +1,4 @@ -#include "modulators/adsr.h" +#include "neuron/dsp/modulators/adsr.h" using namespace neuron; diff --git a/src/processors/filters/filter.cpp b/src/dsp/processors/filter.cpp similarity index 93% rename from src/processors/filters/filter.cpp rename to src/dsp/processors/filter.cpp index 4859c09..5d7a3db 100644 --- a/src/processors/filters/filter.cpp +++ b/src/dsp/processors/filter.cpp @@ -1,5 +1,5 @@ -#include "processors/filters/filter.h" -#include "utilities/arithmetic.h" +#include "neuron/dsp/processors/filter.h" +#include "neuron/utils/arithmetic.h" using namespace neuron; diff --git a/src/processors/effects/saturator.cpp b/src/dsp/processors/saturator.cpp similarity index 92% rename from src/processors/effects/saturator.cpp rename to src/dsp/processors/saturator.cpp index a3b8ed3..57ee3c1 100644 --- a/src/processors/effects/saturator.cpp +++ b/src/dsp/processors/saturator.cpp @@ -1,5 +1,5 @@ -#include "processors/effects/saturator.h" -#include "utilities/arithmetic.h" +#include "neuron/dsp/processors/saturator.h" +#include "neuron/utils/arithmetic.h" using namespace neuron; diff --git a/src/processors/effects/wavefolder.cpp b/src/dsp/processors/wavefolder.cpp similarity index 94% rename from src/processors/effects/wavefolder.cpp rename to src/dsp/processors/wavefolder.cpp index 1046417..88ae1d7 100644 --- a/src/processors/effects/wavefolder.cpp +++ b/src/dsp/processors/wavefolder.cpp @@ -1,5 +1,5 @@ -#include "processors/effects/wavefolder.h" -#include "utilities/arithmetic.h" +#include "neuron/dsp/processors/wavefolder.h" +#include "neuron/utils/arithmetic.h" using namespace neuron; diff --git a/src/neuron.h b/src/neuron.h deleted file mode 100644 index b53261c..0000000 --- a/src/neuron.h +++ /dev/null @@ -1,41 +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 - */ -#pragma once - -#ifndef NEURON_LIB_H -#define NEURON_LIB_H - -// ABSTRACTIONS -#include "abstractions/generator.h" -#include "abstractions/modulator.h" -#include "abstractions/neuron.h" -#include "abstractions/parameter.h" -#include "abstractions/processor.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/midi.h" -#include "utilities/smoothed_value.h" - -#endif diff --git a/src/utilities/smoothed_value.h b/src/utilities/smoothed_value.h deleted file mode 100644 index bbc2b1b..0000000 --- a/src/utilities/smoothed_value.h +++ /dev/null @@ -1,87 +0,0 @@ -#pragma once - -#include "arithmetic.h" - -namespace neuron { - class SmoothedValue { - public: - SmoothedValue() : m_currentValue(0.0f), m_targetValue(0.0f) - { - UpdateIncrement(); - } - - SmoothedValue(float initialValue) : m_currentValue(initialValue), m_targetValue(initialValue) - { - UpdateIncrement(); - } - - ~SmoothedValue() = default; - - 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; - } else { - 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; - }; - -} diff --git a/tests/abstractions/parameter_test.cpp b/tests/core/parameter_test.cpp similarity index 95% rename from tests/abstractions/parameter_test.cpp rename to tests/core/parameter_test.cpp index 74084ee..834f3c9 100644 --- a/tests/abstractions/parameter_test.cpp +++ b/tests/core/parameter_test.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/core/parameter.h" -#include "neuron.h" +#include using namespace neuron; diff --git a/tests/abstractions/parameter_test_atomic.cpp b/tests/core/parameter_test_atomic.cpp similarity index 97% rename from tests/abstractions/parameter_test_atomic.cpp rename to tests/core/parameter_test_atomic.cpp index b42bb97..4efe3c6 100644 --- a/tests/abstractions/parameter_test_atomic.cpp +++ b/tests/core/parameter_test_atomic.cpp @@ -1,8 +1,8 @@ +#include "neuron/core/parameter.h" + #include #include -#include "neuron.h" - using namespace neuron; TEST(parameter_suite, attach_source_test) diff --git a/tests/generators/oscillator_test.cpp b/tests/dsp/generators/oscillator_test.cpp similarity index 98% rename from tests/generators/oscillator_test.cpp rename to tests/dsp/generators/oscillator_test.cpp index 18f4762..56abfcc 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; 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/smoothed_value_test.cpp b/tests/utilities/smoothed_value_test.cpp deleted file mode 100644 index e9624c8..0000000 --- a/tests/utilities/smoothed_value_test.cpp +++ /dev/null @@ -1,206 +0,0 @@ -#include -#include "neuron.h" -#include - -using namespace neuron; - -class SmoothedValueTest : 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; -}; - -// Test default constructor -TEST_F(SmoothedValueTest, DefaultConstructor) { - SmoothedValue sv; - EXPECT_FLOAT_EQ(sv.GetNextValue(), 0.0f); -} - -// Test constructor with initial value -TEST_F(SmoothedValueTest, ConstructorWithInitialValue) { - const float initialValue = 5.0f; - SmoothedValue sv(initialValue); - EXPECT_FLOAT_EQ(sv.GetNextValue(), initialValue); -} - -// Test Reset with valid parameters -TEST_F(SmoothedValueTest, ResetWithValidParameters) { - SmoothedValue 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(SmoothedValueTest, ResetWithInvalidParameters) { - SmoothedValue 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(SmoothedValueTest, SetTargetValue) { - SmoothedValue 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(SmoothedValueTest, GetNextValueReachesTarget) { - SmoothedValue 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(SmoothedValueTest, SmoothingWithNegativeValues) { - SmoothedValue 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(SmoothedValueTest, SkipSamples) { - SmoothedValue sv1(0.0f); - SmoothedValue 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(SmoothedValueTest, ZeroRampLength) { - SmoothedValue 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(SmoothedValueTest, VeryShortRampLength) { - SmoothedValue 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(SmoothedValueTest, MultipleTargetChanges) { - SmoothedValue 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(SmoothedValueTest, SameTargetValue) { - SmoothedValue 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(SmoothedValueTest, LargeSampleRate) { - SmoothedValue 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); -} \ No newline at end of file diff --git a/tests/utilities/arithmetic_test.cpp b/tests/utils/arithmetic_test.cpp similarity index 79% rename from tests/utilities/arithmetic_test.cpp rename to tests/utils/arithmetic_test.cpp index 24154db..0b2f887 100644 --- a/tests/utilities/arithmetic_test.cpp +++ b/tests/utils/arithmetic_test.cpp @@ -1,6 +1,6 @@ -#include +#include "neuron/utils/arithmetic.h" -#include "neuron.h" +#include using namespace neuron; @@ -77,23 +77,23 @@ TEST(arithmetic_suite, isApproximatelyEqual_test) 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)); // 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(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)); // 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 + 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)); // 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)); // 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 @@ -113,8 +113,8 @@ TEST(arithmetic_suite, isApproximatelyEqual_test) 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 + 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)); @@ -129,25 +129,25 @@ TEST(arithmetic_suite, isApproximatelyEqual_test) 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 + 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_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 + 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_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 + 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 + 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)); @@ -155,10 +155,10 @@ TEST(arithmetic_suite, isApproximatelyEqual_test) // 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 + 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 + 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; From 7609c1dce1373d7ed7d07bb2f19a04604f034c80 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 21 Sep 2025 17:44:13 -0500 Subject: [PATCH 30/32] Fix tests for both configs --- .github/workflows/ci.test.yml | 32 +++++++++++++++++++++++++++- CMakeLists.txt | 27 ++++++++++++----------- include/neuron/core/parameter.h | 3 ++- tests/core/parameter_test_atomic.cpp | 3 ++- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.test.yml b/.github/workflows/ci.test.yml index 06932c7..b50efbd 100644 --- a/.github/workflows/ci.test.yml +++ b/.github/workflows/ci.test.yml @@ -10,7 +10,37 @@ defaults: shell: bash jobs: - GoogleTest: + 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index ea11eeb..cf71623 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,26 +34,27 @@ target_include_directories(neuron if (NEO_BUILD_TESTS) function(add_neuron_test name src) add_executable("${name}_test_exe" ${src}) - target_link_libraries("${name}_test_exe" PRIVATE neuron 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() if (NEO_PLUGIN_SUPPORT) add_neuron_test(parameter_atomic tests/core/parameter_test_atomic.cpp) - else () - add_neuron_test(parameter tests/core/parameter_test.cpp) - add_neuron_test(waveform tests/utils/waveform_test.cpp) - 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) - 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) endif () - enable_testing() + add_neuron_test(parameter tests/core/parameter_test.cpp) + + 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) + + 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/include/neuron/core/parameter.h b/include/neuron/core/parameter.h index 2b70a6d..7a4a6b7 100644 --- a/include/neuron/core/parameter.h +++ b/include/neuron/core/parameter.h @@ -47,8 +47,9 @@ namespace neuron { return m_parameter->load(); } - Parameter& operator=(T /* value */) + Parameter& operator=(T value) { + m_parameter->store(value); return *this; } diff --git a/tests/core/parameter_test_atomic.cpp b/tests/core/parameter_test_atomic.cpp index 4efe3c6..2836867 100644 --- a/tests/core/parameter_test_atomic.cpp +++ b/tests/core/parameter_test_atomic.cpp @@ -1,6 +1,7 @@ #include "neuron/core/parameter.h" #include +#include #include using namespace neuron; @@ -24,7 +25,7 @@ TEST(parameter_suite, attach_and_assignment_test) param = 3.0f; - EXPECT_FLOAT_EQ(static_cast(param), 5.0f); + EXPECT_THAT(static_cast(param), testing::Not(testing::FloatEq(5.0f))); } TEST(parameter_suite, attach_source_and_arithmetic_test) From acc4035b3fe3527a0b7bf1213721c9042fb60963 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 21 Sep 2025 18:05:33 -0500 Subject: [PATCH 31/32] Remove C-style casts --- src/dsp/generators/oscillator.cpp | 12 ++++++------ src/dsp/modulators/adsr.cpp | 6 +++--- src/dsp/processors/filter.cpp | 2 +- tests/dsp/generators/oscillator_test.cpp | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/dsp/generators/oscillator.cpp b/src/dsp/generators/oscillator.cpp index 25f1d04..882b001 100644 --- a/src/dsp/generators/oscillator.cpp +++ b/src/dsp/generators/oscillator.cpp @@ -42,7 +42,7 @@ void Oscillator::AttachParameterToSourceImpl(OscillatorParameter parameter, std: 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); @@ -52,7 +52,7 @@ void Oscillator::Reset(float phase) void Oscillator::SetFrequency(float frequency) { p_frequency = frequency; - m_phaseIncrement = p_frequency * (float)WAVETABLE_SIZE / (float)m_context.sampleRate; + m_phaseIncrement = p_frequency * static_cast(WAVETABLE_SIZE) / static_cast(m_context.sampleRate); } void Oscillator::SetWaveform(Waveform waveform) @@ -75,7 +75,7 @@ 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; + float phase = static_cast(idx) * PI * 2.0f / static_cast(WAVETABLE_SIZE); m_wavetable[idx] = sin(phase); } } @@ -83,8 +83,8 @@ void Oscillator::PopulateWavetable() 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); } @@ -95,7 +95,7 @@ Sample Oscillator::Lerp() { 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 index 9921aab..390d84c 100644 --- a/src/dsp/modulators/adsr.cpp +++ b/src/dsp/modulators/adsr.cpp @@ -13,7 +13,7 @@ AdsrEnvelopeModulator::AdsrEnvelopeModulator(Context& context, AdsrEnvelope enve float AdsrEnvelopeModulator::ModulateImpl() { - float position = (float)m_samplesSinceLastStage * (1000.0f / (float)m_context.sampleRate); + 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. @@ -117,8 +117,8 @@ void AdsrEnvelopeModulator::Update(float stageDuration, AdsrStage nextStage, boo m_samplesSinceLastStage++; } - float msPerSample = 1000.0f / (float)m_context.sampleRate; - if ((float)m_samplesSinceLastStage * msPerSample >= stageDuration) { + 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 index 5d7a3db..ee37c99 100644 --- a/src/dsp/processors/filter.cpp +++ b/src/dsp/processors/filter.cpp @@ -40,6 +40,6 @@ void Filter::SetCutoffFrequency(float frequency) void Filter::CalculateAlpha() { float cutoffResponse = 1.0f / (2.0f * PI * p_cutoffFrequency); - float deltaTime = 1.0f / (float)m_context.sampleRate; + float deltaTime = 1.0f / static_cast(m_context.sampleRate); m_alpha = deltaTime / (cutoffResponse + deltaTime); } diff --git a/tests/dsp/generators/oscillator_test.cpp b/tests/dsp/generators/oscillator_test.cpp index 56abfcc..521a381 100644 --- a/tests/dsp/generators/oscillator_test.cpp +++ b/tests/dsp/generators/oscillator_test.cpp @@ -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); } From bffb3ebdeb188deeb7d7b82f8d46796da7e3f735 Mon Sep 17 00:00:00 2001 From: Matthew Maxwell Date: Sun, 21 Sep 2025 18:09:22 -0500 Subject: [PATCH 32/32] Fix REAMDE --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7a45ea2..ea33563 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # `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) @@ -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,12 +81,12 @@ 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(); } ```