├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── melatonin ├── AudioBlockFFT.h ├── block_and_buffer_matchers.h ├── block_and_buffer_test_helpers.h ├── mock_playheads.h ├── parameter_test_helpers.h └── vector_matchers.h ├── melatonin_test_helpers.h └── tests └── block_and_buffer_test_helpers.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # specific files 2 | .DS_Store 3 | CMakeUserPresets.json 4 | 5 | # build directories 6 | [Bb]uilds/ 7 | [Bb]uild/ 8 | 9 | # other directories generated by various tools 10 | Testing/ 11 | [Cc]ache/ 12 | .cache/ 13 | [Dd]eploy/ 14 | 15 | # IDE-specific files and directories 16 | xcuserdata/ 17 | .idea/ 18 | cmake-build-*/ 19 | [Oo]ut/ 20 | .vs/ 21 | .vscode/ 22 | .history/ 23 | *.vim 24 | [._]*.un~ 25 | *.sublime-workspace 26 | 27 | # other files 28 | .[Tt]rash* 29 | .nfs* 30 | *.bak 31 | *.tmp 32 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | project(MelatoninTestHelpers VERSION 1.0.0 LANGUAGES CXX 4 | DESCRIPTION "JUCE module for inspecting Components" 5 | HOMEPAGE_URL "https://github.com/sudara/melatonin_inspector") 6 | 7 | if (MelatoninTestHelpers_IS_TOP_LEVEL) 8 | include(FetchContent) 9 | message(STATUS "Cloning JUCE...") 10 | 11 | FetchContent_Declare(JUCE 12 | GIT_REPOSITORY https://github.com/juce-framework/JUCE.git 13 | GIT_TAG origin/master 14 | GIT_SHALLOW TRUE 15 | GIT_PROGRESS TRUE 16 | FIND_PACKAGE_ARGS 7.0.5) 17 | 18 | FetchContent_MakeAvailable(JUCE) 19 | 20 | FetchContent_Declare( 21 | Catch2 22 | GIT_REPOSITORY https://github.com/catchorg/Catch2.git 23 | GIT_TAG v3.3.2) 24 | FetchContent_MakeAvailable(Catch2) # find_package equivalent 25 | 26 | enable_testing() 27 | 28 | file(GLOB_RECURSE TestFiles CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.h") 29 | 30 | add_executable(Tests ${TestFiles}) 31 | 32 | include(${Catch2_SOURCE_DIR}/extras/Catch.cmake) 33 | catch_discover_tests(Tests) 34 | 35 | # this flag allows parent projects to run tests as well 36 | target_compile_definitions(Tests PRIVATE RUN_MELATONIN_TESTS=1) 37 | 38 | endif () 39 | 40 | if (NOT COMMAND juce_add_module) 41 | message(FATAL_ERROR "JUCE must be added to your project before melatonin_test_helpers!") 42 | endif () 43 | 44 | set(CMAKE_CXX_STANDARD 17) 45 | set(CMAKE_CXX_STANDARD_REQUIRED YES) 46 | 47 | juce_add_module("${CMAKE_CURRENT_LIST_DIR}") 48 | 49 | add_library(Melatonin::TestHelpers ALIAS melatonin_test_helpers) 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sudara Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Melatonin Test Helpers 2 | 3 | This module contains two things 4 | 5 | 1. A collection of C++ free functions for tests that take an `dsp::dsp::AudioBlock` or a `juce::AudioBuffer`. 6 | 2. [Catch2](https://github.com/catchorg/Catch2) matchers using the free functions that make it convenient to do things 7 | like `REQUIRE_THAT (result, isValidAudio())` 8 | 9 | ## Catch2 Matchers 10 | 11 | The Catch2 matchers give detail on AudioBlocks when a test fails. This includes displaying summary stats 12 | and [sparkline](https://github.com/sudara/melatonin_audio_sparklines) for any AudioBlock: 13 | 14 | ``` 15 | REQUIRE_THAT(myAudioBlock, isEqualTo (someOtherBlock)) 16 | with expansion: 17 | 18 | Block is 1 channel, 480 samples, min -0.766242, max 0.289615, 100% filled 19 | [0—⎻—x—⎼⎽_⎽⎼—] 20 | is equal to 21 | Block is 1 channel, 480 samples, min -1, max 1, 100% filled 22 | [0—⎻⎺‾⎺⎻—x—⎼⎽_⎽⎼—] 23 | ``` 24 | 25 | ### isEqualTo 26 | 27 | ```cpp 28 | REQUIRE_THAT (myAudioBlock, isEqualTo (someOtherBlock)); 29 | REQUIRE_THAT (myStdVector, isEqualTo (myStdVector)); 30 | ``` 31 | 32 | This compares each channel of the two blocks and confirms each sample is within a tolerance 33 | of `std::numeric_limits::epsilon() * 100`. Don't @ me, but assuming you are doing things like multiplying a 34 | number of floats together to arrive at the end result, you probably don't want this tolerance any tighter. 35 | 36 | However, you can loosen or tighten to your desire by passing an absolute tolerance: 37 | 38 | ```cpp 39 | REQUIRE_THAT (myAudioBlock, isEqualTo (someOtherBlock), 0.0005f)); 40 | ``` 41 | 42 | Note that you can also use this vector matcher with `std::vector`. I needed this because Catch's built in 43 | vector matcher `Catch::Matchers::Approx` doesn't handle small numbers around 0 very 44 | well: https://github.com/catchorg/Catch2/issues/2659 45 | 46 | ### isFilled 47 | 48 | ```cpp 49 | REQUIRE_THAT (myAudioBlock, isFilled); 50 | REQUIRE_THAT (myAudioBuffer, isFilled); 51 | ``` 52 | 53 | Passes when the block is completely filled. No more than 1 consecutive zero is allowed. 54 | 55 | ### isFilledUntil 56 | 57 | ```cpp 58 | REQUIRE_THAT (myAudioBlock, isFilledUntil(256)); 59 | REQUIRE_THAT (myAudioBuffer, isFilledUntil(256)); 60 | ``` 61 | 62 | Passes when the block is filled (doesn't contain consecutive zeros) up to to and including sample 256. 63 | 64 | ### isFilledAfter 65 | 66 | ```cpp 67 | REQUIRE_THAT (myAudioBlock, isFilledAfter(256)); 68 | REQUIRE_THAT (myAudioBuffer, isFilledAfter(256)); 69 | ``` 70 | 71 | Passes if, starting with this sample, the block is filled and doesn't contain consecutive zeros. 72 | 73 | If the sample specified is zero (eg, start of a sine wave) but there are no consecutive zeros, it'll pass. 74 | 75 | Fails if the block ends at the sample specified. 76 | 77 | ### isFilledBetween 78 | 79 | ```cpp 80 | REQUIRE_THAT (myAudioBlock, isFilledBetween(64, 128)); 81 | REQUIRE_THAT (myAudioBuffer, isFilledBetween(64, 128)); 82 | ``` 83 | 84 | Passes when sample values 64 and 128 have a non zero value for all channels. 85 | 86 | ### isEmpty 87 | 88 | ```cpp 89 | REQUIRE_THAT (myAudioBlock, isEmpty); 90 | REQUIRE_THAT (myAudioBuffer, isEmpty); 91 | ``` 92 | 93 | The block only has 0s. 94 | 95 | ### isEmptyUntil 96 | 97 | ```cpp 98 | REQUIRE_THAT (myAudioBlock, isEmptyUntil(256)); 99 | REQUIRE_THAT (myAudioBuffer, isEmptyUntil(256)); 100 | ``` 101 | 102 | Passes when the block has only zeros up to and including this sample number. 103 | 104 | ### isEmptyAfter 105 | 106 | ```cpp 107 | REQUIRE_THAT (myAudioBlock, isEmptyAfter(256)); 108 | REQUIRE_THAT (myAudioBuffer, isEmptyAfter(256)); 109 | ``` 110 | 111 | Passes when the block only contains zeros after this sample number, or when the block ends at this point. 112 | 113 | ## Other helpers 114 | 115 | The matchers above call out to free functions test helpers (prepended with `block`) which can be used seperately. 116 | 117 | So for example you can do use the Catch2 matcher style: 118 | 119 | ```cpp 120 | REQUIRE_THAT (myAudioBlock, isFilledUntil(256)); 121 | ``` 122 | 123 | Or the free function style: 124 | 125 | ```cpp 126 | REQUIRE (blockIsFilledUntil(myAudioBlock, 256); 127 | REQUIRE (bufferIsFilledUntil(myAudioBuffer, 256); 128 | ``` 129 | 130 | Additionally, there are some other helpers: 131 | 132 | ### maxMagnitude 133 | 134 | Returns the peak absolute value (magnitude) from any channel in the block. 135 | 136 | ```cpp 137 | REQUIRE (maxMagnitude (myAudioBlock) <= Catch::Approx (1.0f)); 138 | ``` 139 | 140 | ### magnitudeOfFrequency 141 | 142 | ```cpp 143 | REQUIRE (magnitudeOfFrequency (myAudioBlock, 440.f, sampleRate) < 0.005); 144 | ``` 145 | 146 | This one is my favorite. 147 | 148 | Use this if you know the frequencies you expect to encounter in the AudioBlock. You'll need to pass it the sample rate 149 | you are using. 150 | 151 | This uses frequency correlation and compares your AudioBlock against sine and cosine probes. It's essentially the same 152 | thing as what goes on under the hood with DFT, but for exactly the frequency you specify. 153 | 154 | This is a much more accurate way to confirm the presence of a known frequency than FFT. 155 | 156 | ### normalized 157 | 158 | ```cpp 159 | REQUIRE_THAT (normalized (myAudioBlock), isEqualTo(someOtherBlock)); 160 | ``` 161 | 162 | Normalizes a block to a range of -1 to 1. 163 | 164 | ### isBetween 165 | 166 | This is vector only. 167 | 168 | ```cpp 169 | REQUIRE_THAT (myStdVector, isBetween (0.0f, 1.0f)); 170 | ``` 171 | 172 | ### FFT 173 | 174 | There are various FFT related functions available which rely on creating an instance of the FFT class. 175 | 176 | Tip: Prefer the `magnitudeOfFrequency` helper if you know the frequency you are expecting and wish to somewhat 177 | accurately confirm magnitude. FFT is inherently messy. You'll get better results when your expected frequencies are in 178 | the middle of FFT bins. 179 | 180 | However, you can still sorta sloppily check for the strongest frequency: 181 | 182 | ```cpp 183 | REQUIRE (FFT (myAudioBlock, 44100.0f).strongestFrequencyIs (200.f)); 184 | ``` 185 | 186 | ```cpp 187 | auto fft = FFT (myAudioBlock, 44100.f); 188 | fft.printDebug(); // prints the bins 189 | REQUIRE (fft.strongestFrequencyBin() == fft.frequencyBinFor (220.f)); 190 | ``` 191 | 192 | ```cpp 193 | auto fft = FFT (myAudioBlock, 44100.f); 194 | REQUIRE_THAT (fft.strongFrequencyBins(), Catch::Matchers::Contains (fft.frequencyBinFor (220.f))); 195 | ``` 196 | 197 | ## Installing 198 | 199 | Prerequisites: 200 | 201 | 1. Catch2 202 | 2. The [melatonin_audio_sparklines](https://github.com/sudara/melatonin_audio_sparklines) JUCE module. It provides 203 | Catch2 with fancy descriptions of AudioBlocks when tests fail. 204 | 205 | ### Add the module 206 | 207 | Set up with a git submodule tracking the `main` branch: 208 | 209 | ```git 210 | git submodule add -b main https://github.com/sudara/melatonin_test_helpers modules/melatonin_test_helpers 211 | git commit -m "Added melatonin_test_helpers submodule." 212 | ``` 213 | 214 | To update these test helpers, you can: 215 | 216 | ```git 217 | git submodule update --remote --merge modules/melatonin_test_helpers 218 | ``` 219 | 220 | If you use CMake, inform JUCE about the module in your `CMakeLists.txt`: 221 | 222 | ```cmake 223 | juce_add_module("modules/melatonin_test_helpers") 224 | ``` 225 | 226 | And link your target against it (using `PRIVATE`, as juce recommends for modules): 227 | 228 | ```cmake 229 | target_link_libraries(my_target PRIVATE melatonin_test_helpers) 230 | ``` 231 | 232 | ### Include the module and use the melatonin namespace 233 | 234 | Add the following to any .cpp you'd like to use the helpers: 235 | 236 | ```cpp 237 | #include "melatonin_test_helpers/melatonin_test_helpers.h" 238 | 239 | // make your life easier while you are at it... 240 | using namespace melatonin; 241 | 242 | ``` 243 | 244 | ## Updating from melatonin_audio_block_test_helpers 245 | 246 | The repo has been renamed now that we support `AudioBuffers`. You'll want to do 2 things: 247 | 248 | 1. [Update the submodule url](https://stackoverflow.com/questions/913701/how-to-change-the-remote-repository-for-a-git-submodule) 249 | 250 | ``` 251 | git submodule set-url -- modules/melatonin_audio_block_test_helpers https://github.com/sudara/melatonin_test_helpers 252 | ``` 253 | 254 | 2. Rename it locally (this will update .gitmodules) 255 | 256 | ``` 257 | git mv modules/melatonin_audio_block_test_helpers modules/melatonin_test_helpers 258 | ``` 259 | 260 | ## Caveats 261 | 262 | 1. The matchers are the "new style" and require Catch2 v3.x. 263 | 2. This is just a random assortment of things I actually use for my tests! I'm open to more things being added, submit a PR! 264 | 3. It's possible some multi-channel, float/double or block/buffer variants are missing. Open a PR! 265 | 266 | ## Acknowledgements 267 | 268 | * Thanks to @chrhaase for improving module compatibility and performance. 269 | -------------------------------------------------------------------------------- /melatonin/AudioBlockFFT.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace melatonin 4 | { 5 | template 6 | class FFT 7 | { 8 | public: 9 | FFT (AudioBlock& b, float rate, bool scale = true, bool debug = false) : block (b), sampleRate (rate) 10 | { 11 | // fill up all 1024 samples 12 | for (size_t i = 0; i < fftSize; i++) 13 | fftData[i] = block.getSample (0, (int) (i % block.getNumSamples())); 14 | 15 | // Hann is best for sinusoids 16 | window.multiplyWithWindowingTable (fftData.getData(), fftSize); 17 | fft.performFrequencyOnlyForwardTransform (fftData.getData()); 18 | 19 | SampleType maxValue = 0.0; 20 | 21 | // Normalize from fftSize magnitudes down to a 0-1 magnitude range 22 | for (size_t i = 0; i < fftSize; i++) 23 | { 24 | fftData[i] = fftData[i] / fftSize; 25 | maxValue = fftData[i] > maxValue ? fftData[i] : maxValue; 26 | if (debug) 27 | DBG (fftData[i]); 28 | } 29 | 30 | // Different tests use different amplitudes... 31 | if (scale) 32 | { 33 | for (size_t i = 0; i < fftSize; i++) 34 | { 35 | fftData[i] = fftData[i] * (SampleType) 0.5 / maxValue; // 0.5 is our arbitrary max value for a bin 36 | } 37 | } 38 | } 39 | 40 | // VORSICHT!: This will only return the strongest *single* bin 41 | // But in reality FFT is sloppy and a single freq can easily spread across bins 42 | size_t frequencyBinFor (float frequency) const 43 | { 44 | // add 0.5 so that the implicit truncation behaves as rounding 45 | return (size_t) ((frequency / sampleRate * fftSize) + 0.5); 46 | } 47 | 48 | float approxFrequencyForBin (size_t bin) const 49 | { 50 | float binWidth = sampleRate / fftSize; 51 | return (float) (bin + 1) * binWidth; 52 | } 53 | 54 | [[maybe_unused]] std::vector frequencyBinsFor (const std::vector& frequencies) const 55 | { 56 | std::vector frequencyBins; 57 | 58 | for (const auto freq : frequencies) 59 | frequencyBins.push_back (frequencyBinFor (freq)); 60 | 61 | return frequencyBins; 62 | } 63 | 64 | bool strongestFrequencyIs (const float frequency) const 65 | { 66 | // Ideally we could detect single sine waves of magnitude 1.0 and call it a day. 67 | // However, FFT is messy. Frequencies might be split between bins 68 | // We might be ramping up, have multiple frequencies present, etc. 69 | auto index = frequencyBinFor (frequency); 70 | return fftData[index] == juce::FloatVectorOperations::findMaximum (fftData, numberOfBins); 71 | } 72 | 73 | size_t strongestFrequencyBin() 74 | { 75 | size_t strongestBin = 0; 76 | for (size_t i = 0; i < numberOfBins; i++) 77 | { 78 | //DBG(fftData[i]); 79 | if (fftData[i] > fftData[strongestBin]) 80 | { 81 | strongestBin = i; 82 | } 83 | } 84 | return strongestBin; 85 | } 86 | 87 | std::vector strongFrequencyBins() const 88 | { 89 | std::vector strongFrequencyBins; 90 | for (size_t i = 0; i < numberOfBins; i++) 91 | { 92 | //DBG(fftData[i]); 93 | if (fftData[i] > 0.05) 94 | strongFrequencyBins.push_back (i); 95 | } 96 | return strongFrequencyBins; 97 | } 98 | 99 | bool frequencyNotPresent (const float frequency) const 100 | { 101 | // Ideally, FFT would be more accurate. 102 | // However, bleed of 0.01 is often seen 2 bins over 103 | return fftData[frequencyBinFor (frequency)] < 0.02; 104 | } 105 | 106 | /* Produces an output like the following: 107 | 108 | FFT bins | freq | signal 109 | 9 | 215.332 | 0.246468 110 | 10 | 236.865 | 0.5 111 | 11 | 258.398 | 0.192688 112 | 12 | 279.932 | 0.118243 113 | 13 | 301.465 | 0.091966 114 | */ 115 | [[maybe_unused]] void printDebug() const 116 | { 117 | DBG ("FFT bins | freq | signal"); 118 | for (auto bin : strongFrequencyBins()) 119 | { 120 | DBG (bin << " | " << approxFrequencyForBin (bin) << " | " << fftData[bin]); 121 | } 122 | } 123 | 124 | private: 125 | AudioBlock& block; 126 | float sampleRate; 127 | const size_t fftSize = 2048; 128 | const size_t numberOfBins = fftSize / 2; 129 | juce::HeapBlock fftData { fftSize * 2, true }; // Even though we aren't calculating negative freqs, still needs to be 2x the size 130 | juce::dsp::WindowingFunction window { fftSize, juce::dsp::WindowingFunction::WindowingMethod::hann }; 131 | juce::dsp::FFT fft { 11 }; // 2^10 (10th order) is the same as a size of 1024 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /melatonin/block_and_buffer_matchers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "melatonin_audio_sparklines/melatonin_audio_sparklines.h" 3 | 4 | // This teaches Catch how to convert an AudioBlock to a string 5 | // This allows us to print out detail about the AudioBlock on matcher failure 6 | namespace Catch 7 | { 8 | template <> 9 | struct StringMaker> 10 | { 11 | static std::string convert (juce::dsp::AudioBlock const& value) 12 | { 13 | return melatonin::sparkline (value).toStdString(); 14 | } 15 | }; 16 | 17 | template <> 18 | struct StringMaker> 19 | { 20 | static std::string convert (juce::dsp::AudioBlock const& value) 21 | { 22 | return melatonin::sparkline (value).toStdString(); 23 | } 24 | }; 25 | 26 | template <> 27 | struct StringMaker> 28 | { 29 | static std::string convert (juce::AudioBuffer value) 30 | { 31 | return melatonin::sparkline (value).toStdString(); 32 | } 33 | }; 34 | 35 | template <> 36 | struct StringMaker> 37 | { 38 | static std::string convert (juce::AudioBuffer& value) 39 | { 40 | return melatonin::sparkline (value).toStdString(); 41 | } 42 | }; 43 | 44 | } 45 | 46 | namespace melatonin 47 | { 48 | 49 | struct isValidAudio : Catch::Matchers::MatcherGenericBase 50 | { 51 | template 52 | [[nodiscard]] bool match (const AudioBlock& block) const 53 | { 54 | return validAudio (block); 55 | } 56 | 57 | template 58 | [[nodiscard]] bool match (juce::AudioBuffer& buffer) const 59 | { 60 | return validAudio (buffer); 61 | } 62 | 63 | [[nodiscard]] std::string describe() const override 64 | { 65 | return "Block is free of NaNs, INFs and subnormals\n"; 66 | } 67 | }; 68 | 69 | struct isFilled : Catch::Matchers::MatcherGenericBase 70 | { 71 | template 72 | [[nodiscard]] bool match (const AudioBlock& block) const 73 | { 74 | return blockIsFilled (block); 75 | } 76 | 77 | template 78 | [[nodiscard]] bool match (juce::AudioBuffer& block) const 79 | { 80 | return bufferIsFilled (block); 81 | } 82 | 83 | [[nodiscard]] std::string describe() const override 84 | { 85 | return "Block is completely filled"; 86 | } 87 | }; 88 | 89 | struct isEmpty : Catch::Matchers::MatcherGenericBase 90 | { 91 | template 92 | [[nodiscard]] bool match (const AudioBlock& block) const 93 | { 94 | return blockIsEmpty (block); 95 | } 96 | 97 | template 98 | [[nodiscard]] bool match (juce::AudioBuffer& block) const 99 | { 100 | return bufferIsEmpty (block); 101 | } 102 | 103 | [[nodiscard]] std::string describe() const override 104 | { 105 | return "Block is completely empty"; 106 | } 107 | }; 108 | 109 | struct isFilledUntil : Catch::Matchers::MatcherGenericBase 110 | { 111 | size_t boundary = 0; 112 | explicit isFilledUntil (size_t sampleNum) : boundary (sampleNum) {} 113 | 114 | template 115 | [[nodiscard]] bool match (const AudioBlock& block) const 116 | { 117 | return blockIsFilledUntil (block, (int) boundary); 118 | } 119 | 120 | template 121 | [[nodiscard]] bool match (juce::AudioBuffer& block) const 122 | { 123 | return bufferIsFilledUntil (block, (int) boundary); 124 | } 125 | 126 | [[nodiscard]] std::string describe() const override 127 | { 128 | std::ostringstream ss; 129 | ss << "Block is filled to sample " << boundary; 130 | return ss.str(); 131 | } 132 | }; 133 | 134 | struct isFilledAfter : Catch::Matchers::MatcherGenericBase 135 | { 136 | size_t boundary; 137 | explicit isFilledAfter (size_t sampleNum) : boundary (sampleNum) {} 138 | 139 | template 140 | [[nodiscard]] bool match (const AudioBlock& block) const 141 | { 142 | return blockIsFilledAfter (block, (int) boundary); 143 | } 144 | 145 | template 146 | [[nodiscard]] bool match (juce::AudioBuffer& block) const 147 | { 148 | return bufferIsFilledAfter (block, (int) boundary); 149 | } 150 | 151 | [[nodiscard]] std::string describe() const override 152 | { 153 | std::ostringstream ss; 154 | ss << "Block is filled after sample " << boundary; 155 | return ss.str(); 156 | } 157 | }; 158 | 159 | struct isFilledBetween : Catch::Matchers::MatcherGenericBase 160 | { 161 | size_t start; 162 | size_t end; 163 | isFilledBetween (size_t s, size_t e) : start (s), end (e) {} 164 | 165 | template 166 | [[nodiscard]] bool match (const AudioBlock& block) const 167 | { 168 | return blockIsFilledBetween (block, start, end); 169 | } 170 | 171 | template 172 | [[nodiscard]] bool match (juce::AudioBuffer& block) const 173 | { 174 | return bufferIsFilledBetween (block, start, end); 175 | } 176 | 177 | [[nodiscard]] std::string describe() const override 178 | { 179 | std::ostringstream ss; 180 | ss << "Block is filled between samples " << start << " and " << end; 181 | return ss.str(); 182 | } 183 | }; 184 | 185 | struct isEmptyAfter : Catch::Matchers::MatcherGenericBase 186 | { 187 | size_t boundary = 0; 188 | 189 | explicit isEmptyAfter (size_t sampleNum) : boundary (sampleNum) {} 190 | 191 | template 192 | [[nodiscard]] bool match (const AudioBlock& block) const 193 | { 194 | return blockIsEmptyAfter (block, boundary); 195 | } 196 | 197 | template 198 | [[nodiscard]] bool match (juce::AudioBuffer& block) const 199 | { 200 | return bufferIsEmptyAfter (block, boundary); 201 | } 202 | 203 | [[nodiscard]] std::string describe() const override 204 | { 205 | std::ostringstream ss; 206 | ss << "Block is empty after sample " << boundary; 207 | return ss.str(); 208 | } 209 | }; 210 | 211 | struct isEmptyUntil : Catch::Matchers::MatcherGenericBase 212 | { 213 | size_t boundary = 0; 214 | explicit isEmptyUntil (size_t sampleNum) : boundary (sampleNum) {} 215 | 216 | template 217 | [[nodiscard]] bool match (const AudioBlock& block) const 218 | { 219 | return blockIsEmptyUntil (block, boundary); 220 | } 221 | 222 | template 223 | [[nodiscard]] bool match (juce::AudioBuffer& block) const 224 | { 225 | return bufferIsEmptyUntil (block, boundary); 226 | } 227 | 228 | [[nodiscard]] std::string describe() const override 229 | { 230 | std::ostringstream ss; 231 | ss << "Block is empty until sample " << boundary; 232 | return ss.str(); 233 | } 234 | }; 235 | 236 | struct hasRMS : Catch::Matchers::MatcherGenericBase 237 | { 238 | double expectedRMS = 0; 239 | mutable double actualRMS = 0; 240 | double tolerance = 0; 241 | explicit hasRMS (double r, double t = 0.0001) : expectedRMS (r), tolerance (t) {} 242 | 243 | template 244 | [[nodiscard]] bool match (const AudioBlock& block) const 245 | { 246 | actualRMS = rms (block); 247 | return std::abs (actualRMS - expectedRMS) < tolerance; 248 | } 249 | 250 | [[nodiscard]] std::string describe() const override 251 | { 252 | std::ostringstream ss; 253 | ss << "Block RMS of " << actualRMS << " was expected to be within " << tolerance << " of " << expectedRMS; 254 | return ss.str(); 255 | } 256 | }; 257 | 258 | // supports audioblock and std::vector 259 | template 260 | struct isEqualTo : Catch::Matchers::MatcherGenericBase 261 | { 262 | juce::HeapBlock expectedData; 263 | AudioBlock expected = {}; 264 | std::vector expectedVector = {}; 265 | mutable std::vector testedVector = {}; 266 | const float tolerance = 0; 267 | mutable size_t sampleNumber = 0; 268 | mutable double blockValue = 0; 269 | mutable double expectedValue = 0; 270 | mutable std::string descriptionOfOther = ""; 271 | 272 | explicit isEqualTo (const AudioBlock& e, float t = std::numeric_limits::epsilon() * 100) 273 | : expected (e), tolerance (t) {} 274 | 275 | // allow us to easily compare vector to vector 276 | // needed because Catch::Matchers::Approx for std::vector is broken around 0.0 277 | // convenient for test writing 278 | explicit isEqualTo (const std::vector& vector, float t = std::numeric_limits::epsilon() * 100) 279 | : expected(), expectedVector (vector), tolerance (t) 280 | { 281 | // also populate the expected block in case we want to compare incoming vector against blocks 282 | expected = AudioBlock (expectedData, 1, expectedVector.size()); 283 | for (int i = 0; i < (int) expectedVector.size(); ++i) 284 | expected.setSample (0, i, expectedVector[(size_t) i]); 285 | } 286 | 287 | [[nodiscard]] bool match (AudioBlock& block) const 288 | { 289 | jassert (expected.getNumSamples() == block.getNumSamples()); 290 | for (int channel = 0; channel < (int) block.getNumChannels(); ++channel) 291 | { 292 | // TODO: clean this up! 293 | for (int i = 0; i < (int) block.getNumSamples(); ++i) 294 | { 295 | testedVector.push_back (block.getSample (channel, i)); 296 | } 297 | for (int i = 0; i < (int) block.getNumSamples(); ++i) 298 | { 299 | // juce::approximatelyEqual was not quite tolerant enough for my needs 300 | // if you are doing things like adding deltas 100 times vs. multiplying a delta by 1000, you'll need more 301 | if (!juce::isWithin (expected.getSample (channel, i), block.getSample (channel, i), tolerance)) 302 | { 303 | sampleNumber = (size_t) i; 304 | blockValue = block.getSample (channel, i); 305 | expectedValue = expected.getSample (channel, i); 306 | return false; 307 | } 308 | } 309 | } 310 | return true; 311 | } 312 | 313 | [[nodiscard]] bool 314 | match (juce::AudioBuffer& block) const 315 | { 316 | return match (AudioBlock (block)); 317 | } 318 | 319 | [[nodiscard]] bool match (const std::vector& vector) const 320 | { 321 | // hi, you have to write your expectation for the exact number of elements! 322 | if (vector.size() != expectedVector.size()) 323 | jassertfalse; 324 | 325 | testedVector = vector; 326 | 327 | for (auto& value : vector) 328 | { 329 | if (!juce::isWithin (expectedVector[sampleNumber], value, tolerance)) 330 | { 331 | expectedValue = expectedVector[sampleNumber]; 332 | return false; 333 | } 334 | sampleNumber++; 335 | } 336 | return true; 337 | } 338 | 339 | std::string describe() const override 340 | { 341 | if (descriptionOfOther.empty()) 342 | descriptionOfOther = sparkline (expected).toStdString(); 343 | 344 | auto expectedString = expectedVector.empty() ? blockToString(expected) : vectorToString (expectedVector); 345 | std::ostringstream ss; 346 | ss << "is equal to \n" 347 | << descriptionOfOther << "\n"; 348 | ss << "Expected: " << expectedString << "\nActual: " << melatonin::vectorToString (testedVector); 349 | return ss.str(); 350 | } 351 | }; 352 | } 353 | -------------------------------------------------------------------------------- /melatonin/block_and_buffer_test_helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace melatonin 4 | { 5 | template 6 | using AudioBlock = juce::dsp::AudioBlock; 7 | 8 | // Ensures there's no INF, NaN or subnormals in the block 9 | template 10 | static inline bool validAudio (const AudioBlock& block) 11 | { 12 | for (size_t c = 0; c < block.getNumChannels(); ++c) 13 | for (size_t i = 0; i < block.getNumSamples(); ++i) 14 | { 15 | auto value = std::fpclassify (block.getSample ((int) c, (int) i)); 16 | if (value == FP_SUBNORMAL || value == FP_INFINITE || value == FP_NAN) 17 | return false; 18 | } 19 | return true; 20 | } 21 | 22 | template 23 | static inline bool validAudio (juce::AudioBuffer& buffer) 24 | { 25 | auto block = AudioBlock (buffer); 26 | return validAudio (block); 27 | } 28 | 29 | // returns the number of full cycles of a waveform contained by a block 30 | template 31 | static inline int numberOfCycles (const AudioBlock& block) 32 | { 33 | auto waveform = block.getChannelPointer (0); 34 | int numberOfZeroCrossings = 0; 35 | for (size_t i = 0; i < block.getNumSamples(); ++i) 36 | { 37 | if (waveform[i] == 0 && ((i == 0) || waveform[i - 1] != 0)) 38 | numberOfZeroCrossings++; 39 | if ((i > 0) && ((waveform[i - 1] < 0) != (waveform[i] < 0))) 40 | numberOfZeroCrossings++; 41 | } 42 | return numberOfZeroCrossings / 2; 43 | } 44 | 45 | template 46 | static inline bool numberOfCycles (juce::AudioBuffer& buffer) 47 | { 48 | const auto block = AudioBlock (buffer); 49 | return numberOfCycles (block); 50 | } 51 | 52 | template 53 | static inline bool channelsAreIdentical (const AudioBlock& block) 54 | { 55 | jassert (block.getNumChannels() > 1); 56 | for (size_t i = 0; i < block.getNumSamples(); ++i) 57 | { 58 | float channelZeroValue = block.getSample (0, (int) i); 59 | for (size_t c = 0; c < block.getNumChannels(); ++c) 60 | { 61 | if (block.getSample ((int) c, (int) i) != channelZeroValue) 62 | return false; 63 | } 64 | } 65 | return true; 66 | } 67 | 68 | template 69 | static inline bool channelsAreIdentical (juce::AudioBuffer& buffer) 70 | { 71 | const auto block = AudioBlock (buffer); 72 | return channelsAreIdentical (block); 73 | } 74 | 75 | template 76 | static inline SampleType maxMagnitude (const AudioBlock& block) 77 | { 78 | float max = 0.0; 79 | for (size_t c = 0; c < block.getNumChannels(); ++c) 80 | { 81 | auto channel_max = juce::FloatVectorOperations::findMaximum (block.getChannelPointer (c), block.getNumSamples()); 82 | auto channel_abs_min = abs (juce::FloatVectorOperations::findMinimum (block.getChannelPointer (c), block.getNumSamples())); 83 | if (channel_max > max) 84 | max = channel_max; 85 | else if (channel_abs_min > max) 86 | max = channel_abs_min; 87 | } 88 | return max; 89 | } 90 | 91 | template 92 | static inline SampleType minMagnitude (const AudioBlock& block) 93 | { 94 | float min = 1e100f; // a very large number 95 | for (size_t c = 0; c < block.getNumChannels(); ++c) 96 | { 97 | auto channel_min = juce::FloatVectorOperations::findMinimum (block.getChannelPointer (c), block.getNumSamples()); 98 | auto channel_abs_max = abs (juce::FloatVectorOperations::findMaximum (block.getChannelPointer (c), block.getNumSamples())); 99 | if (channel_min < min) 100 | min = channel_min; 101 | else if (channel_abs_max < min) 102 | min = channel_abs_max; 103 | } 104 | return min; 105 | } 106 | 107 | template 108 | static inline SampleType maxMagnitude (juce::AudioBuffer& buffer) 109 | { 110 | const auto block = AudioBlock (buffer); 111 | return maxMagnitude (block); 112 | } 113 | 114 | template 115 | static inline bool betweenMagnitudes(const AudioBlock& block, SampleType min, SampleType max) 116 | { 117 | return minMagnitude (block) >= min && maxMagnitude (block) <= max; 118 | } 119 | 120 | template 121 | static inline SampleType rms (const AudioBlock& block) 122 | { 123 | float sum = 0.0; 124 | for (int c = 0; c < (int) block.getNumChannels(); ++c) 125 | { 126 | for (int i = 0; i < (int) block.getNumSamples(); ++i) 127 | { 128 | sum += (float) block.getSample (c, i) * (float) block.getSample (c, i); 129 | } 130 | } 131 | 132 | return static_cast (std::sqrt (sum / float (block.getNumSamples() * block.getNumChannels()))); 133 | } 134 | 135 | template 136 | static inline SampleType rms (juce::AudioBuffer& buffer) 137 | { 138 | const auto block = AudioBlock (buffer); 139 | return rms (block); 140 | } 141 | 142 | template 143 | static inline SampleType rmsInDB (const AudioBlock& block) 144 | { 145 | return static_cast (juce::Decibels::gainToDecibels (rms (block))); 146 | } 147 | 148 | template 149 | static inline SampleType rmsInDB (juce::AudioBuffer& buffer) 150 | { 151 | const auto block = AudioBlock (buffer); 152 | return rmsInDB (block); 153 | } 154 | 155 | template 156 | static inline void fillBlock(AudioBlock& block, std::vector&& values) 157 | { 158 | jassert (block.getNumChannels() == 1); 159 | jassert (block.getNumSamples() == values.size()); 160 | for (size_t i = 0; i < block.getNumSamples(); ++i) 161 | { 162 | block.setSample (0, i, values[i]); 163 | } 164 | } 165 | 166 | template 167 | static inline AudioBlock& fillBlockWithFunction (AudioBlock& block, const std::function& function, float frequency, float sampleRate, float gain = 1.0f, bool accumulate = false) 168 | { 169 | auto angleDelta = juce::MathConstants::twoPi * frequency / sampleRate; 170 | for (int c = 0; c < (int) block.getNumChannels(); ++c) 171 | { 172 | auto currentAngle = 0.0f; 173 | for (int i = 0; i < (int) block.getNumSamples(); ++i) 174 | { 175 | auto sampleValue = gain * function (currentAngle); 176 | block.setSample (c, i, accumulate ? block.getSample (c, i) + sampleValue : sampleValue); 177 | currentAngle += angleDelta; 178 | if (currentAngle >= juce::MathConstants::pi) 179 | currentAngle -= juce::MathConstants::twoPi; 180 | } 181 | } 182 | return block; 183 | } 184 | 185 | template 186 | static inline juce::AudioBuffer& fillBufferWithFunction (juce::AudioBuffer& buffer, const std::function& function, float frequency, float sampleRate, float gain = 1.0f) 187 | { 188 | const auto block = AudioBlock (buffer); 189 | return fillBlockWithFunction (block, function, frequency, sampleRate, gain); 190 | } 191 | 192 | // assumes mono for now 193 | template 194 | static inline AudioBlock& fillWithSine (AudioBlock& block, float frequency, float sampleRate, float gain = 1.0f) 195 | { 196 | return fillBlockWithFunction ( 197 | block, [] (float angle) { return juce::dsp::FastMathApproximations::sin (angle); }, frequency, sampleRate, gain); 198 | } 199 | 200 | template 201 | static inline AudioBlock& addSineToBlock (AudioBlock& block, float frequency, float sampleRate, float gain = 1.0f) 202 | { 203 | return fillBlockWithFunction ( 204 | block, [] (float angle) { return juce::dsp::FastMathApproximations::sin (angle); }, frequency, sampleRate, gain, true); 205 | } 206 | 207 | template 208 | static inline juce::AudioBuffer& fillBufferWithSine (juce::AudioBuffer& buffer, float frequency, float sampleRate, float gain = 1.0f) 209 | { 210 | const auto block = AudioBlock (buffer); 211 | return fillWithSine (block, frequency, sampleRate, gain); 212 | } 213 | 214 | // assumes mono for now 215 | template 216 | static inline AudioBlock& fillWithCosine (AudioBlock& block, float frequency, float sampleRate, float gain = 1.0f) 217 | { 218 | return fillBlockWithFunction ( 219 | block, [] (float angle) { return juce::dsp::FastMathApproximations::cos (angle); }, frequency, sampleRate, gain); 220 | } 221 | 222 | template 223 | static inline juce::AudioBuffer& fillBufferWithCosine (juce::AudioBuffer& buffer, float frequency, float sampleRate, float gain = 1.0f) 224 | { 225 | const auto block = AudioBlock (buffer); 226 | return fillWithCosine (block, frequency, sampleRate, gain); 227 | } 228 | 229 | // all zeros 230 | template 231 | static inline bool blockIsEmpty (const AudioBlock& block) 232 | { 233 | for (size_t i = 0; i < block.getNumChannels(); ++i) 234 | { 235 | if (!juce::FloatVectorOperations::findMinAndMax (block.getChannelPointer (i), (int) block.getNumSamples()).isEmpty()) 236 | { 237 | return false; 238 | } 239 | } 240 | return true; 241 | } 242 | 243 | template 244 | static inline bool bufferIsEmpty (juce::AudioBuffer& buffer) 245 | { 246 | const auto block = AudioBlock (buffer); 247 | return blockIsEmpty (block); 248 | } 249 | 250 | template 251 | static inline bool blockIsEmptyUntil (const AudioBlock& block, size_t numSamples) 252 | { 253 | jassert (block.getNumSamples() >= numSamples); 254 | 255 | for (size_t i = 0; i < block.getNumChannels(); ++i) 256 | { 257 | if (!juce::FloatVectorOperations::findMinAndMax (block.getChannelPointer (i), (int) numSamples).isEmpty()) 258 | { 259 | return false; 260 | } 261 | } 262 | return true; 263 | } 264 | 265 | template 266 | static inline bool bufferIsEmptyUntil (juce::AudioBuffer& buffer, size_t numSamples) 267 | { 268 | const auto block = AudioBlock (buffer); 269 | return blockIsEmptyUntil (block, numSamples); 270 | } 271 | 272 | template 273 | static inline bool blockIsEmptyAfter (const AudioBlock& block, size_t firstZeroAt) 274 | { 275 | jassert (block.getNumSamples() >= firstZeroAt); 276 | 277 | if (block.getNumSamples() == firstZeroAt) 278 | return true; 279 | 280 | auto numSamples = block.getNumSamples() - firstZeroAt; 281 | auto subBlock = block.getSubBlock (firstZeroAt, numSamples); 282 | for (size_t i = 0; i < subBlock.getNumChannels(); ++i) 283 | { 284 | if (!juce::FloatVectorOperations::findMinAndMax (subBlock.getChannelPointer (i), (int) numSamples).isEmpty()) 285 | { 286 | return false; 287 | } 288 | } 289 | return true; 290 | } 291 | 292 | template 293 | static inline bool bufferIsEmptyAfter (juce::AudioBuffer& buffer, size_t firstZeroAt) 294 | { 295 | const auto block = AudioBlock (buffer); 296 | return blockIsEmptyAfter (block, firstZeroAt); 297 | } 298 | 299 | template 300 | static inline bool blockIsFilled (const AudioBlock& block) 301 | { 302 | for (size_t ch = 0; ch < block.getNumChannels(); ++ch) 303 | { 304 | auto channelBlock = block.getSingleChannelBlock (ch); 305 | if (numberOfConsecutiveZeros (channelBlock) > 0) 306 | return false; 307 | } 308 | return true; 309 | } 310 | 311 | template 312 | static inline bool bufferIsFilled (juce::AudioBuffer& buffer) 313 | { 314 | const auto block = AudioBlock (buffer); 315 | return blockIsFilled (block); 316 | } 317 | 318 | template 319 | static inline bool blockIsFilledUntil (const AudioBlock& block, int sampleNum) 320 | { 321 | jassert ((int) block.getNumSamples() >= sampleNum); 322 | 323 | for (int c = 0; c < (int) block.getNumChannels(); ++c) 324 | { 325 | for (int i = 0; i < sampleNum; ++i) 326 | { 327 | if (i > 0 && (juce::approximatelyEqual (block.getSample (c, i), {}) && juce::approximatelyEqual (block.getSample (c, i - 1), {}))) 328 | return false; 329 | } 330 | } 331 | return true; 332 | } 333 | 334 | template 335 | static inline bool bufferIsFilledUntil (juce::AudioBuffer& buffer, int sampleNum) 336 | { 337 | const auto block = AudioBlock (buffer); 338 | return blockIsFilledUntil (block, sampleNum); 339 | } 340 | 341 | template 342 | static inline bool blockIsFilledAfter (const AudioBlock& block, int sampleNum) 343 | { 344 | jassert ((int) block.getNumSamples() >= sampleNum); 345 | 346 | if ((int) block.getNumSamples() == sampleNum) 347 | return false; 348 | 349 | for (int c = 0; c < (int) block.getNumChannels(); ++c) 350 | { 351 | for (int i = sampleNum; i < (int) block.getNumSamples(); ++i) 352 | { 353 | if (i > sampleNum && ((block.getSample (c, i) == 0) && (block.getSample (c, i - 1) == 0))) 354 | return false; 355 | } 356 | } 357 | return true; 358 | } 359 | 360 | template 361 | static inline bool bufferIsFilledAfter (juce::AudioBuffer& buffer, int sampleNum) 362 | { 363 | const auto block = AudioBlock (buffer); 364 | return blockIsFilledAfter (block, sampleNum); 365 | } 366 | 367 | template 368 | static inline bool blockIsFilledBetween (const AudioBlock& block, int start, int end) 369 | { 370 | jassert (end > start); 371 | for (int c = 0; c < (int) block.getNumChannels(); ++c) 372 | { 373 | for (int i = start; i < (end - start); ++i) 374 | { 375 | if (i > start && ((block.getSample (c, i) == 0) && (block.getSample (c, i - 1) == 0))) 376 | return false; 377 | } 378 | } 379 | return true; 380 | } 381 | 382 | template 383 | static inline bool bufferIsFilledBetween (juce::AudioBuffer& buffer, int start, int end) 384 | { 385 | const auto block = AudioBlock (buffer); 386 | return blockIsFilledBetween (block, start, end); 387 | } 388 | 389 | // MONO FOR NOW 390 | // Manual frequency correlation using a known frequency 391 | // https://github.com/juce-framework/JUCE/blob/master/modules/juce_dsp/frequency/juce_FFT_test.cpp#L59-L82 392 | template 393 | static inline float magnitudeOfFrequency (const AudioBlock& block, float freq, float sampleRate) 394 | { 395 | // happy to allocate memory here, this is a test, not real time audio 396 | juce::HeapBlock sineData; 397 | juce::HeapBlock cosineData; 398 | 399 | const size_t length = block.getNumSamples(); 400 | 401 | // we can get more accurate results by assuming the block is full with the frequency 402 | // and only taking an integer number of cycles out of the block 403 | const int lastFullCycle = (int) length - ((int) length % (int) (sampleRate / freq)); 404 | 405 | auto sineBlock = juce::dsp::AudioBlock (sineData, block.getNumChannels(), length); 406 | auto cosineBlock = juce::dsp::AudioBlock (cosineData, block.getNumChannels(), length); 407 | 408 | fillWithSine (sineBlock, freq, sampleRate).multiplyBy (block); 409 | fillWithCosine (cosineBlock, freq, sampleRate).multiplyBy (block); 410 | 411 | float sineSum = 0; 412 | float cosineSum = 0; 413 | 414 | for (int i = 0; i < lastFullCycle; ++i) 415 | { 416 | sineSum += sineBlock.getSample (0, i); 417 | cosineSum += cosineBlock.getSample (0, i); 418 | } 419 | return std::sqrt ((float) juce::square (sineSum / (float) lastFullCycle) + juce::square (cosineSum / (float) lastFullCycle)) * 2.0f; 420 | } 421 | 422 | template 423 | static inline float magnitudeOfFrequency (juce::AudioBuffer& buffer, float freq, float sampleRate) 424 | { 425 | const auto block = AudioBlock (buffer); 426 | return magnitudeOfFrequency (block, freq, sampleRate); 427 | } 428 | 429 | template 430 | static inline AudioBlock& normalized (AudioBlock& block) 431 | { 432 | for (auto channel = 0; channel < block.getNumChannels(); ++channel) 433 | { 434 | float absMax = abs (juce::FloatVectorOperations::findMaximum (block.getChannelPointer (channel), block.getNumSamples())); 435 | float absMin = abs (juce::FloatVectorOperations::findMinimum (block.getChannelPointer (channel), block.getNumSamples())); 436 | float max = juce::jmax (absMin, absMax); 437 | if (max > 0.0) 438 | { 439 | juce::FloatVectorOperations::multiply (block.getChannelPointer (channel), 1 / max, block.getNumSamples()); 440 | } 441 | } 442 | return block; 443 | } 444 | 445 | template 446 | static inline AudioBlock& normalized (juce::AudioBuffer& buffer) 447 | { 448 | const auto block = AudioBlock (buffer); 449 | return normalized (block); 450 | } 451 | 452 | template 453 | static inline AudioBlock& reverse (AudioBlock& block) 454 | { 455 | juce::HeapBlock tempHeap; 456 | auto reversedBlock = AudioBlock (tempHeap, block.getNumChannels(), block.getNumSamples()); 457 | 458 | for (int channel = 0; channel < (int) block.getNumChannels(); ++channel) 459 | { 460 | for (int i = 0; i < (int) block.getNumSamples(); ++i) 461 | reversedBlock.setSample (channel, i, block.getSample (channel, (int) block.getNumSamples() - 1 - i)); 462 | } 463 | block.copyFrom (reversedBlock); 464 | return block; 465 | } 466 | 467 | template 468 | static inline AudioBlock& reverse (juce::AudioBuffer& buffer) 469 | { 470 | const auto block = AudioBlock (buffer); 471 | return reverse (block); 472 | } 473 | 474 | template 475 | void printHistogram (AudioBlock& block) 476 | { 477 | // Calculate the range of the samples 478 | float rangeStart = juce::FloatVectorOperations::findMinimum (block.getChannelPointer (0), block.getNumSamples()); 479 | double rangeEnd = juce::FloatVectorOperations::findMaximum (block.getChannelPointer (0), block.getNumSamples()); 480 | 481 | // Calculate the histogram of the samples 482 | const size_t numBins = 10; // Number of bins for histogram 483 | std::vector histogram (numBins, 0); 484 | const double binSize = (rangeEnd - rangeStart) / numBins; 485 | 486 | for (auto i = 0; i < block.getNumSamples(); ++i) 487 | { 488 | size_t binIndex = static_cast ((block.getSample (0, i) - rangeStart) / binSize); 489 | histogram[binIndex]++; 490 | } 491 | 492 | // Print the histogram 493 | for (size_t i = 0; i < numBins; ++i) 494 | { 495 | std::cout << "[" << rangeStart + i * binSize << ", " << rangeStart + (i + 1) * binSize << "): "; 496 | for (size_t j = 0; j < histogram[i]; ++j) 497 | std::cout << "|"; 498 | std::cout << std::endl; 499 | } 500 | } 501 | 502 | template 503 | bool isUniformlyDistributed (AudioBlock& block) 504 | { 505 | const double epsilon = 0.3; // lol, this is pretty accepting... 506 | 507 | // Calculate the range of the samples 508 | float rangeStart = juce::FloatVectorOperations::findMinimum (block.getChannelPointer (0), block.getNumSamples()); 509 | double rangeEnd = juce::FloatVectorOperations::findMaximum (block.getChannelPointer (0), block.getNumSamples()); 510 | 511 | // Calculate the histogram of the samples 512 | const size_t numBins = 10; // Number of bins for histogram 513 | std::vector histogram (numBins, 0); 514 | const double binSize = (rangeEnd - rangeStart) / numBins; 515 | 516 | for (auto i = 0; i < block.getNumSamples(); ++i) 517 | { 518 | auto binIndex = juce::jlimit(size_t(0), numBins - 1, static_cast ((block.getSample (0, i) - rangeStart) / binSize)); 519 | histogram[binIndex]++; 520 | } 521 | 522 | // Check if the histogram values are relatively equal 523 | const double expectedCount = block.getNumSamples() / static_cast (numBins); 524 | for (const size_t& count : histogram) 525 | { 526 | if (std::abs (count - expectedCount) > epsilon * expectedCount) 527 | { 528 | return false; 529 | } 530 | } 531 | return true; 532 | } 533 | 534 | template 535 | float average (AudioBlock& block) 536 | { 537 | float sum = 0; 538 | for (int i = 0; i < block.getNumSamples(); ++i) 539 | sum += block.getSample (0, i); 540 | return sum / (float) block.getNumSamples(); 541 | } 542 | 543 | } 544 | -------------------------------------------------------------------------------- /melatonin/mock_playheads.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace melatonin 4 | { 5 | class NoPlayhead : public juce::AudioPlayHead 6 | { 7 | public: 8 | [[nodiscard]] juce::Optional getPosition() const override 9 | { 10 | return {}; 11 | } 12 | }; 13 | 14 | class PlayingPlayhead : public juce::AudioPlayHead 15 | { 16 | public: 17 | PlayingPlayhead() 18 | { 19 | info.setIsPlaying (true); 20 | } 21 | 22 | void setTempo (double bpm) 23 | { 24 | info.setBpm (bpm); 25 | } 26 | 27 | [[nodiscard]] juce::Optional getPosition() const override 28 | { 29 | return info; 30 | } 31 | 32 | void setIsPlaying (bool isPlaying) 33 | { 34 | info.setIsPlaying (isPlaying); 35 | } 36 | 37 | void advancePosition (size_t numSamples) 38 | { 39 | auto samplesPerBeat = 44100.f * 60.f / info.getBpm().orFallback (120.f); 40 | info.setPpqPosition (info.getPpqPosition().orFallback(0) + (float) numSamples / samplesPerBeat); 41 | } 42 | 43 | private: 44 | PositionInfo info; 45 | double sampleRate = 44100.0; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /melatonin/parameter_test_helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace melatonin 4 | { 5 | // You'll want to use this anytime you call someParam->setValueNotifyingHost (1.0f); 6 | static inline void waitForParameterChange(int ms=1) 7 | { 8 | juce::MessageManager::getInstance()->runDispatchLoopUntil (ms); 9 | juce::Timer::callPendingTimersSynchronously(); 10 | } 11 | 12 | // the apvts can take up to half a second (!) due to back off to flush its state 13 | // the only way to circumvent and get flushParameterValuesToValueTree to fire is by copying state 14 | // so we (pointlessly) copy the state and then run the dispatch loop 15 | static inline void flushAPVTS (juce::AudioProcessorValueTreeState& apvts) 16 | { 17 | auto unusedState = apvts.copyState(); 18 | waitForParameterChange(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /melatonin/vector_matchers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace melatonin 4 | { 5 | struct isBetween : Catch::Matchers::MatcherGenericBase 6 | { 7 | float min; 8 | float max; 9 | float margin; 10 | 11 | isBetween (float m, float x, float g = std::numeric_limits::epsilon() * 100) : min (m), max (x), margin (g) {} 12 | 13 | template 14 | [[nodiscard]] bool match (SampleType item) const 15 | { 16 | jassert (min < max); 17 | 18 | if (item < (min - margin) || item > (max + margin)) 19 | return false; 20 | return true; 21 | } 22 | 23 | template 24 | [[nodiscard]] bool match (std::vector array) const 25 | { 26 | jassert (min < max); 27 | 28 | for (auto& item : array) 29 | if (item < (min - margin) || item > (max + margin)) 30 | return false; 31 | return true; 32 | } 33 | 34 | template 35 | [[nodiscard]] bool match (juce::dsp::AudioBlock& block) const 36 | { 37 | jassert (min < max); 38 | 39 | for (int i = 0; i < (int) block.getNumSamples(); ++i) 40 | if (block.getSample (0, i) < min - margin || block.getSample (0, i) > max + margin) 41 | return false; 42 | return true; 43 | } 44 | 45 | [[nodiscard]] std::string describe() const override 46 | { 47 | std::ostringstream ss; 48 | ss << "\n items are between " 49 | << min << " and " << max << "\n"; 50 | return ss.str(); 51 | } 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /melatonin_test_helpers.h: -------------------------------------------------------------------------------- 1 | /* 2 | BEGIN_JUCE_MODULE_DECLARATION 3 | 4 | ID: melatonin_test_helpers 5 | vendor: Sudara 6 | version: 1.0.0 7 | name: Melatonin Catch2 Test Helpers 8 | description: Nobody Tests Audio Code (but don't forget to add Catch2 to your build!) 9 | license: MIT 10 | dependencies: juce_audio_processors,juce_dsp,melatonin_audio_sparklines 11 | 12 | END_JUCE_MODULE_DECLARATION 13 | */ 14 | 15 | #pragma once 16 | #include 17 | #include 18 | #include 19 | 20 | #include "juce_audio_processors/juce_audio_processors.h" 21 | #include 22 | #include 23 | 24 | #include "melatonin/AudioBlockFFT.h" 25 | #include "melatonin/block_and_buffer_test_helpers.h" 26 | #include "melatonin/block_and_buffer_matchers.h" 27 | #include "melatonin/vector_matchers.h" 28 | #include "melatonin/mock_playheads.h" 29 | #include "melatonin/parameter_test_helpers.h" 30 | -------------------------------------------------------------------------------- /tests/block_and_buffer_test_helpers.cpp: -------------------------------------------------------------------------------- 1 | #if RUN_MELATONIN_TESTS 2 | 3 | #endif 4 | --------------------------------------------------------------------------------