├── .gitignore ├── Doxyfile ├── LICENSE.txt ├── Makefile ├── README.md ├── benchmarks ├── envelopes-peak-hold │ ├── _others │ │ └── kvr-vadim-zavalishin.h │ ├── _previous │ │ ├── signalsmith-constant-v1.h │ │ └── signalsmith-run-length-amortised.h │ ├── peak-hold.cpp │ └── plots.py └── fft │ ├── _previous │ └── signalsmith-fft-v1.h │ ├── complex.cpp │ └── plots.py ├── dsp ├── LICENSE.txt ├── README.md ├── common.h ├── curves.h ├── delay.h ├── envelopes.h ├── fft.h ├── filters.h ├── mix.h ├── perf.h ├── rates.h ├── spectral.h └── windows.h ├── extra-style.js ├── git-sub-branch ├── historical-docs.sh ├── index.html ├── manual ├── diagrams │ ├── stft-buffer-validity.svg │ └── upsampling-headroom.svg ├── filters │ └── responses │ │ ├── Makefile │ │ ├── README.md │ │ ├── index.html │ │ ├── main.cpp │ │ ├── wasm-api.h │ │ └── wasm-api.js └── fractional-delay.html ├── tests ├── common.h ├── common │ └── version.cpp ├── curves │ ├── 00-linear.cpp │ ├── 01-cubic-segments.cpp │ ├── 02-reciprocal.cpp │ └── 03-reciprocal-bark.cpp ├── delay │ ├── 00-buffer.cpp │ ├── 01-delay.cpp │ ├── 02-multi-buffer.cpp │ ├── 03-multi-delay.cpp │ ├── fractional-delay.py │ └── test-delay-stats.h ├── envelopes │ ├── 00-cubic-lfo.cpp │ ├── 01-box-sum-average.cpp │ ├── 02-box-stack.cpp │ ├── 03-peak-hold.cpp │ ├── 04-peak-decay.cpp │ ├── box-stack.py │ ├── cubic-lfo.py │ ├── peak-hold.py │ └── plain-plots.py ├── fft │ ├── 00-fft.cpp │ ├── 00-fft.py │ └── fft-errors-numpy.py ├── filters │ ├── 00-highpass-lowpass.cpp │ ├── 01-bandpass.cpp │ ├── 02-responses.cpp │ ├── 03-gain.cpp │ ├── 04-allpass.cpp │ ├── 05-shelves.cpp │ ├── 06-spec-equivalence.cpp │ ├── 07-accuracy.cpp │ ├── filter-tests.h │ └── plots.cpp ├── mix │ ├── cheap-fade.cpp │ ├── mixing-matrix.cpp │ └── stereo-multi.cpp ├── perf │ ├── 00-utils.cpp │ ├── 01-lagrange-methods.cpp │ └── perf-lagrange.py ├── rates │ ├── 00-kaiser-sinc.cpp │ └── 01-oversampler-2x-fir.cpp ├── spectral │ ├── 01-windowed-fft.cpp │ ├── 02-stft.cpp │ └── 03-stft-processor.cpp └── windows │ ├── 00-kaiser.cpp │ ├── 01-approx-confined-gaussian.cpp │ ├── kaiser-plots.py │ └── window-stats.h ├── util ├── article.py ├── console-colours.h ├── csv-writer.h ├── heatmap.h ├── plot.h ├── simple-args.h ├── stopwatch.h ├── test │ ├── benchmarks.h │ ├── main.cpp │ └── tests.h └── wav.h └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/*.pyc 3 | out/ 4 | html/ 5 | v*/ 6 | 7 | # Personal dev setup 8 | util/article 9 | tests/plot.h 10 | Doxyfile-local 11 | version-notes.txt 12 | -------------------------------------------------------------------------------- /Doxyfile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = "Signalsmith Audio's DSP Library" 2 | PROJECT_NUMBER = 1.6.2 3 | PROJECT_BRIEF = Useful C++ classes/templates for audio effects 4 | 5 | OUTPUT_DIRECTORY = . 6 | 7 | INPUT = dsp/ 8 | USE_MDFILE_AS_MAINPAGE = dsp/README.md 9 | 10 | IMAGE_PATH = manual/diagrams out/analysis -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2022 Geraint Luff / Signalsmith Audio Ltd. 2 | 3 | Unless released elsewhere under another license, or a license/description is included in the file itself, the contents of this directory is licensed for use and redistribution (including modified versions) solely for developing, testing or documenting the Signalsmith Audio DSP Library. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | .SILENT: test out/test dev-setup 4 | .PHONY: clean 5 | 6 | clean: 7 | @# Tests and analysis 8 | rm -rf out 9 | @# Doxygen 10 | rm -rf html 11 | 12 | CPP_BASE := . 13 | ALL_H := Makefile $(shell find $(CPP_BASE) -iname \*.h) 14 | GCC := g++ -std=c++11 -g -O3 -ffast-math -fno-rtti \ 15 | -Wall -Wextra -Wfatal-errors -Wpedantic -pedantic-errors \ 16 | -I "util" -I dsp/ 17 | 18 | # Compile C++ object files 19 | # Everything depends on the .h files in the parent directory 20 | out/%.cpp.o: %.cpp $(ALL_H) 21 | @echo "$$(tput setaf 3)$<$$(tput sgr0) -> $$(tput setaf 1)$@$$(tput sgr0)" 22 | @mkdir -p $$(dirname "$@") 23 | @$(GCC) -c $< -o $@ 24 | 25 | ############## Testing ############## 26 | # 27 | # make test 28 | # builds all tests (.cpp files) in tests/, excluding directories starting with "_" 29 | # 30 | # make test-foo 31 | # builds all tests in tests/foo/, excluding directories starting with "_" 32 | # 33 | # make benchmark-foo 34 | # builds all tests in benchmarks/foo/, excluding directories starting with "_" 35 | # 36 | # For any of these, you can specify SEED=??? in the environment to reproduce randomised tests 37 | 38 | # Used for plots and stuff 39 | export PYTHONPATH=$(shell cd util && pwd) 40 | 41 | TEST_CPP_FILES := $(shell find tests -iname "*.cpp" -not -path "*/_*/*" | sort) 42 | TEST_CPP_O_FILES := $(patsubst %, out/%.o, $(TEST_CPP_FILES)) 43 | test: out/test 44 | mkdir -p out/analysis 45 | cd out/analysis && ../test 46 | cd out/analysis && find ../../tests -iname \*.py -print0 | xargs -0 -n1 python 47 | 48 | out/test: out/util/test/main.cpp.o $(TEST_CPP_O_FILES) 49 | @TEST_OPP_FILES=$$(find out/tests -iname "*.cpp.o" | sort) ;\ 50 | echo "Linking tests:" ;\ 51 | echo "$${TEST_OPP_FILES}" | sed 's/^/ /' ;\ 52 | $(GCC) out/util/test/main.cpp.o $${TEST_OPP_FILES} -o out/test 53 | 54 | windows: 55 | mkdir -p out/windows 56 | # /O2 for fast code, /Od to disable 57 | @echo "/Od /Iutil /Idsp /EHsc /Fo\"out/windows/\" /Fe\"out/test.exe\" $(TEST_CPP_FILES)" > tmp.txt 58 | 59 | ## Individual tests 60 | 61 | test-% : out/test-% 62 | mkdir -p out/analysis 63 | cd out/analysis && ../test-$* 64 | cd out/analysis && find ../../tests/$* -iname \*.py -print0 | xargs -0 -n1 python 65 | 66 | out/test-%: out/util/test/main.cpp.o 67 | @# A slight hack: we find the .cpp files, get a list of .o files, and call "make" again 68 | @TEST_OPP_FILES=$$(find "tests/$*" -iname "*.cpp" | sort | sed "s/\(.*\)/out\/\1.o/") ;\ 69 | $(MAKE) $$TEST_OPP_FILES ;\ 70 | echo "Linking tests:" ;\ 71 | echo "$${TEST_OPP_FILES}" | sed 's/^/ /' ;\ 72 | $(GCC) out/util/test/main.cpp.o $${TEST_OPP_FILES} -o out/test-$* 73 | 74 | ## Benchmarks 75 | 76 | benchmark-% : out/benchmark-% 77 | mkdir -p out/benchmarks 78 | cd out/benchmarks && ../benchmark-$* 79 | cd out/benchmarks && find ../../benchmarks/$* -iname \*.py -print0 | xargs -0 -n1 python 80 | 81 | benchmarkpy-%: 82 | cd out/benchmarks && find ../../benchmarks/$* -iname \*.py -print0 | xargs -0 -n1 python 83 | 84 | 85 | out/benchmark-%: out/util/test/main.opp 86 | @TEST_OPP_FILES=$$(find "benchmarks/$*" -iname "*.cpp" | sort | sed "s/\(.*\)/out\/\1.o/") ;\ 87 | make $$TEST_OPP_FILES ;\ 88 | echo "Linking tests:" ;\ 89 | echo "$${TEST_OPP_FILES}" | sed 's/^/ /' ;\ 90 | $(GCC) out/util/test/main.cpp.o $${TEST_OPP_FILES} -o out/benchmark-$* 91 | 92 | ############## Docs and releases ############## 93 | 94 | # These rely on specific things in my dev setup, but you probably don't need to run them yourself 95 | 96 | dev-setup: 97 | #echo "Copying Git hooks (.githooks)" 98 | #cp .githooks/* .git/hooks 99 | 100 | echo "Adding \"git graph\" and \"git graph-all\" 101 | git config alias.graph "log --oneline --graph" 102 | git config alias.graph-all "log --graph --oneline --all" 103 | cd .. && git config alias.graph "log --oneline --graph" 104 | cd .. && git config alias.graph-all "log --graph --oneline --all" 105 | 106 | historical-docs: 107 | ./historical-docs.sh 108 | 109 | release: historical-docs publish publish-git 110 | 111 | # bump-patch, bump-minor, bump-major 112 | bump-%: clean all doxygen 113 | @VERSION=$$(python version.py bump-$*) ; \ 114 | git commit -a -m "Release v$$VERSION" -e && \ 115 | git tag "dev-v$$VERSION" && \ 116 | ./git-sub-branch dsp main && \ 117 | git tag "v$$VERSION" main && \ 118 | cp -r html v$$VERSION 119 | 120 | release-%: bump-% release 121 | 122 | doxygen: 123 | doxygen Doxyfile-local 124 | cp html/topics.html html/modules.html || echo "Old Doxygen version" 125 | 126 | publish: 127 | find out -iname \*.csv -exec rm {} \; 128 | find out -iname \*.o -exec rm {} \; 129 | publish-signalsmith-audio /code/dsp 130 | cd util && python article 131 | 132 | publish-git: 133 | # Self-hosted 134 | git checkout main && publish-signalsmith-git /code/dsp.git 135 | git checkout dev && publish-signalsmith-git /code/dsp-doc.git ../dsp/ 136 | # GitHub 137 | git push github 138 | git push github main:main 139 | git push --tags github 140 | git push release main:main 141 | git tag | grep "^v[0-9]*\.[0-9]*\.[0-9]*$$" | xargs git push release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DSP Library: Documentation and Tests 2 | 3 | This provides tests and doc-generating scripts for Signalsmith Audio's DSP Library. 4 | 5 | This should be placed as a sub-directory of the main DSP library (e.g. `doc/`). 6 | 7 | ## Tests 8 | 9 | Tests are in `tests/`, and (where possible) they test the actual audio characteristics of the tools (e.g. frequency responses and aliasing levels). 10 | 11 | ``` 12 | make tests 13 | ``` 14 | 15 | You can compile/run just specific groups of tests, based on the subfolders in `tests/`, e.g.: 16 | 17 | ``` 18 | make test-delay 19 | ``` 20 | 21 | The tests are defined with some [simple macros](util/test/tests.h). Some tests use random numbers, but you can reproduce a run by setting `SEED=` when running. 22 | 23 | ## Plots 24 | 25 | Some of the tests write results to CSV files (as well as verifying them). Any Python files in the `tests/` (sub-)directory are run if the tests succeed, and these generally plot the CSV results into graphs. 26 | 27 | Both the tests and the Python scripts are run from the `out/analysis/` directory. 28 | 29 | You'll need SciPy/NumPy and Matplotlib. These Python plots are being gradually replaced by [my C++ plotting library](https://signalsmith-audio.co.uk/code/plot/). 30 | 31 | There are some supplemental animations, which (if you have `ffmpeg` installed) you can generate with: 32 | 33 | ``` 34 | make analysis-animations 35 | ``` 36 | -------------------------------------------------------------------------------- /benchmarks/envelopes-peak-hold/_others/kvr-vadim-zavalishin.h: -------------------------------------------------------------------------------- 1 | // Copyright Vadim Zavalishin, year unknown 2 | // Posted on KVR: https://www.kvraudio.com/forum/viewtopic.php?p=7523506#p7523506 3 | 4 | #include 5 | #include 6 | 7 | /* 8 | 9 | writepos scan scanpos scanend outpos outrange scanrange inrange 10 | 0 -- 8 5 1 1..4 4..8 < 0 11 | 1 8 7 7 5 2 2..4 5..8 0..1 12 | 2 7 6 6 5 3 3..4 5..8 0..2 13 | 3 6 5 5 5 4 4 5..8 0..3 14 | 4 -- 4 0 5 5..8 0..3 < 4 15 | 5 4 3 3 0 6 6..8 0..3 4..5 16 | 6 3 2 2 0 7 7..8 0..3 4..6 17 | 7 2 1 1 0 8 8 0..3 4..7 18 | 8 1 0 0 0 0 0..4 0..3 4..8 19 | 20 | writepos scan scanpos scanend outpos outrange scanrange inrange 21 | 0 -- 7 4 1 1..3 4..7 < 0 22 | 1 7 6 6 4 2 2..3 4..7 0..1 23 | 2 6 5 5 4 3 3 4..7 0..2 24 | 3 5 4 4 4 4 4..7 4..7 0..3 25 | 4 -- 3 0 5 5..7 0..3 < 4 26 | 5 3 2 2 0 6 6..7 0..3 4..5 27 | 6 2 1 1 0 7 7 0..3 4..6 28 | 7 1 0 0 0 0 0..3 0..3 4..7 29 | 30 | */ 31 | 32 | template 33 | class MovingMax 34 | { 35 | public: 36 | enum { N = 8 }; 37 | private: 38 | enum { 39 | SCANSTARTLOW = (N-1)/2, 40 | SCANSTARTHIGH = N-1, 41 | SCANSTARTXOR = SCANSTARTLOW ^ SCANSTARTHIGH 42 | }; 43 | public: 44 | MovingMax() 45 | { 46 | m_scanmax = m_inmax = std::numeric_limits::lowest(); 47 | m_data.fill(m_inmax); 48 | m_writepos = 0; 49 | m_scanend = 0; 50 | m_scanpos = 0; 51 | m_scanstart = SCANSTARTLOW; 52 | } 53 | T operator<<(T x) 54 | { 55 | if( --m_scanpos >= m_scanend ) 56 | { 57 | m_inmax = std::max(m_inmax,x); 58 | m_data[m_scanpos] = std::max( m_data[m_scanpos], m_data[m_scanpos+1] ); 59 | } 60 | else 61 | { 62 | m_scanmax = m_inmax; 63 | m_inmax = x; 64 | m_scanend = m_scanend ^ ((N+1)/2); 65 | m_scanstart = m_scanstart ^ SCANSTARTXOR; 66 | m_scanpos = m_scanstart; 67 | } 68 | 69 | m_data[m_writepos] = x; 70 | if( ++m_writepos >= N ) 71 | m_writepos = 0; 72 | T outmax = m_data[m_writepos]; 73 | T movingmax = std::max( m_inmax, std::max(m_scanmax,outmax) ); 74 | return movingmax; 75 | } 76 | private: 77 | int m_writepos; 78 | int m_scanpos; 79 | int m_scanend; 80 | int m_scanstart; 81 | T m_inmax; 82 | T m_scanmax; 83 | std::array m_data; 84 | }; 85 | 86 | /*----------------------------------------*/ 87 | 88 | /// Signalsmith: adapted the above code to give it dynamic size for benchmarking 89 | 90 | template 91 | class MovingMaxDynamic { 92 | private: 93 | int N; 94 | int SCANSTARTLOW, SCANSTARTHIGH, SCANSTARTXOR; 95 | public: 96 | MovingMaxDynamic(int N) : N(N), SCANSTARTLOW((N-1)/2), SCANSTARTHIGH(N-1), SCANSTARTXOR(SCANSTARTLOW ^ SCANSTARTHIGH), m_data(N) 97 | { 98 | m_scanmax = m_inmax = std::numeric_limits::lowest(); 99 | m_data.assign(N, m_inmax); 100 | m_writepos = 0; 101 | m_scanend = 0; 102 | m_scanpos = 0; 103 | m_scanstart = SCANSTARTLOW; 104 | } 105 | T operator<<(T x) 106 | { 107 | if( --m_scanpos >= m_scanend ) 108 | { 109 | m_inmax = std::max(m_inmax,x); 110 | m_data[m_scanpos] = std::max( m_data[m_scanpos], m_data[m_scanpos+1] ); 111 | } 112 | else 113 | { 114 | m_scanmax = m_inmax; 115 | m_inmax = x; 116 | m_scanend = m_scanend ^ ((N+1)/2); 117 | m_scanstart = m_scanstart ^ SCANSTARTXOR; 118 | m_scanpos = m_scanstart; 119 | } 120 | 121 | m_data[m_writepos] = x; 122 | if( ++m_writepos >= N ) 123 | m_writepos = 0; 124 | T outmax = m_data[m_writepos]; 125 | T movingmax = std::max( m_inmax, std::max(m_scanmax,outmax) ); 126 | return movingmax; 127 | } 128 | private: 129 | int m_writepos; 130 | int m_scanpos; 131 | int m_scanend; 132 | int m_scanstart; 133 | T m_inmax; 134 | T m_scanmax; 135 | std::vector m_data; 136 | }; 137 | -------------------------------------------------------------------------------- /benchmarks/envelopes-peak-hold/_previous/signalsmith-constant-v1.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "delay.h" 6 | 7 | namespace signalsmith_constant_v1 { 8 | 9 | /** Peak-hold filter. 10 | \diagram{peak-hold.svg} 11 | 12 | The size is variable, and can be changed instantly with `.set()`, or by using `.push()`/`.pop()` in an unbalanced way. 13 | 14 | To avoid allocations while running, it uses a fixed-size array (not a `std::deque`) which determines the maximum length. 15 | */ 16 | template 17 | class PeakHold { 18 | static constexpr double lowest = std::numeric_limits::lowest(); 19 | signalsmith::delay::Buffer buffer; 20 | int backIndex = 0, middleStart = 0, workingIndex = 0, middleEnd = 0, frontIndex = 0; 21 | Sample frontMax = lowest, workingMax = lowest, middleMax = lowest; 22 | 23 | public: 24 | PeakHold(int maxLength) : buffer(maxLength) { 25 | backIndex = -maxLength; // use as starting length as well 26 | reset(); 27 | } 28 | int size() { 29 | return frontIndex - backIndex; 30 | } 31 | void resize(int maxLength) { 32 | buffer.resize(maxLength); 33 | frontIndex = 0; 34 | backIndex = -maxLength; 35 | reset(); 36 | } 37 | void reset(Sample fill=lowest) { 38 | int prevSize = size(); 39 | buffer.reset(fill); 40 | frontMax = workingMax = middleMax = lowest; 41 | middleEnd = workingIndex = frontIndex = 0; 42 | middleStart = middleEnd - (prevSize/2); 43 | backIndex = frontIndex - prevSize; 44 | if (middleStart == backIndex) ++middleStart; // size-0 case 45 | } 46 | void set(int newSize) { 47 | while (size() < newSize) { 48 | push(frontMax); 49 | } 50 | while (size() > newSize) { 51 | pop(); 52 | } 53 | } 54 | 55 | void push(Sample v) { 56 | buffer[frontIndex] = v; 57 | ++frontIndex; 58 | frontMax = std::max(frontMax, v); 59 | } 60 | void pop() { 61 | if (backIndex == middleStart) { 62 | // Move along the maximums 63 | workingMax = lowest; 64 | middleMax = frontMax; 65 | frontMax = lowest; 66 | 67 | int prevFrontLength = frontIndex - middleEnd; 68 | int prevMiddleLength = middleEnd - middleStart; 69 | if (prevFrontLength <= prevMiddleLength) { 70 | // Swap over simply 71 | middleStart = middleEnd; 72 | middleEnd = frontIndex; 73 | workingIndex = middleEnd; 74 | } else { 75 | // The front is longer than expected - this only happens when we're changing size 76 | // We don't move *all* of the front over, keeping half the surplus in the front 77 | int middleLength = (frontIndex - middleStart)/2; 78 | middleStart = middleEnd; 79 | if (middleStart == backIndex) ++middleStart; 80 | middleEnd += middleLength; 81 | // Since the front was not completely consumed, we re-calculate the front's maximum 82 | for (int i = middleEnd; i != frontIndex; ++i) { 83 | frontMax = std::max(frontMax, buffer[i]); 84 | } 85 | 86 | // Working index is close enough that it will be finished by the time the back is empty 87 | int backLength = middleStart - backIndex; 88 | int workingLength = std::min(backLength - 1, middleEnd - middleStart); 89 | workingIndex = middleStart + workingLength; 90 | // The index might not start at the end of the working block - compute the last bit immediately 91 | for (int i = middleEnd - 1; i != workingIndex - 1; --i) { 92 | buffer[i] = workingMax = std::max(workingMax, buffer[i]); 93 | } 94 | } 95 | 96 | } 97 | 98 | ++backIndex; 99 | if (workingIndex != middleStart) { 100 | --workingIndex; 101 | buffer[workingIndex] = workingMax = std::max(workingMax, buffer[workingIndex]); 102 | } 103 | } 104 | Sample read() { 105 | Sample backMax = buffer[backIndex]; 106 | return std::max(backMax, std::max(middleMax, frontMax)); 107 | } 108 | 109 | // For simple use as a constant-length filter 110 | Sample operator ()(Sample v) { 111 | push(v); 112 | pop(); 113 | return read(); 114 | } 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /benchmarks/envelopes-peak-hold/_previous/signalsmith-run-length-amortised.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace signalsmith_run_length_amortised { 6 | 7 | /** Peak-hold filter. 8 | \diagram{peak-hold.svg} 9 | 10 | The size is variable, and can be changed instantly with `.set()`, or by using `.push()`/`.pop()` in an unbalanced way. 11 | 12 | To avoid allocations while running, it uses a fixed-size array (not a `std::deque`) which determines the maximum length. 13 | */ 14 | template 15 | class PeakHold { 16 | int frontIndex = 0, backIndex = 0; 17 | int indexMask; 18 | struct Segment { 19 | Sample value; 20 | int length; 21 | }; 22 | std::vector segments; 23 | public: 24 | PeakHold(int maxLength) { 25 | resize(maxLength); 26 | } 27 | /// Sets the maximum guaranteed size. 28 | void resize(int maxLength) { 29 | int length = 1; 30 | while (length < maxLength) length *= 2; 31 | indexMask = length - 1; 32 | 33 | frontIndex = backIndex = 0; 34 | // Sets the size 35 | segments.assign(length, Segment{0, maxLength}); 36 | } 37 | /// Calculates the current size 38 | int size() const { 39 | int result = 0; 40 | for (int i = frontIndex; i <= backIndex; ++i) { 41 | result += segments[i&indexMask].length; 42 | } 43 | return result; 44 | } 45 | /// Resets the filter, preserving the size 46 | void reset(Sample fill=Sample()) { 47 | int s = size(); 48 | frontIndex = backIndex = 0; 49 | segments[0] = {fill, s}; 50 | } 51 | 52 | /// Sets a new size, extending the oldest value if needed 53 | void set(int newSize) { 54 | int oldSize = size(); 55 | if (newSize > oldSize) { 56 | auto &front = segments[frontIndex&indexMask]; 57 | front.length += newSize - oldSize; 58 | } else { 59 | for (int i = 0; i < oldSize - newSize; ++i) { 60 | pop(); 61 | } 62 | } 63 | } 64 | /// Adds a new value and drops an old one. 65 | Sample operator()(Sample v) { 66 | pop(); 67 | push(v); 68 | return read(); 69 | } 70 | 71 | /// Drops the oldest value from the peak tracker. 72 | void pop() { 73 | auto &front = segments[frontIndex&indexMask]; 74 | if (--front.length == 0) { 75 | ++frontIndex; 76 | } 77 | } 78 | /// Adds a value to the peak tracker 79 | void push(Sample v) { 80 | int length = 1; 81 | while (backIndex >= frontIndex && segments[backIndex&indexMask].value <= v) { 82 | // Consume the segment 83 | length += segments[backIndex&indexMask].length; 84 | --backIndex; 85 | } 86 | ++backIndex; 87 | segments[backIndex&indexMask] = {v, length}; 88 | } 89 | /// Returns the current value 90 | Sample read() const { 91 | return segments[frontIndex&indexMask].value; 92 | } 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /benchmarks/envelopes-peak-hold/peak-hold.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "envelopes.h" 5 | #include "_previous/signalsmith-run-length-amortised.h" 6 | #include "_previous/signalsmith-constant-v1.h" 7 | 8 | #include "_others/kvr-vadim-zavalishin.h" 9 | 10 | template 11 | struct PeakHoldImpl { 12 | int inputSize; 13 | std::vector input, output; 14 | PeakHold peakHold; 15 | PeakHoldImpl(int size) : inputSize(size*1000), input(size*1000), output(size*1000), peakHold(size) {} 16 | 17 | inline void run() { 18 | for (int i = 0; i < inputSize; ++i) { 19 | output[i] = peakHold(input[i]); 20 | } 21 | } 22 | }; 23 | 24 | template 25 | struct PeakHoldKvrVadimDynamic { 26 | int inputSize; 27 | std::vector input, output; 28 | MovingMaxDynamic movingMax; 29 | PeakHoldKvrVadimDynamic(int size) : inputSize(size*1000), input(size*1000), output(size*1000), movingMax(size) {} 30 | 31 | inline void run() { 32 | for (int i = 0; i < inputSize; ++i) { 33 | output[i] = (movingMax << input[i]); 34 | } 35 | } 36 | }; 37 | 38 | template 39 | void benchmarkPeakHold(Test &test, std::string name) { 40 | Benchmark benchmark(name, "size"); 41 | 42 | benchmark.add>>("current"); 43 | benchmark.add>>("run-length"); 44 | benchmark.add>>("constant-v1"); 45 | benchmark.add>("Vadim"); 46 | 47 | for (int n = 1; n <= 65536; n *= 2) { 48 | int n2 = (int(n*0.853253)/2)*2 + 1; 49 | if (n >= 4) { 50 | test.log("N = ", n2); 51 | benchmark.run(n2, n2); 52 | } 53 | 54 | test.log("N = ", n); 55 | benchmark.run(n, n); 56 | } 57 | } 58 | 59 | TEST("Peak hold", peak_hold) { 60 | benchmarkPeakHold(test, "envelopes_peak_hold_double"); 61 | benchmarkPeakHold(test, "envelopes_peak_hold_float"); 62 | benchmarkPeakHold(test, "envelopes_peak_hold_int"); 63 | } 64 | -------------------------------------------------------------------------------- /benchmarks/envelopes-peak-hold/plots.py: -------------------------------------------------------------------------------- 1 | import article 2 | 3 | def plainPlot(name, legend_loc="best"): 4 | columns, data = article.readCsv("%s.csv"%name) 5 | 6 | figure, axes = article.medium(); 7 | def display(x): 8 | if x == int(x): 9 | return str(int(x)) 10 | return str(x) 11 | xlabels = [display(x) for x in data[0]]; 12 | xticks = range(len(data[0])); 13 | 14 | divisor = int(len(xlabels)*0.2); 15 | for i in range(len(xlabels)): 16 | if i%divisor != 0: 17 | xlabels[i] = "" 18 | 19 | for i in range(1, len(columns)): 20 | axes.plot(xticks, 1/data[i], label=columns[i]); 21 | axes.set(ylabel="speed (higher is better)", xticks=xticks, xticklabels=xlabels); 22 | figure.save("%s.svg"%name, legend_loc=legend_loc) 23 | 24 | plainPlot("envelopes_peak_hold_double") 25 | plainPlot("envelopes_peak_hold_float") 26 | plainPlot("envelopes_peak_hold_int") 27 | -------------------------------------------------------------------------------- /benchmarks/fft/complex.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "fft.h" 5 | #include "_previous/signalsmith-fft-v1.h" 6 | 7 | template 8 | void benchmarkComplex(std::string name) { 9 | Benchmark benchmark(name, "size"); 10 | 11 | struct CreateVectors { 12 | std::vector> input, output; 13 | CreateVectors(int size) : input(size), output(size) {} 14 | }; 15 | struct Current : CreateVectors { 16 | signalsmith::fft::FFT fft; 17 | Current(int size) : CreateVectors(size), fft(size) {}; 18 | SIGNALSMITH_INLINE void run() { 19 | fft.fft(this->input, this->output); 20 | } 21 | }; 22 | benchmark.add("current"); 23 | 24 | struct SignalsmithV1 : CreateVectors { 25 | std::shared_ptr> fft; 26 | SignalsmithV1(int size) : CreateVectors(size), fft(signalsmith_v1::fft::getFft(size)) {}; 27 | SIGNALSMITH_INLINE void run() { 28 | fft->fft((Sample *)this->input.data(), (Sample *)this->output.data()); 29 | fft->permute((Sample *)this->input.data(), (Sample *)this->output.data()); 30 | } 31 | }; 32 | struct SignalsmithV1NoPermute : CreateVectors { 33 | std::shared_ptr> fft; 34 | SignalsmithV1NoPermute(int size) : CreateVectors(size), fft(signalsmith_v1::fft::getFft(size)) {}; 35 | SIGNALSMITH_INLINE void run() { 36 | fft->fft((Sample *)this->input.data(), (Sample *)this->output.data()); 37 | } 38 | }; 39 | benchmark.add("signalsmith-v1"); 40 | benchmark.add("signalsmith-v1-nopermute"); 41 | 42 | for (int n = 1; n <= 65536*16; n *= 2) { 43 | LOG_EXPR(n); 44 | benchmark.run(n, std::log2(n)*n + 1); 45 | } 46 | } 47 | 48 | TEST("Complex FFT", complex_fft) { 49 | benchmarkComplex("complex_fft_double"); 50 | } 51 | -------------------------------------------------------------------------------- /benchmarks/fft/plots.py: -------------------------------------------------------------------------------- 1 | import article 2 | 3 | def plainPlot(name): 4 | columns, data = article.readCsv("%s.csv"%name) 5 | 6 | figure, axes = article.medium(); 7 | def display(x): 8 | if x == int(x): 9 | return str(int(x)) 10 | return str(x) 11 | xlabels = [display(x) for x in data[0]]; 12 | xticks = range(len(data[0])); 13 | 14 | divisor = int(len(xlabels)*0.2); 15 | for i in range(len(xlabels)): 16 | if i%divisor != 0: 17 | xlabels[i] = "" 18 | 19 | for i in range(1, len(columns)): 20 | axes.plot(xticks, 1/data[i], label=columns[i]); 21 | axes.set(ylabel="speed (higher is better)", xticks=xticks, xticklabels=xlabels); 22 | figure.save("%s.svg"%name, legend_loc="upper center") 23 | 24 | plainPlot("complex_fft_double") 25 | -------------------------------------------------------------------------------- /dsp/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Geraint Luff / Signalsmith Audio Ltd. 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. -------------------------------------------------------------------------------- /dsp/README.md: -------------------------------------------------------------------------------- 1 | # Signalsmith Audio's DSP Library 2 | 3 | A C++11 header-only library, providing classes/templates for (mostly audio) signal-processing tasks. 4 | 5 | More detail is in the [main project page](https://signalsmith-audio.co.uk/code/dsp/), and the [Doxygen docs](https://signalsmith-audio.co.uk/code/dsp/html/modules.html). 6 | 7 | ## Basic use 8 | 9 | ``` 10 | git clone https://signalsmith-audio.co.uk/code/dsp.git 11 | ``` 12 | 13 | Just include the header file(s) you need, and start using classes: 14 | 15 | ```cpp 16 | #include "dsp/delay.h" 17 | 18 | using Delay = signalsmith::delay::Delay; 19 | Delay delayLine(1024); 20 | ``` 21 | 22 | You can add a compile-time version-check to make sure you have a compatible version of the library: 23 | ```cpp 24 | #include "dsp/envelopes.h" 25 | SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 2) 26 | ``` 27 | 28 | ### Development / contributing 29 | 30 | Tests (and source-scripts for the above docs) are available in a separate repo: 31 | 32 | ``` 33 | git clone https://signalsmith-audio.co.uk/code/dsp-doc.git 34 | ``` 35 | 36 | The goal (where possible) is to measure/test the actual audio characteristics of the tools (e.g. frequency responses and aliasing levels). 37 | 38 | ### License 39 | 40 | This code is [MIT licensed](LICENSE.txt). If you'd prefer something else, get in touch. 41 | -------------------------------------------------------------------------------- /dsp/common.h: -------------------------------------------------------------------------------- 1 | #ifndef SIGNALSMITH_DSP_COMMON_H 2 | #define SIGNALSMITH_DSP_COMMON_H 3 | 4 | #if defined(__FAST_MATH__) && (__apple_build_version__ >= 16000000) && (__apple_build_version__ <= 16000099) 5 | # error Apple Clang 16.0.0 generates incorrect SIMD for ARM. If you HAVE to use this version of Clang, turn off -ffast-math. 6 | #endif 7 | 8 | #ifndef M_PI 9 | #define M_PI 3.14159265358979323846264338327950288 10 | #endif 11 | 12 | namespace signalsmith { 13 | /** @defgroup Common Common 14 | @brief Definitions and helper classes used by the rest of the library 15 | 16 | @{ 17 | @file 18 | */ 19 | 20 | #define SIGNALSMITH_DSP_VERSION_MAJOR 1 21 | #define SIGNALSMITH_DSP_VERSION_MINOR 6 22 | #define SIGNALSMITH_DSP_VERSION_PATCH 2 23 | #define SIGNALSMITH_DSP_VERSION_STRING "1.6.2" 24 | 25 | /** Version compatability check. 26 | \code{.cpp} 27 | static_assert(signalsmith::version(1, 4, 1), "version check"); 28 | \endcode 29 | ... or use the equivalent `SIGNALSMITH_DSP_VERSION_CHECK`. 30 | Major versions are not compatible with each other. Minor and patch versions are backwards-compatible. 31 | */ 32 | constexpr bool versionCheck(int major, int minor, int patch=0) { 33 | return major == SIGNALSMITH_DSP_VERSION_MAJOR 34 | && (SIGNALSMITH_DSP_VERSION_MINOR > minor 35 | || (SIGNALSMITH_DSP_VERSION_MINOR == minor && SIGNALSMITH_DSP_VERSION_PATCH >= patch)); 36 | } 37 | 38 | /// Check the library version is compatible (semver). 39 | #define SIGNALSMITH_DSP_VERSION_CHECK(major, minor, patch) \ 40 | static_assert(::signalsmith::versionCheck(major, minor, patch), "signalsmith library version is " SIGNALSMITH_DSP_VERSION_STRING); 41 | 42 | /** @} */ 43 | } // signalsmith:: 44 | #else 45 | // If we've already included it, check it's the same version 46 | static_assert(SIGNALSMITH_DSP_VERSION_MAJOR == 1 && SIGNALSMITH_DSP_VERSION_MINOR == 6 && SIGNALSMITH_DSP_VERSION_PATCH == 2, "multiple versions of the Signalsmith DSP library"); 47 | #endif // include guard 48 | -------------------------------------------------------------------------------- /dsp/mix.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_MULTI_CHANNEL_H 4 | #define SIGNALSMITH_DSP_MULTI_CHANNEL_H 5 | 6 | #include 7 | 8 | namespace signalsmith { 9 | namespace mix { 10 | /** @defgroup Mix Multichannel mixing 11 | @brief Utilities for stereo/multichannel mixing operations 12 | 13 | @{ 14 | @file 15 | */ 16 | 17 | /** @defgroup Matrices Orthogonal matrices 18 | @brief Some common matrices used for audio 19 | @ingroup Mix 20 | @{ */ 21 | 22 | /// @brief Hadamard: high mixing levels, N log(N) operations 23 | template 24 | class Hadamard { 25 | public: 26 | static_assert(size >= 0, "Size must be positive (or -1 for dynamic)"); 27 | /// Applies the matrix, scaled so it's orthogonal 28 | template 29 | static void inPlace(Data &&data) { 30 | unscaledInPlace(data); 31 | 32 | Sample factor = scalingFactor(); 33 | for (int c = 0; c < size; ++c) { 34 | data[c] *= factor; 35 | } 36 | } 37 | 38 | /// Scaling factor applied to make it orthogonal 39 | static Sample scalingFactor() { 40 | /// TODO: test for C++20, or whatever makes this constexpr. Maybe a `#define` in `common.h`? 41 | return std::sqrt(Sample(1)/(size ? size : 1)); 42 | } 43 | 44 | /// Skips the scaling, so it's a matrix full of `1`s 45 | template 46 | static void unscaledInPlace(Data &&data) { 47 | if (size <= 1) return; 48 | constexpr int hSize = size/2; 49 | 50 | Hadamard::template unscaledInPlace(data); 51 | Hadamard::template unscaledInPlace(data); 52 | 53 | for (int i = 0; i < hSize; ++i) { 54 | Sample a = data[i + startIndex], b = data[i + startIndex + hSize]; 55 | data[i + startIndex] = (a + b); 56 | data[i + startIndex + hSize] = (a - b); 57 | } 58 | } 59 | }; 60 | /// @brief Hadamard with dynamic size 61 | template 62 | class Hadamard { 63 | int size; 64 | public: 65 | Hadamard(int size) : size(size) {} 66 | 67 | /// Applies the matrix, scaled so it's orthogonal 68 | template 69 | void inPlace(Data &&data) const { 70 | unscaledInPlace(data); 71 | 72 | Sample factor = scalingFactor(); 73 | for (int c = 0; c < size; ++c) { 74 | data[c] *= factor; 75 | } 76 | } 77 | 78 | /// Scaling factor applied to make it orthogonal 79 | Sample scalingFactor() const { 80 | return std::sqrt(Sample(1)/(size ? size : 1)); 81 | } 82 | 83 | /// Skips the scaling, so it's a matrix full of `1`s 84 | template 85 | void unscaledInPlace(Data &&data) const { 86 | int hSize = size/2; 87 | while (hSize > 0) { 88 | for (int startIndex = 0; startIndex < size; startIndex += hSize*2) { 89 | for (int i = startIndex; i < startIndex + hSize; ++i) { 90 | Sample a = data[i], b = data[i + hSize]; 91 | data[i] = (a + b); 92 | data[i + hSize] = (a - b); 93 | } 94 | } 95 | hSize /= 2; 96 | } 97 | } 98 | }; 99 | /// @brief Householder: moderate mixing, 2N operations 100 | template 101 | class Householder { 102 | public: 103 | static_assert(size >= 0, "Size must be positive (or -1 for dynamic)"); 104 | template 105 | static void inPlace(Data &&data) { 106 | if (size < 1) return; 107 | /// TODO: test for C++20, which makes `std::complex::operator/` constexpr 108 | const Sample factor = Sample(-2)/Sample(size ? size : 1); 109 | 110 | Sample sum = data[0]; 111 | for (int i = 1; i < size; ++i) { 112 | sum += data[i]; 113 | } 114 | sum *= factor; 115 | for (int i = 0; i < size; ++i) { 116 | data[i] += sum; 117 | } 118 | } 119 | /// @deprecated The matrix is already orthogonal, but this is here for compatibility with Hadamard 120 | constexpr static Sample scalingFactor() { 121 | return 1; 122 | } 123 | }; 124 | /// @brief Householder with dynamic size 125 | template 126 | class Householder { 127 | int size; 128 | public: 129 | Householder(int size) : size(size) {} 130 | 131 | template 132 | void inPlace(Data &&data) const { 133 | if (size < 1) return; 134 | const Sample factor = Sample(-2)/Sample(size ? size : 1); 135 | 136 | Sample sum = data[0]; 137 | for (int i = 1; i < size; ++i) { 138 | sum += data[i]; 139 | } 140 | sum *= factor; 141 | for (int i = 0; i < size; ++i) { 142 | data[i] += sum; 143 | } 144 | } 145 | /// @deprecated The matrix is already orthogonal, but this is here for compatibility with Hadamard 146 | constexpr static Sample scalingFactor() { 147 | return 1; 148 | } 149 | }; 150 | /// @} 151 | 152 | /** @brief Upmix/downmix a stereo signal to an (even) multi-channel signal 153 | 154 | When spreading out, it rotates the input by various amounts (e.g. a four-channel signal would produce `(left, right, mid side)`), such that energy is preserved for each pair. 155 | 156 | When mixing together, it uses the opposite rotations, such that upmix → downmix produces the same stereo signal (when scaled by `.scalingFactor1()`. 157 | */ 158 | template 159 | class StereoMultiMixer { 160 | static_assert((channels/2)*2 == channels, "StereoMultiMixer must have an even number of channels"); 161 | static_assert(channels > 0, "StereoMultiMixer must have a positive number of channels"); 162 | static constexpr int hChannels = channels/2; 163 | std::array coeffs; 164 | public: 165 | StereoMultiMixer() { 166 | coeffs[0] = 1; 167 | coeffs[1] = 0; 168 | for (int i = 1; i < hChannels; ++i) { 169 | double phase = M_PI*i/channels; 170 | coeffs[2*i] = std::cos(phase); 171 | coeffs[2*i + 1] = std::sin(phase); 172 | } 173 | } 174 | 175 | template 176 | void stereoToMulti(In &input, Out &output) const { 177 | output[0] = input[0]; 178 | output[1] = input[1]; 179 | for (int i = 2; i < channels; i += 2) { 180 | output[i] = input[0]*coeffs[i] + input[1]*coeffs[i + 1]; 181 | output[i + 1] = input[1]*coeffs[i] - input[0]*coeffs[i + 1]; 182 | } 183 | } 184 | template 185 | void multiToStereo(In &input, Out &output) const { 186 | output[0] = input[0]; 187 | output[1] = input[1]; 188 | for (int i = 2; i < channels; i += 2) { 189 | output[0] += input[i]*coeffs[i] - input[i + 1]*coeffs[i + 1]; 190 | output[1] += input[i + 1]*coeffs[i] + input[i]*coeffs[i + 1]; 191 | } 192 | } 193 | /// Scaling factor for the downmix, if channels are phase-aligned 194 | static constexpr Sample scalingFactor1() { 195 | return 2/Sample(channels); 196 | } 197 | /// Scaling factor for the downmix, if channels are independent 198 | static Sample scalingFactor2() { 199 | return std::sqrt(scalingFactor1()); 200 | } 201 | }; 202 | 203 | /// A cheap (polynomial) almost-energy-preserving crossfade 204 | /// Maximum energy error: 1.06%, average 0.64%, curves overshoot by 0.3% 205 | /// See: http://signalsmith-audio.co.uk/writing/2021/cheap-energy-crossfade/ 206 | template 207 | void cheapEnergyCrossfade(Sample x, Result &toCoeff, Result &fromCoeff) { 208 | Sample x2 = 1 - x; 209 | // Other powers p can be approximated by: k = -6.0026608 + p*(6.8773512 - 1.5838104*p) 210 | Sample A = x*x2, B = A*(1 + (Sample)1.4186*A); 211 | Sample C = (B + x), D = (B + x2); 212 | toCoeff = C*C; 213 | fromCoeff = D*D; 214 | } 215 | 216 | /** @} */ 217 | }} // signalsmith::delay:: 218 | #endif // include guard 219 | -------------------------------------------------------------------------------- /dsp/perf.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_PERF_H 4 | #define SIGNALSMITH_DSP_PERF_H 5 | 6 | #include 7 | 8 | #if defined(__SSE__) || defined(_M_X64) 9 | # include 10 | #else 11 | # include // for uintptr_t 12 | #endif 13 | 14 | namespace signalsmith { 15 | namespace perf { 16 | /** @defgroup Performance Performance helpers 17 | @brief Nothing serious, just some `#defines` and helpers 18 | 19 | @{ 20 | @file 21 | */ 22 | 23 | /// *Really* insist that a function/method is inlined (mostly for performance in DEBUG builds) 24 | #ifndef SIGNALSMITH_INLINE 25 | #ifdef __GNUC__ 26 | #define SIGNALSMITH_INLINE __attribute__((always_inline)) inline 27 | #elif defined(__MSVC__) 28 | #define SIGNALSMITH_INLINE __forceinline inline 29 | #else 30 | #define SIGNALSMITH_INLINE inline 31 | #endif 32 | #endif 33 | 34 | /** @brief Complex-multiplication (with optional conjugate second-arg), without handling NaN/Infinity 35 | The `std::complex` multiplication has edge-cases around NaNs which slow things down and prevent auto-vectorisation. Flags like `-ffast-math` sort this out anyway, but this helps with Debug builds. 36 | */ 37 | template 38 | SIGNALSMITH_INLINE static std::complex mul(const std::complex &a, const std::complex &b) { 39 | return conjugateSecond ? std::complex{ 40 | b.real()*a.real() + b.imag()*a.imag(), 41 | b.real()*a.imag() - b.imag()*a.real() 42 | } : std::complex{ 43 | a.real()*b.real() - a.imag()*b.imag(), 44 | a.real()*b.imag() + a.imag()*b.real() 45 | }; 46 | } 47 | 48 | #if defined(__SSE__) || defined(_M_X64) 49 | class StopDenormals { 50 | unsigned int controlStatusRegister; 51 | public: 52 | StopDenormals() : controlStatusRegister(_mm_getcsr()) { 53 | _mm_setcsr(controlStatusRegister|0x8040); // Flush-to-Zero and Denormals-Are-Zero 54 | } 55 | ~StopDenormals() { 56 | _mm_setcsr(controlStatusRegister); 57 | } 58 | }; 59 | #elif (defined (__ARM_NEON) || defined (__ARM_NEON__)) 60 | class StopDenormals { 61 | uintptr_t status; 62 | public: 63 | StopDenormals() { 64 | uintptr_t asmStatus; 65 | asm volatile("mrs %0, fpcr" : "=r"(asmStatus)); 66 | status = asmStatus = asmStatus|0x01000000U; // Flush to Zero 67 | asm volatile("msr fpcr, %0" : : "ri"(asmStatus)); 68 | } 69 | ~StopDenormals() { 70 | uintptr_t asmStatus = status; 71 | asm volatile("msr fpcr, %0" : : "ri"(asmStatus)); 72 | } 73 | }; 74 | #else 75 | # if __cplusplus >= 202302L 76 | # warning "The `StopDenormals` class doesn't do anything for this architecture" 77 | # endif 78 | class StopDenormals {}; // FIXME: add for other architectures 79 | #endif 80 | 81 | /** @} */ 82 | }} // signalsmith::perf:: 83 | 84 | #endif // include guard 85 | -------------------------------------------------------------------------------- /dsp/rates.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_RATES_H 4 | #define SIGNALSMITH_DSP_RATES_H 5 | 6 | #include "./windows.h" 7 | #include "./delay.h" 8 | 9 | namespace signalsmith { 10 | namespace rates { 11 | /** @defgroup Rates Multi-rate processing 12 | @brief Classes for oversampling/upsampling/downsampling etc. 13 | 14 | @{ 15 | @file 16 | */ 17 | 18 | /// @brief Fills a container with a Kaiser-windowed sinc for an FIR lowpass. 19 | /// \diagram{rates-kaiser-sinc.svg,33-point results for various pass/stop frequencies} 20 | template 21 | void fillKaiserSinc(Data &&data, int length, double passFreq, double stopFreq) { 22 | if (length <= 0) return; 23 | double kaiserBandwidth = (stopFreq - passFreq)*length; 24 | kaiserBandwidth += 1.25/kaiserBandwidth; // heuristic for transition band, see `InterpolatorKaiserSincN` 25 | auto kaiser = signalsmith::windows::Kaiser::withBandwidth(kaiserBandwidth); 26 | kaiser.fill(data, length); 27 | 28 | double centreIndex = (length - 1)*0.5; 29 | double sincScale = M_PI*(passFreq + stopFreq); 30 | double ampScale = (passFreq + stopFreq); 31 | for (int i = 0; i < length; ++i) { 32 | double x = (i - centreIndex), px = x*sincScale; 33 | double sinc = (std::abs(px) > 1e-6) ? std::sin(px)*ampScale/px : ampScale; 34 | data[i] *= sinc; 35 | } 36 | }; 37 | /// @brief If only the centre frequency is specified, a heuristic is used to balance ripples and transition width 38 | /// \diagram{rates-kaiser-sinc-heuristic.svg,The transition width is set to: 0.9/sqrt(length)} 39 | template 40 | void fillKaiserSinc(Data &&data, int length, double centreFreq) { 41 | double halfWidth = 0.45/std::sqrt(length); 42 | if (halfWidth > centreFreq) halfWidth = (halfWidth + centreFreq)*0.5; 43 | fillKaiserSinc(data, length, centreFreq - halfWidth, centreFreq + halfWidth); 44 | } 45 | 46 | /** 2x FIR oversampling for block-based processing. 47 | 48 | \diagram{rates-oversampler2xfir-responses-45.svg,Upsample response for various lengths} 49 | 50 | The oversampled signal is stored inside this object, with channels accessed via `oversampler[c]`. For example, you might do: 51 | \code{.cpp} 52 | // Upsample from multi-channel input (inputBuffers[c][i] is a sample) 53 | oversampler.up(inputBuffers, bufferLength) 54 | 55 | // Modify the contents at the higher rate 56 | for (int c = 0; c < 2; ++c) { 57 | float *channel = oversampler[c]; 58 | for (int i = 0; i < bufferLength*2; ++i) { 59 | channel[i] = std::abs(channel[i]); 60 | } 61 | } 62 | 63 | // Downsample into the multi-channel output 64 | oversampler.down(outputBuffers, bufferLength); 65 | \endcode 66 | 67 | The performance depends not just on the length, but also on where you end the passband, allowing a wider/narrower transition band. Frequencies above this limit (relative to the lower sample-rate) may alias when upsampling and downsampling. 68 | 69 | \diagram{rates-oversampler2xfir-lengths.svg,Resample error rates for different passband thresholds} 70 | 71 | Since both upsample and downsample are stateful, channels are meaningful. If your input channel-count doesn't match your output, you can size it to the larger of the two, and use `.upChannel()` and `.downChannel()` to only process the channels which exist.*/ 72 | template 73 | struct Oversampler2xFIR { 74 | Oversampler2xFIR() : Oversampler2xFIR(0, 0) {} 75 | Oversampler2xFIR(int channels, int maxBlock, int halfLatency=16, double passFreq=0.43) { 76 | resize(channels, maxBlock, halfLatency, passFreq); 77 | } 78 | 79 | void resize(int nChannels, int maxBlockLength) { 80 | resize(nChannels, maxBlockLength, oneWayLatency); 81 | } 82 | void resize(int nChannels, int maxBlockLength, int halfLatency, double passFreq=0.43) { 83 | oneWayLatency = halfLatency; 84 | kernelLength = oneWayLatency*2; 85 | channels = nChannels; 86 | halfSampleKernel.resize(kernelLength); 87 | fillKaiserSinc(halfSampleKernel, kernelLength, passFreq, 1 - passFreq); 88 | inputStride = kernelLength + maxBlockLength; 89 | inputBuffer.resize(channels*inputStride); 90 | stride = (maxBlockLength + kernelLength)*2; 91 | buffer.resize(stride*channels); 92 | } 93 | 94 | void reset() { 95 | inputBuffer.assign(inputBuffer.size(), 0); 96 | buffer.assign(buffer.size(), 0); 97 | } 98 | 99 | /// @brief Round-trip latency (or equivalently: upsample latency at the higher rate). 100 | /// This will be twice the value passed into the constructor or `.resize()`. 101 | int latency() const { 102 | return kernelLength; 103 | } 104 | 105 | /// Upsamples from a multi-channel input into the internal buffer 106 | template 107 | void up(Data &&data, int lowSamples) { 108 | for (int c = 0; c < channels; ++c) { 109 | upChannel(c, data[c], lowSamples); 110 | } 111 | } 112 | 113 | /// Upsamples a single-channel input into the internal buffer 114 | template 115 | void upChannel(int c, Data &&data, int lowSamples) { 116 | Sample *inputChannel = inputBuffer.data() + c*inputStride; 117 | for (int i = 0; i < lowSamples; ++i) { 118 | inputChannel[kernelLength + i] = data[i]; 119 | } 120 | Sample *output = (*this)[c]; 121 | for (int i = 0; i < lowSamples; ++i) { 122 | output[2*i] = inputChannel[i + oneWayLatency]; 123 | Sample *offsetInput = inputChannel + (i + 1); 124 | Sample sum = 0; 125 | for (int o = 0; o < kernelLength; ++o) { 126 | sum += offsetInput[o]*halfSampleKernel[o]; 127 | } 128 | output[2*i + 1] = sum; 129 | } 130 | // Copy the end of the buffer back to the beginning 131 | for (int i = 0; i < kernelLength; ++i) { 132 | inputChannel[i] = inputChannel[lowSamples + i]; 133 | } 134 | } 135 | 136 | /// Downsamples from the internal buffer to a multi-channel output 137 | template 138 | void down(Data &&data, int lowSamples) { 139 | for (int c = 0; c < channels; ++c) { 140 | downChannel(c, data[c], lowSamples); 141 | } 142 | } 143 | 144 | /// Downsamples a single channel from the internal buffer to a single-channel output 145 | template 146 | void downChannel(int c, Data &&data, int lowSamples) { 147 | Sample *input = buffer.data() + c*stride; // no offset for latency 148 | for (int i = 0; i < lowSamples; ++i) { 149 | Sample v1 = input[2*i + kernelLength]; 150 | Sample sum = 0; 151 | for (int o = 0; o < kernelLength; ++o) { 152 | Sample v2 = input[2*(i + o) + 1]; 153 | sum += v2*halfSampleKernel[o]; 154 | } 155 | Sample v2 = sum; 156 | Sample v = (v1 + v2)*Sample(0.5); 157 | data[i] = v; 158 | } 159 | // Copy the end of the buffer back to the beginning 160 | for (int i = 0; i < kernelLength*2; ++i) { 161 | input[i] = input[lowSamples*2 + i]; 162 | } 163 | } 164 | 165 | /// Gets the samples for a single (higher-rate) channel. The valid length depends how many input samples were passed into `.up()`/`.upChannel()`. 166 | Sample * operator[](int c) { 167 | return buffer.data() + kernelLength*2 + stride*c; 168 | } 169 | const Sample * operator[](int c) const { 170 | return buffer.data() + kernelLength*2 + stride*c; 171 | } 172 | 173 | private: 174 | int oneWayLatency, kernelLength; 175 | int channels; 176 | int stride, inputStride; 177 | std::vector inputBuffer; 178 | std::vector halfSampleKernel; 179 | std::vector buffer; 180 | }; 181 | 182 | /** @} */ 183 | }} // namespace 184 | #endif // include guard 185 | -------------------------------------------------------------------------------- /extra-style.js: -------------------------------------------------------------------------------- 1 | // We generated this text file from the Git tags 2 | fetch("../version-notes.txt").then(response => { 3 | if (!response.ok) return; 4 | return response.text().then(text => { 5 | let numElement = document.getElementById('projectnumber'); 6 | let current = numElement.textContent; 7 | numElement.innerHTML = ""; 8 | let select = document.createElement('select'); 9 | // The format is like `vA.B.C: ...` 10 | text.replace(/^v([0-9]+.[0-9]+.[0-9]):+/gm, (all, version) => { 11 | let option = document.createElement('option'); 12 | option.textContent = version; 13 | select.appendChild(option); 14 | }); 15 | select.value = current; 16 | select.onchange = () => { 17 | // The modules page for the other version is a reasonable default 18 | let fallbackHref = location.href.replace(/\/html\/.*/, "/v" + select.value + "/modules.html"); 19 | let parts = location.href.split(current); 20 | if (parts.length > 1) { 21 | fallbackHref = parts[0] + select.value + "/modules.html"; 22 | } 23 | // However, check (with a HEAD request) whether there's an exact equivalent to the current page 24 | let newHref = location.href.replace(current, select.value); 25 | if (newHref == location.href) newHref = newHref.replace("/html/", "/v" + select.value + "/"); 26 | fetch(newHref, {method: 'HEAD'}).then(response =>{ 27 | location.href = response.ok ? newHref : fallbackHref; 28 | }).catch(() => { 29 | location.href = fallbackHref; 30 | }); 31 | }; 32 | numElement.appendChild(select); 33 | }); 34 | }).then(console.log).catch(console.log); 35 | -------------------------------------------------------------------------------- /git-sub-branch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$1" -o -z "$2" ] 4 | then 5 | echo "Usage:\n\tgit sub-branch " 6 | exit 1; 7 | fi 8 | 9 | sumDir="." 10 | tree=`git cat-file commit HEAD | grep '^tree ' | awk '{print \$2}'` 11 | 12 | # Find the tree ID for the sub-directory 13 | for dir in `echo "$1" | tr "/" "\n"` 14 | do 15 | sumDir="$sumDir/$dir" 16 | if [ ! -z "$dir" ] 17 | then 18 | tree=`git cat-file -p "$tree" | grep "\t${dir}$" | awk '{print $3}'` 19 | if [ -z "$tree" ] 20 | then 21 | echo "Could not find sub-dir: ${sumDir}" 22 | exit 1 23 | fi 24 | fi 25 | done 26 | 27 | # Copy latest commit message 28 | git log -n 1 --format="%B" > .git/.SUB_BRANCH_MSG 29 | 30 | if (git rev-parse --verify "$2" &> /dev/null) 31 | then 32 | echo "\n\n# adding to branch: $2\n" >> .git/.SUB_BRANCH_MSG 33 | $EDITOR .git/.SUB_BRANCH_MSG 34 | commitMsg=`cat .git/.SUB_BRANCH_MSG | sed '/^#/d'` 35 | newCommit=`cat .git/.SUB_BRANCH_MSG | git commit-tree "${tree}" -p "$2" -m "$commitMsg"` 36 | else 37 | echo "\n\n# new branch: $2\n" >> .git/.SUB_BRANCH_MSG 38 | $EDITOR .git/.SUB_BRANCH_MSG 39 | commitMsg=`cat .git/.SUB_BRANCH_MSG | sed '/^#/d'` 40 | newCommit=`cat .git/.SUB_BRANCH_MSG | git commit-tree "${tree}" -m "$commitMsg"` 41 | fi 42 | git branch -f "$2" "$newCommit" 43 | -------------------------------------------------------------------------------- /historical-docs.sh: -------------------------------------------------------------------------------- 1 | printf "" > version-notes.txt 2 | 3 | current=$(git symbolic-ref --short HEAD) 4 | 5 | for tag in $(git tag -l "dev-v*" --sort=-version:refname) 6 | do 7 | tag=`echo "$tag" | sed -e "s/^dev-//"` 8 | printf "$tag: " >> version-notes.txt 9 | git log -n 1 --format="[%cs] %s" "$tag" -- >> version-notes.txt 10 | git log -n 1 --format="%b" "$tag" -- | sed 's/^/ /' >> version-notes.txt 11 | 12 | if [ -d "$tag" ] 13 | then 14 | echo "Already exists: $tag" 15 | else 16 | echo "Tag: $tag" 17 | git -c advice.detachedHead=false checkout "dev-$tag" 18 | 19 | make clean test doxygen 20 | 21 | mv html "$tag" 22 | fi 23 | done 24 | git checkout "$current" 25 | 26 | for tag in $(git tag -l "dev-v*" --sort=-version:refname) 27 | do 28 | tag=`echo "$tag" | sed -e "s/^dev-//"` 29 | cp extra-style.js "$tag/" 30 | done 31 | 32 | if make clean test doxygen 33 | then 34 | cp extra-style.js html/ 35 | else 36 | exit 1 37 | fi 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DSP Library - Signalsmith Audio 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 24 |
25 |
26 |
27 |

DSP Library

28 |
Signalsmith Audio
29 |
30 | 31 | 32 | 33 |

This is a set of C++11 header-only classes/templates to support certain DSP tasks (mostly audio).

34 |

It's still growing, but currently includes:

35 |
    36 |
  • Delay tools (circular buffers, single/multi-channel delay-lines)
  • 37 |
  • Interpolators (Lagrange, polyphase, Kaiser-sinc)
  • 38 |
  • Envelope tools (e.g. box-filter, peak-hold)
  • 39 |
  • FFT and spectral processing (including multi-channel STFT)
  • 40 |
41 | 42 |

How to use

43 |

Clone using Git:

44 |

45 | 				git clone https://signalsmith-audio.co.uk/code/dsp.git
46 | 			
There's also a GitHub mirror
47 |

Include the appropriate header file, and start using classes:

48 |
54 |

API docs

55 |

Check out the API documentation (Doxygen) for detailed information.

56 | 57 |

Testing

58 |

Tests (and scripts to plot graphs etc.) are in a separate repository, to keep the main repo neater:

59 |

60 | 				git clone https://signalsmith-audio.co.uk/code/dsp-doc.git
61 | 			
There's a GitHub mirror for this too
62 |

Goals

63 |

Where reasonable, the tests should measure actual output quality (not just basic correctness and smoke-tests).

64 |

For example, here's the aliasing/accuracy performance of a delay line with windowed-sinc interpolation:

65 |
66 |

The automated tests check this performance for various input bandwidths, using a table like this:

67 |

68 | 				// A table of acceptable limits for the 20-point windowed-sinc
69 | 				double bandwidth[] = {90, 80, 50, 25, 12.5};
70 | 				double aliasing[] = {-20.5, -61, -70.5, -76, -77.5};
71 | 				double ampLow[] = {-2, -0.02, -0.01, -0.01, -0.01};
72 | 				double ampHigh[] = {0.02, 0.02, 0.01, 0.01, 0.01};
73 | 				double delayError[] = {0.9, 0.03, 0.01, 0.01, 0.01};
74 | 			
Each column specifies performance for a particular input bandwidth - e.g. 25% would correspond to 4x oversampled input.
75 | 76 |

Running the tests

77 |

To run the tests:

78 |
make test
79 |

To run just the tests in tests/delay/ (or similar):

80 |
make test-delay
81 |

There are a few other targets (e.g. for plotting graphs) - check the Makefile if you're curious.

82 | 83 |

License

84 |

The main library is MIT licensed (see LICENSE.txt in the main repo), but the tests, docs and support scripts are licensed differently (just for developing/testing the library).

85 |

We're pretty flexible though - if you need something else, get in touch.

86 | 87 |
88 | 89 | 90 | -------------------------------------------------------------------------------- /manual/filters/responses/Makefile: -------------------------------------------------------------------------------- 1 | all: out/main.js 2 | 3 | .PHONY: all dev-setup emsdk-latest clean 4 | 5 | clean: 6 | rm -rf out 7 | 8 | CPP_BASE := . 9 | ALL_H := Makefile $(shell find $(CPP_BASE) -iname \*.h) 10 | 11 | EMSDK_DIR ?= $${HOME}/Development/emsdk 12 | emsdk: 13 | git clone https://github.com/emscripten-core/emsdk.git "$(EMSDK_DIR)" 14 | emsdk-latest: 15 | cd "$(EMSDK_DIR)" && git pull && ./emsdk install latest && ./emsdk activate latest 16 | emsdk-env: 17 | . "$(EMSDK_DIR)/emsdk_env.sh" &&\ 18 | python3 --version &&\ 19 | cmake --version 20 | 21 | EXPORT_NAME ?= Main 22 | out/%.js: %.cpp $(ALL_H) 23 | mkdir -p out 24 | . "$(EMSDK_DIR)/emsdk_env.sh" && emcc \ 25 | $*.cpp -o out/$*.js \ 26 | -I ../../../tests \ 27 | -I ../../.. \ 28 | -std=c++11 -O3 -ffast-math \ 29 | -Wall -Wextra -Wfatal-errors -Wpedantic -pedantic-errors \ 30 | -sSINGLE_FILE=1 -sMODULARIZE \ 31 | -sEXPORT_NAME=$(EXPORT_NAME) \ 32 | -sMEMORY_GROWTH_GEOMETRIC_STEP=0.5 -sALLOW_MEMORY_GROWTH=1 33 | 34 | out/main.html: out/main.js 35 | python3 combine.py index.html out/main.js > out/main.html -------------------------------------------------------------------------------- /manual/filters/responses/README.md: -------------------------------------------------------------------------------- 1 | # Emscripten build 2 | 3 | C++ should include [`wasm-api.h`](wasm-api.h). This provides some helper methods for passing back arrays and strings, by stashing them in a property on the module. 4 | 5 | This can be run from `WasmApi().run(Main, callback) in `wasm-api.js`, which wraps all the exposed functions so that array results and callbacks work. 6 | 7 | The `EXPORT_NAME` argument in the `Makefile` should match the `#define` in `main.cpp`. 8 | 9 | ### Setting up 10 | 11 | If you don't already have the Emscripten SDK installed: 12 | 13 | ``` 14 | EMSDK_DIR= make emsdk emsdk-latest 15 | ``` 16 | 17 | If you already have it, you can make `emsdk-latest` to update. Then get the environment set up with: 18 | 19 | ``` 20 | EMSDK_DIR= make emsdk-env 21 | ``` 22 | 23 | ### Building 24 | 25 | ``` 26 | EMSDK_DIR= make out/main.js 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /manual/filters/responses/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Interactive Filter Responses 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 26 | 27 | 28 | 29 | 38 | 39 | 40 | 41 | 44 | 45 |
shape 16 | 25 |
design 30 | 37 |
logarithmic 42 | 43 |
46 | 47 | 48 | 90 |
91 |
92 |
93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /manual/filters/responses/main.cpp: -------------------------------------------------------------------------------- 1 | #define EXPORT_NAME Main 2 | #include "wasm-api.h" 3 | 4 | #include "tests/plot.h" 5 | #include "dsp/filters.h" 6 | 7 | int main() { 8 | } 9 | 10 | #include 11 | #include 12 | 13 | extern "C" { 14 | EXPORT_API 15 | void filterGraph(double width, double height, int type, int intDesign, double freq, bool logFreq, double octaves, double db) { 16 | using Filter = signalsmith::filters::BiquadStatic; 17 | double idealFactor = 0.01; 18 | Filter filter, idealFilter; 19 | 20 | Figure figure; 21 | auto &plot = figure.plot(width, height); 22 | plot.x.label("frequency"); 23 | if (logFreq) { 24 | double low = 0.001; 25 | plot.x.range(std::log, low, 0.5).minors(0.001, 0.01, 0.1, 0.5); 26 | } else { 27 | plot.x.linear(0, 0.5).major(0).minors(0.25, 0.5); 28 | } 29 | plot.y.linear(-60, 24).label("dB").major(0).minors(24, 12, -12, -24, -36, -48, -60); 30 | 31 | auto addLine = [&](int intDesign, bool addIdeal) { 32 | signalsmith::filters::BiquadDesign design = (signalsmith::filters::BiquadDesign)intDesign; 33 | auto bilinear = signalsmith::filters::BiquadDesign::bilinear; 34 | if (type == 0) { 35 | filter.lowpass(freq, octaves, design); 36 | idealFilter.lowpass(freq*idealFactor, octaves, bilinear); 37 | } else if (type == 1) { 38 | filter.highpass(freq, octaves, design); 39 | idealFilter.highpass(freq*idealFactor, octaves, bilinear); 40 | } else if (type == 2) { 41 | filter.bandpass(freq, octaves, design); 42 | idealFilter.bandpass(freq*idealFactor, octaves, bilinear); 43 | } else if (type == 3) { 44 | filter.notch(freq, octaves, design); 45 | idealFilter.notch(freq*idealFactor, octaves, bilinear); 46 | } else if (type == 4) { 47 | filter.peakDb(freq, db, octaves, design); 48 | idealFilter.peakDb(freq*idealFactor, db, octaves, bilinear); 49 | } else if (type == 5) { 50 | filter.highShelfDb(freq, db, octaves, design); 51 | idealFilter.highShelfDb(freq*idealFactor, db, octaves, bilinear); 52 | } else if (type == 6) { 53 | filter.lowShelfDb(freq, db, octaves, design); 54 | idealFilter.lowShelfDb(freq*idealFactor, db, octaves, bilinear); 55 | } else { 56 | } 57 | 58 | auto &line = plot.line(), &idealLine = addIdeal ? plot.line() : line; 59 | for (double f = logFreq ? 0.001 : 0; f < 0.5; f += (f > 0.1 ? 1e-4 : f > 0.01 ? 1e-5 : 1e-6)) { 60 | line.add(f, std::max(-300, filter.responseDb(f))); 61 | if (addIdeal) idealLine.add(f, std::max(-300, idealFilter.responseDb(f*idealFactor))); 62 | } 63 | line.marker(freq, std::max(-60, filter.responseDb(freq))); 64 | }; 65 | if (intDesign >= 0) { 66 | addLine(intDesign, true); 67 | } else { 68 | addLine(0, true); 69 | addLine(1, false); 70 | addLine(2, false); 71 | addLine(3, false); 72 | plot.legend(0, 0) 73 | .add(0, "bilinear") 74 | .add(2, "cookbook") 75 | .add(3, "oneSided") 76 | .add(4, "vicanek") 77 | .add(1, "ideal"); 78 | } 79 | 80 | std::stringstream stream; 81 | figure.write(stream); 82 | heapResultString(stream.str()); 83 | }} 84 | -------------------------------------------------------------------------------- /manual/filters/responses/wasm-api.h: -------------------------------------------------------------------------------- 1 | /** 2 | Usage: 3 | #define EXPORT_NAME Main 4 | #include "wasm-api.h" 5 | 6 | extern "C" { 7 | EXPORT_API 8 | void string(double value) { 9 | std::string foo = "foo:" + std::to_string(value); 10 | heapResultString(foo); 11 | } 12 | } 13 | 14 | Use `heapResultF64()` when the result will persist until at least the next C++ call. 15 | Use `heapCallbackF64()` when the result is short-lived (e.g. on the stack) and a callback must be used. 16 | * F64 -> double 17 | * F32 -> float 18 | * U32 -> uint32_t 19 | * U16 -> uint16_t 20 | * U8 -> uint8_t 21 | * 32 -> int32_t 22 | * 16 -> int16_t 23 | * 8 -> int8_t 24 | 25 | There's also `heapResultString(const char *, int ?length)` and `heapResultString(std::string)`. 26 | 27 | Your JavaScript can then call the C++ method (e.g. `Main._myFunc()`), and then call `heapResult()` to get the result, which will be a typed array (referencing live memory) or a string. 28 | 29 | If you run this with `WasmApi(Main).run(callback)` from "wasm-api.js", it will wrap your C++ function to handle callbacks or results. 30 | */ 31 | 32 | #ifndef EXPORT_NAME 33 | # error Must define EXPORT_NAME before including "wasm-api.h" 34 | #endif 35 | 36 | #include "emscripten.h" 37 | 38 | #define EXPORT_API EMSCRIPTEN_KEEPALIVE 39 | 40 | #include 41 | #define HEAP_RESULT_TYPED(Module, Type, size, name) \ 42 | EM_JS(void, heapResult##name, (const Type *result, int length), { \ 43 | Module.heapResult = Module.HEAP##name.subarray(result/size, result/size + length); \ 44 | }); \ 45 | EM_JS(void, heapCallback##name, (const Type *result, int length), { \ 46 | if (!Module.heapResultCallback) throw "No callback registered"; \ 47 | Module.heapResultCallback(Module.HEAP##name.subarray(result/size, result/size + length)); \ 48 | }); 49 | HEAP_RESULT_TYPED(EXPORT_NAME, int8_t, 1, 8); 50 | HEAP_RESULT_TYPED(EXPORT_NAME, int16_t, 2, 16); 51 | HEAP_RESULT_TYPED(EXPORT_NAME, int32_t, 4, 32); 52 | HEAP_RESULT_TYPED(EXPORT_NAME, uint8_t, 1, U8); 53 | HEAP_RESULT_TYPED(EXPORT_NAME, uint16_t, 2, U16); 54 | HEAP_RESULT_TYPED(EXPORT_NAME, uint32_t, 4, U32); 55 | HEAP_RESULT_TYPED(EXPORT_NAME, float, 4, F32); 56 | HEAP_RESULT_TYPED(EXPORT_NAME, double, 8, F64); 57 | // Unpack strings from `const char *` 58 | #include 59 | EM_JS(void, heapResultString, (const char *result, int length), { 60 | let str = ""; 61 | for (let i = result; i < result + length; ++i) { 62 | str += String.fromCharCode(Module.HEAP8[i]); 63 | } 64 | Module.heapResult = str; 65 | }); 66 | void heapResultString(const char *str) { 67 | heapResultString(str, strlen(str)); 68 | } 69 | void heapResultString(const std::string &str) { 70 | heapResultString(str.c_str(), str.size()); 71 | } 72 | -------------------------------------------------------------------------------- /manual/filters/responses/wasm-api.js: -------------------------------------------------------------------------------- 1 | /* 2 | Mostly wraps a module which uses `wasm-api.h`, so that array/string results get returned properly. 3 | 4 | It also provides a `.range()` method: 5 | WasmApi(Module).range({ 6 | key: 'key', label: 'Label' 7 | min: 0, max: 100, step: 1, 8 | initial: 75 9 | }); 10 | This inserts an HTML table (if one is not referenced in the optional second argument) with a slider. 11 | */ 12 | function WasmApi(Module, table) { 13 | if (!(this instanceof WasmApi)) return new WasmApi(Module, table); 14 | let script = document.currentScript; 15 | function createTable() { 16 | if (typeof table == 'string') table = document.querySelector(table); 17 | if (!table) { 18 | table = document.createElement('table'); 19 | script.parentNode.insertBefore(table, script); 20 | } 21 | this.table = table; 22 | } 23 | 24 | this.range = (obj) => { 25 | createTable(); 26 | let row = document.createElement('tr'); 27 | 28 | let label = document.createElement('th'); 29 | label.textContent = obj.label || obj.key; 30 | row.appendChild(label); 31 | 32 | let values = document.createElement('td'); 33 | let input = document.createElement('input'); 34 | input.type = 'range'; 35 | input.min = obj.min; 36 | input.max = obj.max; 37 | input.step = obj.step; 38 | input.value = obj.initial; 39 | input.setAttribute('value', obj.initial); 40 | input.name = obj.key; 41 | values.appendChild(input); 42 | 43 | let text = document.createElement('input'); 44 | text.type = 'number'; 45 | text.value = input.value; 46 | text.step = input.step; 47 | input.addEventListener('input', e => { 48 | text.value = input.value; 49 | }); 50 | input.addEventListener('dblclick', e => { 51 | text.value = input.value = obj.initial; 52 | }); 53 | text.addEventListener('change', e => { 54 | input.value = text.value; 55 | update(); 56 | }); 57 | values.appendChild(text); 58 | 59 | row.appendChild(values); 60 | table.appendChild(row); 61 | return this; 62 | }; 63 | 64 | this.module = null; 65 | let firstUpdate = true; 66 | this.run = (controlsChanged) => { 67 | return Module().then(module => { 68 | this.module = module; 69 | 70 | function wrap(fn) { 71 | return function wrappedResult() { 72 | let args = [].slice.call(arguments, 0); 73 | let callback = arguments[arguments.length - 1]; 74 | if (typeof callback == 'function') { 75 | // Callback 76 | module.heapResultCallback = (result) => { 77 | module.heapResultCallback = null; 78 | callback(result); 79 | }; 80 | fn.apply(null, args.slice(0, args.length - 1)); 81 | if (module.heapResultCallback) throw "Function did not trigger callback"; 82 | } else { 83 | // No callback 84 | let result = fn.apply(this, arguments); 85 | if (typeof result == 'undefined') { 86 | result = module.heapResult; 87 | module.heapResult = null; 88 | } 89 | return result; 90 | } 91 | } 92 | } 93 | let api = {}; 94 | for (let key in module) { 95 | if (/^_[^_]/.test(key)) { 96 | api[key.substring(1)] = wrap(module[key]); 97 | } 98 | } 99 | 100 | function update() { 101 | let obj = {}; 102 | if (table) table.querySelectorAll('input, select').forEach(input => { 103 | let name = input.name || input.id; 104 | if (!name) return; 105 | let value = input.value; 106 | if (input.type == 'range') value = parseFloat(value); 107 | if (input.type == 'checkbox') value = input.checked; 108 | obj[name] = value; 109 | }); 110 | controlsChanged(api, obj, firstUpdate); 111 | firstUpdate = false; 112 | } 113 | update(); 114 | if (table) table.querySelectorAll('input, select').forEach(input => { 115 | let initialValue = input.getAttribute("value"); 116 | input.oninput = input.onchange = update; 117 | input.ondblclick = () => { 118 | input.value = initialValue; 119 | update(); 120 | }; 121 | }); 122 | }); 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /manual/fractional-delay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Choosing a fractional delay 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

Choosing a fractional delay

14 |
Geraint Luff
15 | 16 |

There are various parameters or approaches to choose from when implementing a fractional-delay or interpolator, each with their own trade-offs. How can we decide?

17 |
18 | 19 | 20 | 21 |

The problem

22 |

There are probably a few things we want in a fractional-delay or interpolator:

23 |
    24 |
  • a flat response
  • 25 |
  • low aliasing (if we're modulating at all)
  • 26 |
  • low latency
  • 27 |
  • fast performance / low CPU
  • 28 |
29 |

Ideally, we'll have some hard requirements for some of these, and then we can choose how to trade off the rest.

30 | 31 |

Aliasing

32 |

Let's take a quick look at where aliasing comes from in a fractional delay. As the fractional delay varies between two integer samples, our response changes:

33 |
34 | 35 |
Varying responses for cubic Catmull-Rom interpolation. "Delay error" means the difference in group-delay compared to our target fractional delay.
36 |
37 |

While cubic interpolation isn't great, it illustrates the problem neatly: as the fractional delay time changes, parts of the spectrum end up being modulated, in both amplitude and phase.

38 | 39 |

This modulation spreads out these input frequencies across the output, resulting in aliasing. The speed of the modulation will affect where the aliasing ends up in the result, but we can determine the level just by looking at the modulation.

40 | 41 | 42 |

Frequency limits

43 |

Generally, our flat-response and low-aliasing requirements will only be relevant up to a certain frequency.

44 |

For example, if we're working with 2x oversampled audio, we might be able to assume there isn't much energy in the top octave of the signal:

45 |
46 |

Unfortunately, fractional delays always have worse performance at the very top end. So, even if we're not oversampled, we need to decide how much we're going to fuss about the top 5% of our bandwidth.

47 | 48 |

Polynomial interpolation

49 |

50 | 51 |

Catmull-Rom Cubic

52 |

This is pretty straightforward, and better than linear, but not great - you have to be dealing with 8x-oversampled signals before the aliasing gets below -60dB:

53 |
54 | 55 |
Aliasing/responses for Catmull-Rom cubic interpolation.
56 |
57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/common.h: -------------------------------------------------------------------------------- 1 | // Custom style if available 2 | #if defined(__has_include) && __has_include("plot-style.h") 3 | # include "plot-style.h" 4 | #else 5 | # include "plot.h" 6 | #endif 7 | 8 | using Figure = signalsmith::plot::Figure; 9 | using Plot2D = signalsmith::plot::Plot2D; 10 | 11 | #include "../util/csv-writer.h" 12 | 13 | -------------------------------------------------------------------------------- /tests/common/version.cpp: -------------------------------------------------------------------------------- 1 | #include "delay.h" // Anything that includes "common.h" should do 2 | 3 | SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 2) 4 | 5 | // Test with some compatible earlier versions 6 | SIGNALSMITH_DSP_VERSION_CHECK( 7 | SIGNALSMITH_DSP_VERSION_MAJOR, 8 | SIGNALSMITH_DSP_VERSION_MINOR, 9 | SIGNALSMITH_DSP_VERSION_PATCH 10 | ) 11 | SIGNALSMITH_DSP_VERSION_CHECK( 12 | SIGNALSMITH_DSP_VERSION_MAJOR, 13 | SIGNALSMITH_DSP_VERSION_MINOR, 14 | 0 15 | ) 16 | SIGNALSMITH_DSP_VERSION_CHECK( 17 | SIGNALSMITH_DSP_VERSION_MAJOR, 18 | 0, 19 | 0 20 | ) 21 | -------------------------------------------------------------------------------- /tests/curves/00-linear.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include "curves.h" 5 | 6 | #include 7 | 8 | TEST("Linear segments") { 9 | using Linear = signalsmith::curves::Linear; 10 | float accuracy = 1e-4; 11 | 12 | for (int repeat = 0; repeat < 100; ++repeat) { 13 | float x0 = test.random(-10, 10), x1 = test.random(-10, 10); 14 | float y0 = test.random(-10, 10), y1 = test.random(-10, 10); 15 | float accuracyX = accuracy/std::abs(x1 - x0); // gets less accurate when the input points are closer together 16 | float accuracyY = accuracy/std::abs(y1 - y0); 17 | 18 | const Linear linear{x0, x1, y0, y1}; 19 | 20 | TEST_ASSERT(std::abs(linear(x0) - y0) < accuracyX); 21 | TEST_ASSERT(std::abs(linear(x1) - y1) < accuracyX); 22 | 23 | double r = test.random(0, 1); 24 | double rx = x0 + (x1 - x0)*r; 25 | double ry = y0 + (y1 - y0)*r; 26 | TEST_ASSERT(std::abs(linear(rx) - ry) < accuracyX); 27 | 28 | Linear inverse = linear.inverse(); 29 | TEST_ASSERT(std::abs(inverse(y0) - x0) < accuracyY); 30 | TEST_ASSERT(std::abs(inverse(y1) - x1) < accuracyY); 31 | 32 | TEST_ASSERT(std::abs(inverse(ry) - rx) < accuracyY); 33 | 34 | double gradient = (y1 - y0)/(x1 - x0); 35 | TEST_APPROX(linear.dx(), gradient, accuracyX); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/curves/01-cubic-segments.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include "curves.h" 5 | 6 | #include 7 | #include 8 | 9 | TEST("Cubic segments (example)") { 10 | using Curve = signalsmith::curves::CubicSegmentCurve; 11 | struct Point{ 12 | float x, y; 13 | Point(float x, float y) : x(x), y(y) {} 14 | }; 15 | std::vector points; 16 | 17 | points.emplace_back(0, 0.5); 18 | points.emplace_back(1, 1); 19 | points.emplace_back(1.5, 3); 20 | points.emplace_back(2.5, 1.5); 21 | points.emplace_back(2.5, 1.5); 22 | points.emplace_back(3.5, 1.8); 23 | points.emplace_back(4, 3); 24 | points.emplace_back(4, 0.25); 25 | points.emplace_back(5, 2); 26 | points.emplace_back(6, 2.25); 27 | points.emplace_back(6.5, 1.5); 28 | 29 | Plot2D plot(380, 160); 30 | plot.x.blank(); 31 | plot.y.blank(); 32 | 33 | Curve curveSmooth, curveMonotonic, curveLinear; 34 | for (auto &p : points) { 35 | curveSmooth.add(p.x, p.y); 36 | curveMonotonic.add(p.x, p.y); 37 | curveLinear.add(p.x, p.y); 38 | curveLinear.add(p.x, p.y); 39 | } 40 | curveSmooth.update(); 41 | curveMonotonic.update(true); 42 | curveLinear.update(); 43 | 44 | auto &pointLine = plot.line(-1); 45 | plot.styleCounter = 0; 46 | auto &smoothLine = plot.line(), &monotonicLine = plot.line(), &linearLine = plot.line(); 47 | 48 | for (double x = -1; x < 7.5; x += 0.01) { 49 | smoothLine.add(x, curveSmooth(x)); 50 | monotonicLine.add(x, curveMonotonic(x)); 51 | linearLine.add(x, curveLinear(x)); 52 | } 53 | for (size_t i = 0; i < points.size(); ++i) { 54 | auto &p = points[i]; 55 | if (i < points.size() - 1 && p.x == points[i + 1].x && p.y == points[i + 1].y) { 56 | pointLine.marker(p.x, p.y, 1); 57 | ++i; 58 | } else { 59 | pointLine.marker(p.x, p.y, 0); 60 | } 61 | } 62 | 63 | plot.legend(-0.3, 0.8) 64 | .line(smoothLine, "smooth") 65 | .line(monotonicLine, "monotonic") 66 | .line(linearLine.styleIndex, "linear") 67 | .marker(pointLine.styleIndex.withMarker(0), "point") 68 | .marker(pointLine.styleIndex.withMarker(1), "repeated point"); 69 | plot.write("cubic-segments-example.svg"); 70 | 71 | return test.pass(); 72 | } 73 | TEST("Cubic segments (gradient)") { 74 | using Cubic = signalsmith::curves::Cubic; 75 | double x0 = test.random(-1, 1); 76 | 77 | Cubic s( 78 | x0, 79 | test.random(-1, 1), 80 | test.random(-1, 1), 81 | test.random(-1, 1), 82 | test.random(-1, 1) 83 | ); 84 | Cubic grad = s.dx(); 85 | 86 | for (int r = 0; r < 100; ++r) { 87 | double x = test.random(x0, x0 + 2); 88 | double v = s(x); 89 | double dx = 1e-10; 90 | double v2 = s(x + dx); 91 | double approxGrad = (v2 - v)/dx; 92 | double g = grad(x); 93 | 94 | // Just ballpark correct 95 | double diff = std::abs(approxGrad - g); 96 | TEST_ASSERT(diff < 1e-4); 97 | } 98 | } 99 | 100 | TEST("Cubic segments (hermite)") { 101 | using Cubic = signalsmith::curves::Cubic; 102 | 103 | for (int r = 0; r < 100; ++r) { 104 | double x0 = test.random(-1, 1); 105 | double x1 = x0 + test.random(0.01, 2); 106 | double y0 = test.random(-10, 10); 107 | double y1 = test.random(-10, 10); 108 | double g0 = test.random(-5, 5); 109 | double g1 = test.random(-5, 5); 110 | 111 | Cubic s = Cubic::hermite(x0, x1, y0, y1, g0, g1); 112 | Cubic grad = s.dx(); 113 | TEST_ASSERT(std::abs(s(x0) - y0) < 1e-6); 114 | TEST_ASSERT(std::abs(s(x1) - y1) < 1e-6); 115 | TEST_ASSERT(std::abs(grad(x0) - g0) < 1e-6); 116 | TEST_ASSERT(std::abs(grad(x1) - g1) < 1e-6); 117 | } 118 | } 119 | 120 | TEST("Cubic segments (known)") { 121 | using Cubic = signalsmith::curves::Cubic; 122 | 123 | { 124 | Cubic s = Cubic::smooth(0, 1, 2, 3, 0, 1, 2, 3); 125 | TEST_ASSERT(std::abs(s(0) - 0) < 1e-6); 126 | TEST_ASSERT(std::abs(s.dx()(0) - 1) < 1e-6); 127 | TEST_ASSERT(std::abs(s(1.5) - 1.5) < 1e-6); 128 | TEST_ASSERT(std::abs(s.dx()(1.5) - 1) < 1e-6); 129 | } 130 | { 131 | Cubic s = Cubic::smooth(0, 1, 2, 3, 0, 1, 0, 1); 132 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 133 | TEST_ASSERT(std::abs(s.dx()(1) - 0) < 1e-6); 134 | TEST_ASSERT(std::abs(s(2) - 0) < 1e-6); 135 | TEST_ASSERT(std::abs(s.dx()(2) - 0) < 1e-6); 136 | } 137 | 138 | { // monotonic 139 | Cubic s = Cubic::smooth(0, 1, 2, 3, -1, 1, 0, 2, true); 140 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 141 | TEST_ASSERT(std::abs(s.dx()(1) - 0) < 1e-6); 142 | TEST_ASSERT(std::abs(s(2) - 0) < 1e-6); 143 | TEST_ASSERT(std::abs(s.dx()(2) - 0) < 1e-6); 144 | } 145 | } 146 | 147 | TEST("Cubic segments (random)") { 148 | using Cubic = signalsmith::curves::Cubic; 149 | 150 | for (int r = 0; r < 1000; ++r) { 151 | bool monotonic = r%2; 152 | 153 | std::array x, y; 154 | x[0] = test.random(-1, 1); 155 | for (int i = 1; i < 5; ++i) x[i] = x[i - 1] + test.random(1e-10, 2); 156 | for (auto &v : y) v = test.random(-10, 10) - 7.5; 157 | 158 | Cubic sA = Cubic::smooth(x[0], x[1], x[2], x[3], y[0], y[1], y[2], y[3], monotonic); 159 | Cubic sB = Cubic::smooth(x[1], x[2], x[3], x[4], y[1], y[2], y[3], y[4], monotonic); 160 | 161 | // The points agree 162 | TEST_ASSERT(std::abs(sA(x[1]) - y[1]) < 1e-6); 163 | TEST_ASSERT(std::abs(sA(x[2]) - y[2]) < 1e-6); 164 | TEST_ASSERT(std::abs(sB(x[2]) - y[2]) < 1e-6); 165 | TEST_ASSERT(std::abs(sB(x[3]) - y[3]) < 1e-6); 166 | 167 | // Test smoothness - their gradients agree at x2 168 | TEST_ASSERT(std::abs(sA.dx()(x[2]) - sB.dx()(x[2])) < 1e-6); 169 | 170 | if (monotonic) { 171 | double dt = 1e-3; 172 | if (y[1] >= y[2]) { // decreasing 173 | double min = sA(x[1]); 174 | for (double t = x[1]; t <= x[2]; t += dt) { 175 | double y = sA(t); 176 | TEST_ASSERT(y <= min + 1e-6); 177 | min = std::min(y, min); 178 | } 179 | } 180 | if (y[1] <= y[2]) { // increasing 181 | double max = sA(x[1]); 182 | for (double t = x[1]; t <= x[2]; t += dt) { 183 | double y = sA(t); 184 | TEST_ASSERT(y >= max - 1e-6); 185 | max = std::max(y, max); 186 | } 187 | } 188 | } 189 | } 190 | } 191 | 192 | TEST("Cubic segments (duplicate points)") { 193 | using Cubic = signalsmith::curves::Cubic; 194 | 195 | { // Duplicate left point means it continues existing curve (straight here) 196 | Cubic s = Cubic::smooth(1, 1, 2, 3, 1, 1, 2, 3); 197 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 198 | TEST_ASSERT(std::abs(s.dx()(1) - 1) < 1e-6); 199 | TEST_ASSERT(std::abs(s(2) - 2) < 1e-6); 200 | TEST_ASSERT(std::abs(s.dx()(2) - 1) < 1e-6); 201 | } 202 | 203 | { // Duplicate left point means it continues existing curve (quadratic here) 204 | Cubic s = Cubic::smooth(1, 1, 2, 3, 1, 1, 0, 1); 205 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 206 | TEST_ASSERT(std::abs(s.dx()(1) + 2) < 1e-6); 207 | TEST_ASSERT(std::abs(s(2) - 0) < 1e-6); 208 | TEST_ASSERT(std::abs(s.dx()(2) - 0) < 1e-6); 209 | } 210 | 211 | { // Vertical also continues the curve 212 | Cubic s = Cubic::smooth(1, 1, 2, 3, 0, 1, 2, 3); 213 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 214 | TEST_ASSERT(std::abs(s.dx()(1) - 1) < 1e-6); 215 | TEST_ASSERT(std::abs(s(2) - 2) < 1e-6); 216 | TEST_ASSERT(std::abs(s.dx()(2) - 1) < 1e-6); 217 | } 218 | 219 | { // or flat, if it's a min/max 220 | Cubic s = Cubic::smooth(1, 1, 2, 3, 2, 1, 2, 3); 221 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 222 | TEST_ASSERT(std::abs(s.dx()(1) - 0) < 1e-6); 223 | TEST_ASSERT(std::abs(s(2) - 2) < 1e-6); 224 | TEST_ASSERT(std::abs(s.dx()(2) - 1) < 1e-6); 225 | } 226 | 227 | 228 | { // Duplicate right point means it continues existing curve (straight here) 229 | Cubic s = Cubic::smooth(0, 1, 2, 2, 0, 1, 2, 2); 230 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 231 | TEST_ASSERT(std::abs(s.dx()(1) - 1) < 1e-6); 232 | TEST_ASSERT(std::abs(s(2) - 2) < 1e-6); 233 | TEST_ASSERT(std::abs(s.dx()(2) - 1) < 1e-6); 234 | } 235 | 236 | { // Duplicate right point means it continues existing curve (quadratic here) 237 | Cubic s = Cubic::smooth(0, 1, 2, 2, 0, 1, 0, 0); 238 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 239 | TEST_ASSERT(std::abs(s.dx()(1) - 0) < 1e-6); 240 | TEST_ASSERT(std::abs(s(2) - 0) < 1e-6); 241 | TEST_ASSERT(std::abs(s.dx()(2) + 2) < 1e-6); 242 | } 243 | 244 | { // Vertical also continues the curve 245 | Cubic s = Cubic::smooth(0, 1, 2, 2, 0, 1, 2, 3); 246 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 247 | TEST_ASSERT(std::abs(s.dx()(1) - 1) < 1e-6); 248 | TEST_ASSERT(std::abs(s(2) - 2) < 1e-6); 249 | TEST_ASSERT(std::abs(s.dx()(2) - 1) < 1e-6); 250 | } 251 | 252 | { // or flat, if it's a min/max 253 | Cubic s = Cubic::smooth(0, 1, 2, 2, 0, 1, 2, 1); 254 | TEST_ASSERT(std::abs(s(1) - 1) < 1e-6); 255 | TEST_ASSERT(std::abs(s.dx()(1) - 1) < 1e-6); 256 | TEST_ASSERT(std::abs(s(2) - 2) < 1e-6); 257 | TEST_ASSERT(std::abs(s.dx()(2) - 0) < 1e-6); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /tests/curves/02-reciprocal.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include "curves.h" 5 | 6 | #include 7 | 8 | TEST("Reciprocal correctly maps") { 9 | double accuracy = 1e-6; 10 | 11 | for (int repeat = 0; repeat < 100; ++repeat) { 12 | double x0 = test.random(-100, 100), x1 = test.random(-100, 100); 13 | double y0 = test.random(-100, 100), y1 = test.random(-100, 100); 14 | double xm = x0 + (x1 - x0)*test.random(0, 1); 15 | double ym = y0 + (y1 - y0)*test.random(0, 1); 16 | 17 | signalsmith::curves::Reciprocal reciprocal(x0, xm, x1, y0, ym, y1); 18 | TEST_ASSERT(std::abs(reciprocal(x0) - y0) < accuracy); 19 | TEST_ASSERT(std::abs(reciprocal(x1) - y1) < accuracy); 20 | TEST_ASSERT(std::abs(reciprocal(xm) - ym) < accuracy); 21 | 22 | // Test it's monotonic 23 | bool increasing = (y1 > y0); 24 | double prevY = y0; 25 | for (double r = 0.01; r < 1; r += 0.01) { 26 | double x = x0 + (x1 - x0)*r; 27 | double y = reciprocal(x); 28 | TEST_ASSERT((y > prevY) == increasing); 29 | prevY = y; 30 | } 31 | } 32 | } 33 | 34 | TEST("Reciprocal inverse") { 35 | double accuracy = 1e-6; 36 | 37 | for (int repeat = 0; repeat < 10; ++repeat) { 38 | double x0 = test.random(-100, 100), x1 = test.random(-100, 100); 39 | double y0 = test.random(-100, 100), y1 = test.random(-100, 100); 40 | double xm = x0 + (x1 - x0)*test.random(0, 1); 41 | double ym = y0 + (y1 - y0)*test.random(0, 1); 42 | 43 | signalsmith::curves::Reciprocal reciprocal(x0, xm, x1, y0, ym, y1); 44 | signalsmith::curves::Reciprocal inverse = reciprocal.inverse(); 45 | 46 | for (int repeat2 = 0; repeat2 < 10; ++repeat2) { 47 | double x = x0 + (x1 - x0)*test.random(0, 1); 48 | double y = reciprocal(x); 49 | TEST_ASSERT(std::abs(inverse(y) - x) < accuracy) 50 | TEST_ASSERT(inverse(y) == reciprocal.inverse(y)) 51 | } 52 | } 53 | } 54 | 55 | TEST("Reciprocal compose") { 56 | double accuracy = 1e-6; 57 | 58 | for (int repeat = 0; repeat < 10; ++repeat) { 59 | 60 | double x0 = test.random(-100, 100), x1 = test.random(-100, 100); 61 | double y0 = test.random(-100, 100), y1 = test.random(-100, 100); 62 | double z0 = test.random(-100, 100), z1 = test.random(-100, 100); 63 | double xm = x0 + (x1 - x0)*test.random(0, 1); 64 | double ym = y0 + (y1 - y0)*test.random(0, 1); 65 | double zm = z0 + (z1 - z0)*test.random(0, 1); 66 | 67 | signalsmith::curves::Reciprocal reciprocalA(x0, xm, x1, y0, ym, y1); 68 | signalsmith::curves::Reciprocal reciprocalB(y0, ym, y1, z0, zm, z1); 69 | signalsmith::curves::Reciprocal composite = reciprocalA.then(reciprocalB); 70 | 71 | for (int repeat2 = 0; repeat2 < 10; ++repeat2) { 72 | double x = x0 + (x1 - x0)*test.random(0, 1); 73 | double y = reciprocalA(x); 74 | double z = reciprocalB(y); 75 | double zC = composite(x); 76 | TEST_ASSERT(std::abs(zC - z) < accuracy) 77 | } 78 | } 79 | } 80 | 81 | TEST("Reciprocal (example)") { 82 | Plot2D plot(200, 200); 83 | plot.x.linear(-2, 1).major(-2, "").label("input"); 84 | plot.y.linear(0, 10).major(0, "").label("output"); 85 | auto addLine = [&](double xm, double ym) { 86 | signalsmith::curves::Reciprocal reciprocal(-2, xm, 1, 0, ym, 10); 87 | auto &line = plot.line(); 88 | 89 | for (double x = -2; x < 1; x += 0.001) { 90 | line.add(x, reciprocal(x)); 91 | } 92 | line.styleIndex.marker = 0; 93 | line.marker(xm, ym); 94 | }; 95 | addLine(0.2, 0.5); 96 | addLine(-1, 2); 97 | addLine(-0.5, 8); 98 | addLine(-1.5, 9.5); 99 | 100 | plot.write("curves-reciprocal-example.svg"); 101 | return test.pass(); 102 | } 103 | -------------------------------------------------------------------------------- /tests/curves/03-reciprocal-bark.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include "curves.h" 5 | 6 | #include 7 | 8 | TEST("Reciprocal Bark-scale approximation") { 9 | struct Pair { 10 | double centre; 11 | double bandwidth; 12 | }; 13 | std::vector observed{ 14 | {50, 80}, 15 | {150, 100}, 16 | {250, 100}, 17 | {350, 100}, 18 | {450, 110}, 19 | {570, 120}, 20 | {700, 140}, 21 | {840, 150}, 22 | {1000, 160}, 23 | {1170, 190}, 24 | {1370, 210}, 25 | {1600, 240}, 26 | {1850, 280}, 27 | {2150, 320}, 28 | {2500, 380}, 29 | {2900, 450}, 30 | {3400, 550}, 31 | {4000, 700}, 32 | {4800, 900}, 33 | {5800, 1100}, 34 | {7000, 1300}, 35 | {8500, 1800}, 36 | {10500, 2500}, 37 | {13500, 3500} 38 | }; 39 | 40 | auto barkScale = signalsmith::curves::Reciprocal::barkScale(); 41 | 42 | signalsmith::plot::Figure figure; 43 | auto &plot = figure(0, 0).plot(350, 200); 44 | 45 | auto &approxFreq = plot.line(0); 46 | approxFreq.styleIndex.marker = 3; // hollow circle 47 | auto &approxBw = plot.line(3); 48 | approxBw.styleIndex.dash = 0; 49 | approxBw.styleIndex.marker = 1; // hollow diamond 50 | for (double bark = 0.25; bark <= 27.5; bark += 0.01) { 51 | double hz = barkScale(bark); 52 | approxFreq.add(bark, hz); 53 | approxBw.add(bark, barkScale.dx(bark)); 54 | } 55 | 56 | for (int b = 1; b <= 24; ++b) { 57 | auto &pair = observed[b - 1]; 58 | approxFreq.marker(b, pair.centre); 59 | approxBw.marker(b, pair.bandwidth); 60 | } 61 | 62 | plot.y.range(std::log, 30, 20000).major(100, "100 Hz").major(1000, "1 kHz").major(10000, "10 kHz"); 63 | 64 | plot.x.linear(0.39, 25.5).label("Bark scale").majors(1, 24); 65 | for (int i = 2; i <= 9; ++i) { 66 | if (i >= 3) plot.y.minor(10*i, ""); 67 | plot.y.minor(100*i, ""); 68 | plot.y.minor(1000*i, ""); 69 | } 70 | plot.y.minor(20000, ""); 71 | for (int i = 2; i <= 23; ++i) { 72 | plot.x.tick(i, " "); 73 | } 74 | 75 | approxFreq.label(16, 4000, "frequency", 180, 0); 76 | approxBw.label(16, 300, "bandwidth", 0, 0); 77 | 78 | figure.write("curves-reciprocal-approx-bark.svg"); 79 | test.pass(); 80 | } 81 | -------------------------------------------------------------------------------- /tests/delay/00-buffer.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "delay.h" 5 | 6 | #include "test-delay-stats.h" 7 | 8 | TEST("Delay buffer stores data") { 9 | int delaySize = 100; 10 | signalsmith::delay::Buffer buffer(delaySize); 11 | 12 | for (int i = 0; i < delaySize; ++i) { 13 | buffer[-i] = i; 14 | } 15 | for (int i = 0; i < delaySize; ++i) { 16 | TEST_ASSERT(buffer[-i] == i); 17 | } 18 | 19 | ++buffer; 20 | for (int i = 0; i < delaySize - 1; ++i) { 21 | // Incremented index 22 | TEST_ASSERT(buffer[-1 - i] == i); 23 | } 24 | --buffer; 25 | for (int i = 0; i < delaySize; ++i) { 26 | TEST_ASSERT(buffer[-i] == i); 27 | } 28 | auto view = buffer++; 29 | for (int i = 0; i < delaySize - 1; ++i) { 30 | TEST_ASSERT(buffer[-1 - i] == i); 31 | TEST_ASSERT(view[-i] == i); 32 | } 33 | view = buffer--; 34 | for (int i = 0; i < delaySize - 1; ++i) { 35 | TEST_ASSERT(view[-1 - i] == i); 36 | TEST_ASSERT(buffer[-i] == i); 37 | } 38 | 39 | buffer += 10; 40 | for (int i = 0; i < delaySize - 10; ++i) { 41 | TEST_ASSERT(buffer[-10 - i] == i); 42 | } 43 | buffer -= 20; 44 | for (int i = 0; i < delaySize; ++i) { 45 | TEST_ASSERT(buffer[10 - i] == i); 46 | } 47 | buffer += 10; 48 | for (int i = 0; i < delaySize; ++i) { 49 | TEST_ASSERT(buffer[-i] == i); 50 | } 51 | 52 | view = buffer + 10; 53 | for (int i = 0; i < delaySize - 10; ++i) { 54 | TEST_ASSERT(view[-10 - i] == i); 55 | TEST_ASSERT(buffer[-i] == i); 56 | } 57 | view = view - 20; 58 | for (int i = 0; i < delaySize; ++i) { 59 | TEST_ASSERT(view[10 - i] == i); 60 | TEST_ASSERT(buffer[-i] == i); 61 | } 62 | view = buffer - 10; 63 | for (int i = 0; i < delaySize; ++i) { 64 | TEST_ASSERT(view[10 - i] == i); 65 | TEST_ASSERT(buffer[-i] == i); 66 | } 67 | view = buffer.view(5); 68 | for (int i = 0; i < delaySize - 5; ++i) { 69 | TEST_ASSERT(view[-5 - i] == i); 70 | TEST_ASSERT(buffer[-i] == i); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/delay/02-multi-buffer.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "delay.h" 5 | 6 | #include "test-delay-stats.h" 7 | 8 | TEST("Multi-channel buffer stores data") { 9 | int delaySize = 100; 10 | int channels = 4; 11 | using MultiBuffer = signalsmith::delay::MultiBuffer; 12 | MultiBuffer multiBuffer(channels, delaySize); 13 | 14 | for (int c = 0; c < channels; ++c) { 15 | for (int i = 0; i < delaySize; ++i) { 16 | multiBuffer[c][-i] = i + c*delaySize; 17 | } 18 | } 19 | const MultiBuffer &constMultiBuffer = multiBuffer; 20 | for (int c = 0; c < channels; ++c) { 21 | for (int i = 0; i < delaySize; ++i) { 22 | TEST_ASSERT(multiBuffer[c][-i] == i + c*delaySize); 23 | TEST_ASSERT(constMultiBuffer[c][-i] == i + c*delaySize); 24 | } 25 | } 26 | 27 | for (int i = 0; i < delaySize; ++i) { 28 | auto value = multiBuffer.at(-i); 29 | auto constValue = constMultiBuffer.at(-i); 30 | 31 | std::vector expected(channels), zeros(channels, 0); 32 | std::vector read(channels); 33 | constMultiBuffer.at(-i).get(read); 34 | for (int c = 0; c < channels; ++c) { 35 | expected[c] = i + c*delaySize; 36 | TEST_ASSERT(value[c] == expected[c]); 37 | TEST_ASSERT(constValue[c] == expected[c]); 38 | TEST_ASSERT(read[c] == expected[c]); 39 | 40 | value[c] = 0; 41 | TEST_ASSERT(value[c] == 0); 42 | TEST_ASSERT(read[c] == expected[c]); 43 | } 44 | // Reset value 45 | value = expected; 46 | for (int c = 0; c < channels; ++c) { 47 | TEST_ASSERT(value[c] == expected[c]); 48 | } 49 | multiBuffer.at(-i) = zeros; 50 | for (int c = 0; c < channels; ++c) { 51 | TEST_ASSERT(value[c] == 0); 52 | } 53 | multiBuffer.at(-i)[0] = 5.5; 54 | TEST_ASSERT(multiBuffer[0][-i] == 5.5); 55 | multiBuffer.at(-i).set(expected); 56 | for (int c = 0; c < channels; ++c) { 57 | TEST_ASSERT(value[c] == expected[c]); 58 | } 59 | } 60 | 61 | // Tests adapted from the main delayBuffer stuff 62 | 63 | ++multiBuffer; 64 | for (int i = 0; i < delaySize - 1; ++i) { 65 | // Incremented index 66 | TEST_ASSERT(multiBuffer[0][-1 - i] == i); 67 | } 68 | --multiBuffer; 69 | for (int i = 0; i < delaySize; ++i) { 70 | TEST_ASSERT(multiBuffer[0][-i] == i); 71 | } 72 | auto view = (multiBuffer++)[0]; 73 | for (int i = 0; i < delaySize - 1; ++i) { 74 | TEST_ASSERT(multiBuffer[0][-1 - i] == i); 75 | TEST_ASSERT(view[-i] == i); 76 | } 77 | view = (multiBuffer--)[0]; 78 | for (int i = 0; i < delaySize - 1; ++i) { 79 | TEST_ASSERT(view[-1 - i] == i); 80 | TEST_ASSERT(multiBuffer[0][-i] == i); 81 | } 82 | 83 | multiBuffer += 10; 84 | for (int i = 0; i < delaySize - 10; ++i) { 85 | TEST_ASSERT(multiBuffer[0][-10 - i] == i); 86 | } 87 | multiBuffer -= 20; 88 | for (int i = 0; i < delaySize; ++i) { 89 | TEST_ASSERT(multiBuffer[0][10 - i] == i); 90 | } 91 | multiBuffer += 10; 92 | for (int i = 0; i < delaySize; ++i) { 93 | TEST_ASSERT(multiBuffer[0][-i] == i); 94 | } 95 | 96 | view = (multiBuffer + 10)[0]; 97 | for (int i = 0; i < delaySize - 10; ++i) { 98 | TEST_ASSERT(view[-10 - i] == i); 99 | TEST_ASSERT(multiBuffer[0][-i] == i); 100 | } 101 | view = view - 20; 102 | for (int i = 0; i < delaySize; ++i) { 103 | TEST_ASSERT(view[10 - i] == i); 104 | TEST_ASSERT(multiBuffer[0][-i] == i); 105 | } 106 | view = (multiBuffer - 10)[0]; 107 | for (int i = 0; i < delaySize; ++i) { 108 | TEST_ASSERT(view[10 - i] == i); 109 | TEST_ASSERT(multiBuffer[0][-i] == i); 110 | } 111 | view = multiBuffer.view(5)[0]; 112 | for (int i = 0; i < delaySize - 5; ++i) { 113 | TEST_ASSERT(view[-5 - i] == i); 114 | TEST_ASSERT(multiBuffer[0][-i] == i); 115 | } 116 | view = multiBuffer.view(3)[0] + 5; 117 | for (int i = 0; i < delaySize - 8; ++i) { 118 | TEST_ASSERT(view[-8 - i] == i); 119 | TEST_ASSERT(multiBuffer[0][-i] == i); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/delay/03-multi-delay.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "delay.h" 5 | #include "test-delay-stats.h" 6 | 7 | #include 8 | #include 9 | 10 | TEST("Multi-Delay") { 11 | constexpr int channels = 3; 12 | int delayLength = 80; 13 | 14 | signalsmith::delay::MultiDelay multiDelay(channels, delayLength); 15 | 16 | // Put a known sequence in 17 | for (int i = 0; i < delayLength; ++i) { 18 | std::array values; 19 | for (int c = 0; c < channels; ++c) values[c] = c + i*channels; 20 | multiDelay.write(values); 21 | } 22 | 23 | // Read out delayed samples 24 | for (int i = 0; i < delayLength; ++i) { 25 | std::array zero; 26 | for (int c = 0; c < channels; ++c) zero[c] = 0; 27 | 28 | auto values = multiDelay.write(zero).read(delayLength); 29 | for (int c = 0; c < channels; ++c) { 30 | TEST_ASSERT(values[c] == c + i*channels); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/delay/fractional-delay.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import math 3 | import sys 4 | import os 5 | import glob 6 | 7 | import article 8 | 9 | def fractionalPlots(prefix): 10 | figure, (ampAxes, delayAxes) = article.medium(2) 11 | figure.set_size_inches(6.5, 4.5) 12 | 13 | targetDelays = numpy.linspace(0, 0.5, 6) 14 | 15 | def limitedCsv(filename): 16 | columns, data = article.readCsv(filename) 17 | newColumns = [columns[0]] 18 | newData = [data[0]] 19 | 20 | for target in targetDelays: 21 | bestIndex = 1 22 | bestDelay = -1 23 | for i in range(1, len(columns)): 24 | delay = float(columns[i]) 25 | diff = abs(delay - target) 26 | if diff < abs(bestDelay - target): 27 | bestDelay = delay 28 | bestIndex = i 29 | newColumns.append(columns[bestIndex]) 30 | newData.append(data[bestIndex]) 31 | 32 | return newColumns, numpy.array(newData) 33 | 34 | columns, data = limitedCsv(prefix + ".fractional-amplitude.csv") 35 | for i in range(1, len(columns)): 36 | ampAxes.plot(data[0], data[i], label=columns[i]) 37 | ampAxes.set(ylim=[-21, 1], ylabel="response (dB)", xlim=[0, 0.5]) 38 | 39 | columns, data = limitedCsv(prefix + ".fractional-group.csv") 40 | minDelay = data[1:,:-1].min() 41 | maxDelay = data[1:,:-1].max() 42 | for i in range(1, len(columns)): 43 | # Nyquist entry is always spurious 44 | delayAxes.plot(data[0][:-1], data[i][:-1]) 45 | delayAxes.set(ylim=[math.floor(minDelay + 1e-6), math.ceil(maxDelay - 1e-6)], xlim=[0, 0.5], ylabel="samples", xlabel="input frequency (normalised)") 46 | 47 | figure.save(prefix + ".fractional.svg") 48 | 49 | def statsPlots(outputPrefix, names=None, prefixes=None): 50 | figure, (aliasingAxes, responseAxes, delayAxes) = article.medium(3) 51 | figure.set_size_inches(6.5, 6.5) 52 | 53 | targetDelays = numpy.linspace(0, 0.5, 6) 54 | 55 | shade_alpha_bounded = 0.06 56 | shade_alpha = 0.1 57 | 58 | if prefixes == None: 59 | prefixes = [outputPrefix] 60 | if names == None: 61 | names = ["interpolator"] 62 | 63 | for i in range(len(prefixes)): 64 | prefix = prefixes[i] 65 | name = names[i] 66 | 67 | columns, data = article.readCsv(prefix + ".fractional-stats.csv") 68 | aliasingAxes.plot(data[0], data[1], label=name) 69 | aliasingAxes.set(ylim=[-95, 0], ylabel="aliasing (dB)", xlim=[0, 0.5]) 70 | 71 | responseAxes.plot(numpy.concatenate((data[0],)), numpy.concatenate((data[2],))) 72 | fillResponse = responseAxes.fill_between(data[0], data[3], data[4], alpha=shade_alpha, color=article.colors[i]) 73 | responseAxes.set(ylim=[-13, 1], ylabel="mean/range (dB)", xlim=[0, 0.5], yticks=[0, -3, -6, -9, -12]) 74 | 75 | delayRangeIndex = min(len(data[5]) - 1, int(len(data[5])*0.95)); 76 | minDelay = data[5,:delayRangeIndex].min() 77 | maxDelay = data[6:,:delayRangeIndex].max() 78 | fillDelay = delayAxes.fill_between(data[0][:-1], data[5][:-1], data[6][:-1], alpha=shade_alpha_bounded, color=article.colors[i]) 79 | delayAxes.plot(numpy.concatenate((data[0][:-1], [None], data[0][:-1])), numpy.concatenate((data[5][:-1], [None], data[6][:-1]))) 80 | 81 | delayAxes.set(ylim=[math.floor(minDelay - 0.1), math.ceil(maxDelay + 0.1)], xlim=[0, 0.5], ylabel="delay error (samples)", xlabel="input frequency (normalised)") 82 | 83 | figure.save(outputPrefix + ".svg", legend_loc='best') 84 | figure.save(outputPrefix + "@2x.png", legend_loc='best', dpi=180) 85 | 86 | def animatePlots(prefix, doubleBack=True): 87 | columns, impulses = article.readCsv(prefix + ".fractional-impulse.csv") 88 | columns, amplitudes = article.readCsv(prefix + ".fractional-amplitude.csv") 89 | columns, delayErrors = article.readCsv(prefix + ".fractional-group.csv") 90 | 91 | impulseLength = 0 92 | for i in range(len(impulses[1])): 93 | if max(numpy.abs(impulses[1:,i])) > 1e-4: 94 | impulseLength = i 95 | minImpulse = impulses[1:].min() 96 | 97 | delayRangeIndex = min(len(delayErrors[0]) - 1, int(len(delayErrors[0])*0.95)); 98 | minDelay = delayErrors[1:,:delayRangeIndex].min() 99 | maxDelay = delayErrors[1:,:delayRangeIndex].max() 100 | 101 | frameStep = 1 102 | def drawFrame(file, frameNumber, time): 103 | frameNumber *= frameStep 104 | if frameNumber > len(columns) - 2: 105 | frameNumber = (len(columns) - 2)*2 - frameNumber; 106 | index = min(len(columns) - 1, (frameNumber + 1)) 107 | figure, (impulseAxes, ampAxes, delayAxes) = article.medium(3) 108 | figure.set_size_inches(6.5, 6.5) 109 | 110 | impulseAxes.plot(impulses[0], impulses[index], label="max") 111 | impulseAxes.set(xlim=[-2, impulseLength + 3], xlabel="sample", ylim=[min(-0.1, minImpulse - 0.1), 1.1], ylabel="impulse response") 112 | 113 | ampAxes.plot(amplitudes[0], amplitudes[index]) 114 | ampAxes.set(ylim=[-26, 1], ylabel="dB", xlim=[0, 0.5]) 115 | 116 | delayAxes.plot(delayErrors[0][:-1], delayErrors[index][:-1]) 117 | delayAxes.set(ylim=[math.floor(minDelay - 0.1), math.ceil(maxDelay + 0.1)], xlim=[0, 0.5], ylabel="samples", xlabel="input frequency (normalised)") 118 | 119 | figure.save(file, dpi=180) 120 | 121 | frameCount = (len(columns) - 1) 122 | if doubleBack: 123 | frameCount = 2*frameCount - 2; 124 | frameCount = int(math.ceil(frameCount*1.0/frameStep)) 125 | article.animate(prefix + ".mp4", drawFrame, fps=30/frameStep, frameCount=frameCount, previewRatio=0.4) 126 | 127 | def deleteCsv(prefix): 128 | os.remove(prefix + ".fractional-amplitude.csv") 129 | os.remove(prefix + ".fractional-aliasing.csv") 130 | os.remove(prefix + ".fractional-group.csv") 131 | os.remove(prefix + ".fractional-impulse.csv") 132 | # Not currently plotted 133 | os.remove(prefix + ".fractional-phase.csv") 134 | 135 | ########## 136 | 137 | prefix = "." 138 | tasks = sys.argv[1:] 139 | 140 | statsPlots("interpolator-LagrangeN", ["Lagrange3", "Lagrange7", "Lagrange19"], ["delay-random-access-lagrange3", "delay-random-access-lagrange7", "delay-random-access-lagrange19"]) 141 | statsPlots("interpolator-KaiserSincN", ["4-point", "8-point", "20-point"], ["delay-random-access-sinc4", "delay-random-access-sinc8", "delay-random-access-sinc20"]) 142 | statsPlots("interpolator-KaiserSincN-min", ["4-point", "8-point", "20-point"], ["delay-random-access-sinc4min", "delay-random-access-sinc8min", "delay-random-access-sinc20min"]) 143 | statsPlots("delay-random-access-linear") 144 | statsPlots("delay-random-access-cubic") 145 | statsPlots("delay-random-access-nearest") 146 | statsPlots("interpolator-cubic-linear-comparison", ["Spline", "Lagrange-3", "Linear"], ["delay-random-access-cubic", "delay-random-access-lagrange3", "delay-random-access-linear"]) 147 | 148 | if os.path.isdir(prefix): 149 | suffix = ".fractional-amplitude.csv" 150 | candidates = glob.glob(prefix + "/**" + suffix) 151 | prefixes = [path[:-len(suffix)] for path in candidates] 152 | for prefix in prefixes: 153 | # fractionalPlots(prefix) 154 | # statsPlots(prefix) 155 | pass 156 | if "animate" in tasks: 157 | for prefix in prefixes: 158 | animatePlots(prefix) 159 | if "delete" in tasks: 160 | for prefix in prefixes: 161 | deleteCsv(prefix) 162 | else: 163 | fractionalPlots(prefix) 164 | statsPlots(prefix) 165 | if "animate" in tasks: 166 | animatePlots(prefix) 167 | if "delete" in tasks: 168 | deleteCsv(prefix) 169 | -------------------------------------------------------------------------------- /tests/envelopes/00-cubic-lfo.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include "envelopes.h" 5 | #include "spectral.h" 6 | #include "windows.h" 7 | 8 | #include 9 | 10 | TEST("Cubic LFO") { 11 | long seed = 12345; 12 | signalsmith::envelopes::CubicLfo lfoA(seed), lfoB(seed), lfoC(seed), lfoD(seed), lfoE(seed); 13 | 14 | lfoA.set(8, 10, 0.02, 0, 0); 15 | lfoB.set(6, 8, 0.02, 1, 0); 16 | lfoC.set(4, 6, 0.02, 0, 0.5); 17 | lfoD.set(2, 4, 0.02, 0, 1); 18 | lfoE.set(0, 2, 0.02, 1, 1); 19 | 20 | CsvWriter csv("cubic-lfo-example"); 21 | csv.line('i', "no random", "freq", "depth (50%)", "depth (100%)", "both"); 22 | 23 | for (int i = 0; i < 500; ++i) { 24 | csv.line(i, lfoA.next(), lfoB.next(), lfoC.next(), lfoD.next(), lfoE.next()); 25 | } 26 | return test.pass(); 27 | } 28 | 29 | TEST("Cubic LFO (spectrum)") { 30 | constexpr int count = 5; 31 | std::array factors = {0, 0.2, 0.4, 0.6, 0.8}; 32 | 33 | double rate = 1.0/20.382; 34 | int duration = 8192; 35 | int trials = 1000; 36 | std::vector waveform(duration); 37 | std::vector> spectrum(duration); 38 | 39 | auto writeSpectra = [&](std::string name, double freqRandom, double ampRandom) { 40 | std::vector> spectralEnergy; 41 | using Kaiser = signalsmith::windows::Kaiser; 42 | auto kaiser = Kaiser::withBandwidth(Kaiser::energyDbToBandwidth(-120)); 43 | signalsmith::spectral::WindowedFFT fft(duration, [&](double r) { 44 | return kaiser(r); 45 | }); 46 | 47 | for (auto f : factors) { 48 | std::vector energy(duration); 49 | for (int r = 0; r < trials; ++r) { 50 | signalsmith::envelopes::CubicLfo lfo; 51 | lfo.set(-1, 1, rate, f*freqRandom, f*ampRandom); 52 | 53 | for (int i = 0; i < duration; ++i) { 54 | waveform[i] = lfo.next(); 55 | } 56 | fft.fft(waveform, spectrum); 57 | for (int f = 0; f < duration; ++f) { 58 | energy[f] += std::norm(spectrum[f]); 59 | } 60 | } 61 | spectralEnergy.push_back(std::move(energy)); 62 | } 63 | 64 | CsvWriter csv(name); 65 | csv.write("f"); 66 | for (auto f : factors) { 67 | csv.write(f); 68 | } 69 | csv.line(); 70 | for (int f = 0; f < duration; ++f) { 71 | csv.write(f*1.0/duration/rate); 72 | for (int i = 0; i < count; ++i) { 73 | csv.write(spectralEnergy[i][f]); 74 | } 75 | csv.line(); 76 | } 77 | }; 78 | writeSpectra("cubic-lfo-spectrum-freq", 1, 0); 79 | writeSpectra("cubic-lfo-spectrum-depth", 0, 1); 80 | 81 | return test.pass(); 82 | } 83 | 84 | TEST("Cubic LFO (changes)") { 85 | double low = 0, high = 1; 86 | double rate = 0.01; 87 | signalsmith::envelopes::CubicLfo lfo, randomLfo(12345); 88 | 89 | CsvWriter csv("cubic-lfo-changes"); 90 | csv.line("i", "LFO", "randomised", "low", "high"); 91 | int i = 0; 92 | auto writeSamples = [&](int length) { 93 | lfo.set(low, high, rate); 94 | randomLfo.set(low, high, rate, 1, 0.4); 95 | int end = i + length; 96 | for (; i < end; ++i) { 97 | csv.line(i, lfo.next(), randomLfo.next(), low, high); 98 | } 99 | }; 100 | writeSamples(430); 101 | high = 2; 102 | low = 0.5; 103 | writeSamples(285); 104 | low = -2; 105 | high = - 1; 106 | writeSamples(230); 107 | 108 | return test.pass(); 109 | } 110 | -------------------------------------------------------------------------------- /tests/envelopes/01-box-sum-average.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include 5 | #include 6 | 7 | #include "envelopes.h" 8 | 9 | TEST("Box sum") { 10 | int length = 1000; 11 | std::vector signal(length); 12 | for (auto &v : signal) { 13 | v = test.random(-1, 1); 14 | } 15 | 16 | int maxBoxLength = 100; 17 | signalsmith::envelopes::BoxSum boxSum(maxBoxLength); 18 | signalsmith::envelopes::BoxFilter boxFilter(maxBoxLength); 19 | 20 | for (int i = 0; i < length; ++i) { 21 | int boxLength = test.randomInt(0, maxBoxLength); 22 | double result = boxSum.readWrite(signal[i], boxLength); 23 | boxFilter.set(boxLength); 24 | double resultAverage = boxFilter(signal[i]); 25 | 26 | int start = std::max(0, i - boxLength + 1); 27 | double sum = (boxLength ? signal[i] : 0); 28 | for (int j = start; j < i; ++j) { 29 | sum += signal[j]; 30 | } 31 | 32 | double diff = result - sum; 33 | TEST_ASSERT(std::abs(diff) < 1e-12); 34 | 35 | if (boxLength > 0) { 36 | double diffAvg = resultAverage - sum/boxLength; 37 | TEST_ASSERT(std::abs(diffAvg) < 1e-12); 38 | } 39 | } 40 | 41 | boxSum.reset(); 42 | boxFilter.reset(); 43 | 44 | for (int i = 0; i < length; ++i) { 45 | int boxLength = test.randomInt(0, maxBoxLength); 46 | boxSum.write(signal[i]); 47 | double result = boxSum.read(boxLength); 48 | 49 | int start = std::max(0, i - boxLength + 1); 50 | double sum = (boxLength ? signal[i] : 0); 51 | for (int j = start; j < i; ++j) { 52 | sum += signal[j]; 53 | } 54 | 55 | double diff = result - sum; 56 | TEST_ASSERT(std::abs(diff) < 1e-12); 57 | } 58 | } 59 | 60 | TEST("Box sum (drift)") { 61 | int maxBoxLength = 100; 62 | signalsmith::envelopes::BoxSum boxSum(maxBoxLength); 63 | 64 | for (int repeat = 0; repeat < 10; ++repeat) { 65 | for (int i = 0; i < 10000; ++i) { 66 | float v = test.random(1e6, 2e6); 67 | boxSum.write(v); 68 | } 69 | 70 | for (int i = 0; i < maxBoxLength*2; ++i) { 71 | float v = (i&1) ? 1 : -1; 72 | boxSum.write(v); 73 | } 74 | 75 | for (int r = 0; r < 10; ++r) { 76 | int boxLength = test.randomInt(25, 100); 77 | float expected = (boxLength%2) ? 1 : 0; 78 | float actual = boxSum.read(boxLength); 79 | 80 | TEST_ASSERT(expected == actual); // Should be exact match 81 | } 82 | } 83 | } 84 | 85 | TEST("Box filter (example)") { 86 | int boxLength = 100; 87 | signalsmith::envelopes::BoxFilter boxFilter(boxLength + 100); 88 | boxFilter.set(boxLength); 89 | 90 | signalsmith::envelopes::CubicLfo fast, slow; 91 | fast.set(-3, 3, 0.08, 1, 1); 92 | slow.set(-5, 5, 0.005, 1, 1); 93 | 94 | CsvWriter csv("box-filter-example"); 95 | csv.line("i", "signal", "box-filter (100)"); 96 | for (int i = -boxLength; i < boxLength*6; ++i) { 97 | double signal = fast.next() + slow.next(); 98 | double smoothed = boxFilter(signal); 99 | if (i >= 0) { 100 | csv.line(i, signal, smoothed); 101 | } 102 | } 103 | return test.pass(); 104 | } 105 | -------------------------------------------------------------------------------- /tests/envelopes/02-box-stack.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include "envelopes.h" 5 | 6 | #include "fft.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | struct Result { 13 | std::vector> impulses; 14 | std::vector>> spectra; 15 | }; 16 | void analyseStack(Result &result, Test &test, int length, const std::vector &depths) { 17 | std::vector> filters; 18 | for (auto depth : depths) { 19 | filters.emplace_back(length, depth); 20 | } 21 | 22 | std::vector> impulses; 23 | std::vector>> spectra; 24 | 25 | impulses.resize(depths.size()); 26 | spectra.resize(depths.size()); 27 | 28 | for (int i = 0; i < length; ++i) { 29 | double v = (i == 0) ? 1 : 0; 30 | for (size_t di = 0; di < depths.size(); ++di) { 31 | double r = filters[di](v); 32 | impulses[di].push_back(r); 33 | TEST_ASSERT(r != 0); // non-zero impulse for whole range 34 | } 35 | } 36 | for (auto &filter : filters) { 37 | double v = filter(0); 38 | TEST_ASSERT(std::abs(v) < 1e-16); // should get back to 0 39 | } 40 | 41 | int fftSize = 32768; 42 | // Zero-pad 43 | for (auto &impulse : impulses) impulse.resize(fftSize, 0); 44 | 45 | signalsmith::fft::FFT fft(fftSize); 46 | for (size_t di = 0; di < depths.size(); ++di) { 47 | spectra[di].resize(fftSize, 0); 48 | fft.fft(impulses[di], spectra[di]); 49 | } 50 | 51 | result.impulses = impulses; 52 | result.spectra = spectra; 53 | } 54 | 55 | TEST("Box stack") { 56 | std::vector depths = {2, 4, 6}; 57 | Result longResult, shortResult; 58 | analyseStack(longResult, test, 1000, depths); 59 | analyseStack(shortResult, test, 30, depths); 60 | 61 | { 62 | CsvWriter csv("box-stack-long-time"); 63 | csv.write("i"); 64 | for (auto depth : depths) csv.write(depth); 65 | csv.line(); 66 | for (size_t i = 0; i < 1000; ++i) { 67 | csv.write(i); 68 | for (auto &impulse : longResult.impulses) { 69 | csv.write(impulse[i]); 70 | } 71 | csv.line(); 72 | } 73 | } 74 | { 75 | CsvWriter csv("box-stack-long-freq"); 76 | csv.write("f"); 77 | for (auto depth : depths) csv.write(depth); 78 | csv.line(); 79 | size_t fftSize = longResult.spectra[0].size(); 80 | size_t length = 1000; 81 | for (size_t f = 0; f < fftSize/2; ++f) { 82 | double relativeF = f*1.0*length/fftSize; 83 | csv.write(relativeF); 84 | for (size_t di = 0; di < depths.size(); ++di) { 85 | auto &spectrum = longResult.spectra[di]; 86 | double norm = std::norm(spectrum[f]); 87 | double db = 10*std::log10(norm + 1e-100); 88 | csv.write(db); 89 | } 90 | csv.line(); 91 | } 92 | } 93 | { 94 | CsvWriter csv("box-stack-short-freq"); 95 | csv.write("f"); 96 | for (auto depth : depths) csv.write(depth); 97 | csv.line(); 98 | size_t fftSize = shortResult.spectra[0].size(); 99 | size_t length = 30; 100 | for (size_t f = 0; f < fftSize/2; ++f) { 101 | csv.write(f*1.0*length/fftSize); 102 | for (auto &spectrum : shortResult.spectra) { 103 | double norm = std::norm(spectrum[f]); 104 | double db = 10*std::log10(norm + 1e-100); 105 | csv.write(db); 106 | } 107 | csv.line(); 108 | } 109 | } 110 | 111 | depths.clear(); 112 | for (int i = 1; i < 12; ++i) { 113 | depths.push_back(i); 114 | } 115 | analyseStack(longResult, test, 1000, depths); 116 | { 117 | size_t fftSize = longResult.spectra[0].size(); 118 | size_t length = 1000; 119 | for (size_t f = 0; f < fftSize/2; ++f) { 120 | double relativeF = f*1.0*length/fftSize; 121 | for (size_t di = 0; di < depths.size(); ++di) { 122 | auto &spectrum = longResult.spectra[di]; 123 | double norm = std::norm(spectrum[f]); 124 | double db = 10*std::log10(norm + 1e-100); 125 | 126 | double bandwidth = signalsmith::envelopes::BoxStackFilter::layersToBandwidth(depths[di]); 127 | double stopDb = signalsmith::envelopes::BoxStackFilter::layersToPeakDb(depths[di]); 128 | if (relativeF >= bandwidth*0.5) { 129 | TEST_ASSERT(db <= stopDb*0.98); // at most 2% off in dB terms 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | TEST("Box stack properties") { 137 | using Filter = signalsmith::envelopes::BoxStackFilter; 138 | CsvWriter csv("box-stack-stats"); 139 | csv.line("n", "bandwidth", "peak (dB)"); 140 | for (int n = 1; n < 20; ++n) { 141 | csv.line(n, Filter::layersToBandwidth(n), Filter::layersToPeakDb(n)); 142 | } 143 | return test.pass(); 144 | } 145 | 146 | TEST("Box stack custom ratios") { 147 | using Stack = signalsmith::envelopes::BoxStackFilter; 148 | using Filter = signalsmith::envelopes::BoxFilter; 149 | 150 | Filter filterA(61), filterB(31), filterC(11); // Effective length is 101, because a length-1 box-filter does nothing 151 | 152 | Stack stack(50, 1); 153 | stack.resize(101, {6, 3, 1}); 154 | 155 | for (int i = 0; i < 1000; ++i) { 156 | float signal = test.random(-1, 1); 157 | float stackResult = stack(signal); 158 | float filterResult = filterC(filterB(filterA(signal))); 159 | TEST_ASSERT(stackResult == filterResult); 160 | } 161 | 162 | return test.pass(); 163 | } 164 | 165 | TEST("Box stack optimal sizes are the right size") { 166 | using Stack = signalsmith::envelopes::BoxStackFilter; 167 | 168 | for (size_t i = 1; i < 20; ++i) { 169 | auto ratios = Stack::optimalRatios(i); 170 | TEST_ASSERT(ratios.size() == i); 171 | 172 | double sum = 0; 173 | for (auto r : ratios) sum += r; 174 | // Close enough to 1 175 | TEST_ASSERT(sum >= 0.9999 && sum <= 1.0001); 176 | } 177 | } 178 | 179 | TEST("Box stack handles zero sizes without crashing") { 180 | using Stack = signalsmith::envelopes::BoxStackFilter; 181 | 182 | // Results don't have to be good/usable, just not crash. It's assumed this is a temporary state until its configured properly. 183 | 184 | Stack stack(100, 1); 185 | stack.resize(100, 0); 186 | 187 | Stack stack2(100, 0); 188 | Stack stack3(100, -1); 189 | 190 | Stack stack4(0, 4); 191 | stack4.resize(-1, -1); 192 | 193 | return test.pass(); 194 | } 195 | -------------------------------------------------------------------------------- /tests/envelopes/03-peak-hold.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include 5 | #include 6 | 7 | #include "envelopes.h" 8 | 9 | TEST("Peak hold") { 10 | int length = 1000; 11 | std::vector signal(length); 12 | for (auto &v : signal) { 13 | v = test.random(-1, 1); 14 | } 15 | 16 | int maxLength = 100, holdLength = 50; 17 | float startingPeak = -5; 18 | signalsmith::envelopes::PeakHold peakHold(maxLength); 19 | TEST_ASSERT(peakHold.size() == maxLength); 20 | peakHold.reset(5); 21 | TEST_ASSERT(peakHold.read() == 5); 22 | TEST_ASSERT(peakHold.size() == maxLength); 23 | peakHold.set(holdLength); 24 | TEST_ASSERT(peakHold.size() == holdLength); 25 | peakHold.reset(startingPeak); 26 | TEST_ASSERT(peakHold.size() == holdLength); 27 | 28 | for (int i = 0; i < length; ++i) { 29 | float result = peakHold(signal[i]); 30 | 31 | int start = std::max(0, i - holdLength + 1); 32 | float peak = (i >= holdLength ? signal[i] : startingPeak); 33 | for (int j = start; j <= i; ++j) { 34 | peak = std::max(peak, signal[j]); 35 | } 36 | TEST_ASSERT(result == peak); 37 | } 38 | 39 | // Test all sizes 40 | for (int size = 0; size <= maxLength; ++size) { 41 | peakHold.reset(); 42 | peakHold.set(size); 43 | for (int i = 0; i < length; ++i) { 44 | float actual = peakHold(signal[i]); 45 | TEST_ASSERT(actual == peakHold.read()); 46 | if (i >= size - 1) { 47 | float peak = std::numeric_limits::lowest(); 48 | for (int j = i + 1 - size; j <= i; ++j) { 49 | peak = std::max(peak, signal[j]); 50 | } 51 | TEST_ASSERT(peakHold.read() == peak); 52 | } 53 | } 54 | } 55 | } 56 | 57 | TEST("Peak hold (example)") { 58 | int length = 250; 59 | signalsmith::envelopes::CubicLfo lfo(1248); 60 | lfo.set(0, 10, 0.05, 2, 1); 61 | 62 | signalsmith::envelopes::PeakHold peakHoldA(10), peakHoldB(50); 63 | 64 | CsvWriter csv("peak-hold"); 65 | csv.line("i", "signal", "peak (10)", "peak (50)"); 66 | for (int i = 0; i < length; ++i) { 67 | double v = lfo.next(); 68 | csv.line(i, v, peakHoldA(v), peakHoldB(v)); 69 | } 70 | return test.pass(); 71 | } 72 | 73 | TEST("Peak hold (push and pop)") { 74 | int maxLength = 200; 75 | signalsmith::envelopes::PeakHold peakHold(10); 76 | peakHold.resize(maxLength); 77 | 78 | std::vector signal(500); 79 | for (auto &v : signal) v = test.random(-1, 1); 80 | 81 | int start = 0, end = 100; 82 | peakHold.set(0); 83 | for (int i = 0; i < end; ++i) { 84 | peakHold.push(signal[i]); 85 | } 86 | 87 | auto check = [&]() { 88 | float expected = std::numeric_limits::lowest(); 89 | for (int i = start; i < end; ++i) { 90 | expected = std::max(expected, signal[i]); 91 | } 92 | TEST_ASSERT(expected == peakHold.read()); 93 | TEST_ASSERT(peakHold.size() == (end - start)); 94 | }; 95 | 96 | for (; end < 200; ++end) { 97 | peakHold.push(signal[end]); 98 | } 99 | check(); 100 | for (; start < 150; ++start) { 101 | peakHold.pop(); 102 | } 103 | check(); 104 | for (; start < 171; ++start) { 105 | peakHold.pop(); 106 | } 107 | check(); 108 | for (; end < 250; ++end) { 109 | peakHold.push(signal[end]); 110 | } 111 | check(); 112 | for (; start < 160; ++start) { 113 | peakHold.pop(); 114 | } 115 | check(); 116 | } 117 | 118 | TEST("Peak hold (push and pop random)") { 119 | int maxLength = 200; 120 | signalsmith::envelopes::PeakHold peakHold(maxLength); 121 | 122 | std::vector signal(5000); 123 | for (auto &v : signal) { 124 | double r = test.random(0, 1); 125 | v = 2*r*r - 1; 126 | } 127 | 128 | peakHold.set(0); 129 | int start = 0, end = 0; 130 | auto check = [&]() { 131 | float expected = std::numeric_limits::lowest(); 132 | for (int i = start; i < end; ++i) { 133 | expected = std::max(expected, signal[i]); 134 | } 135 | 136 | // // debugging stuff 137 | // { 138 | // CsvWriter csv("frames/peak-hold-" + std::to_string(frame++)); 139 | // peakHold._dumpState(csv); 140 | // } 141 | // if (expected != peakHold.read()) { 142 | // CsvWriter csv("peak-hold-error"); 143 | // peakHold._dumpState(csv); 144 | // } 145 | // LOG_EXPR(signal[end - 1]); 146 | // LOG_EXPR(start); 147 | // LOG_EXPR(end); 148 | // LOG_EXPR(expected); 149 | // LOG_EXPR(peakHold.read()); 150 | 151 | TEST_ASSERT(expected == peakHold.read()); 152 | TEST_ASSERT(peakHold.size() == (end - start)); 153 | }; 154 | 155 | while (1) { 156 | int newEnd = test.randomInt(end, start + maxLength); 157 | if (newEnd >= int(signal.size())) break; 158 | while (end < newEnd) { 159 | peakHold.push(signal[end]); 160 | ++end; 161 | check(); 162 | } 163 | 164 | int newStart = test.randomInt(start, end - 1); 165 | while (start < newStart) { 166 | peakHold.pop(); 167 | ++start; 168 | check(); 169 | } 170 | } 171 | 172 | // Back expands when you resize, including older samples 173 | for (int repeat = 0; repeat < 50; ++repeat) { 174 | int newLength = test.randomInt(0, maxLength); 175 | start = end - newLength; 176 | if (repeat%2) { 177 | peakHold.set(newLength); 178 | } else { 179 | // Defaults to true, so this is identical 180 | peakHold.set(newLength, false); 181 | } 182 | check(); 183 | } 184 | 185 | peakHold.set(50, false); 186 | float max50 = peakHold.read(); 187 | // Preserve current peak values 188 | peakHold.set(80, true); 189 | TEST_ASSERT(peakHold.size() == 80); 190 | TEST_ASSERT(peakHold.read() == max50); 191 | 192 | peakHold.set(40); 193 | start = end - 40; 194 | check(); 195 | } 196 | 197 | TEST("Peak hold (boundary bug)") { 198 | signalsmith::envelopes::PeakHold peakHold(200); 199 | peakHold.set(0); 200 | 201 | peakHold.push(2); 202 | for (int i = 1; i < 10; ++i) { 203 | peakHold.push(1); 204 | } 205 | TEST_ASSERT(peakHold.read() == 2); 206 | peakHold.pop(); 207 | TEST_ASSERT(peakHold.read() == 1); 208 | } 209 | 210 | // TODO: test that expanding size re-includes previous values 211 | 212 | //TEST("Peak hold (overflow)") { 213 | // int maxLength = 200; 214 | // signalsmith::envelopes::PeakHold peakHold(maxLength); 215 | // 216 | // std::vector signal(5000); 217 | // for (auto &v : signal) { 218 | // v = test.random(-1, 1); 219 | // } 220 | // 221 | // // Enough random data to overflow an `int` index 222 | // for (int r = 0; r < 4; ++r) { 223 | // LOG_EXPR(r); 224 | // int intMax = std::numeric_limits::max(); 225 | // for (int i = 0; i < intMax - 10; ++i) { 226 | // peakHold(test.random(0, 1)); 227 | // if (i%10000000 == 0) { 228 | // LOG_EXPR(i); 229 | // LOG_EXPR(i/float(intMax)); 230 | // } 231 | // } 232 | // } 233 | // 234 | // peakHold.set(0); 235 | // int start = 0, end = 0; 236 | // auto check = [&]() { 237 | // float expected = std::numeric_limits::lowest(); 238 | // for (int i = start; i < end; ++i) { 239 | // expected = std::max(expected, signal[i]); 240 | // } 241 | // TEST_ASSERT(expected == peakHold.read()); 242 | // TEST_ASSERT(peakHold.size() == (end - start)); 243 | // }; 244 | // 245 | // while (1) { 246 | // int newEnd = test.randomInt(end, start + maxLength); 247 | // if (newEnd >= int(signal.size())) break; 248 | // while (end < newEnd) { 249 | // peakHold.push(signal[end]); 250 | // ++end; 251 | // check(); 252 | // } 253 | // 254 | // int newStart = test.randomInt(start, end - 1); 255 | // while (start < newStart) { 256 | // peakHold.pop(); 257 | // ++start; 258 | // check(); 259 | // } 260 | // } 261 | //} 262 | -------------------------------------------------------------------------------- /tests/envelopes/04-peak-decay.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../common.h" 3 | 4 | #include 5 | #include 6 | 7 | #include "envelopes.h" 8 | 9 | TEST("Peak decay linear (example)") { 10 | int length = 250; 11 | signalsmith::envelopes::CubicLfo lfo(12345); 12 | lfo.set(0, 1, 0.05, 2, 1); 13 | 14 | signalsmith::envelopes::PeakDecayLinear decayA(20), decayB(40); 15 | 16 | CsvWriter csv("peak-decay-linear"); 17 | csv.line("i", "signal", "decay (20)", "decay (40)"); 18 | for (int i = 0; i < length; ++i) { 19 | double v = lfo.next(); 20 | v = std::max(0, 2*v - 1); 21 | v = v*v*10; 22 | if (i < 90) v += 3; 23 | if (i >= 90 && i < 100) { 24 | v += 3*(1 - (i - 90)*0.1); 25 | } 26 | csv.line(i, v, decayA(v), decayB(v)); 27 | } 28 | return test.pass(); 29 | } 30 | 31 | TEST("Peak decay linear cascade (example)") { 32 | int length = 250; 33 | signalsmith::envelopes::CubicLfo lfo(12345); 34 | lfo.set(0, 1, 0.05, 2, 1); 35 | 36 | signalsmith::envelopes::PeakDecayLinear decayA(40), decayB(30), decayC(10), decayD(10), decayE(30); 37 | 38 | CsvWriter csv("peak-decay-linear-cascade"); 39 | csv.line("i", "signal", "decay (40)", "decay (30-10)", "decay (10-30)"); 40 | for (int i = 0; i < length; ++i) { 41 | double v = lfo.next(); 42 | v = std::max(0, 2*v - 1); 43 | v = v*v*10; 44 | if (i < 90) v += 3; 45 | if (i >= 90 && i < 100) { 46 | v += 3*(1 - (i - 90)*0.1); 47 | } 48 | csv.line(i, v, decayA(v), decayC(decayB(v)), decayE(decayD(v))); 49 | } 50 | return test.pass(); 51 | } 52 | 53 | -------------------------------------------------------------------------------- /tests/envelopes/box-stack.py: -------------------------------------------------------------------------------- 1 | import article 2 | import numpy 3 | from numpy import fft 4 | 5 | stepFig, stepAxes = article.small() 6 | figure = article.wideFigure(1, 5, False); 7 | timeAxes = figure.gridPlot((0, 0), (1, 2)) 8 | #stepAxes = figure.gridPlot((1, 0)) 9 | freqAxes = figure.gridPlot((0, 2), (1, 3)); 10 | 11 | columns, data = article.readCsv("box-stack-long-time.csv") 12 | for i in range(1, len(columns)): 13 | timeAxes.plot(data[0]*1.0/len(data[0]), data[i]*len(data[0]), label="%s layers"%columns[i]); 14 | stepAxes.plot(data[0]*1.0/len(data[0]), numpy.cumsum(data[i])); 15 | timeAxes.set(xlabel="time", ylabel="impulse response (scaled)", xticks=[0, 1], yticks=[0, 1, 2, 3]) 16 | stepAxes.set(xlabel="time", ylabel="step response (scaled)") 17 | 18 | columns, data = article.readCsv("box-stack-long-freq.csv") 19 | for i in range(1, len(columns)): 20 | freqAxes.plot(data[0], data[i]); 21 | freqAxes.set(xlim=[0, 10], ylim=[-120, 5], ylabel="dB", xlabel="frequency (relative to filter length)") 22 | figure.save("box-stack-long.svg", legend_loc="upper right") 23 | stepFig.save("box-stack-long-step.svg") 24 | 25 | ## 26 | 27 | columns, data = article.readCsv("box-stack-short-freq.csv") 28 | 29 | figure, axes = article.small(); 30 | for i in range(1, len(columns)): 31 | axes.plot(data[0], data[i], label="layers = %s"%columns[i]); 32 | axes.set(xlim=[0, 10], ylim=[-120, 5], ylabel="dB", xlabel="frequency (relative to filter length)") 33 | figure.save("box-stack-short-freq.svg", legend_loc="upper right") 34 | 35 | ## 36 | 37 | columns, data = article.readCsv("box-stack-stats.csv") 38 | 39 | figure, axes = article.small(); 40 | axes.scatter(data[0], data[1], label="heuristic", color=article.colors[0]); 41 | axes.set(xlim=[0, 10.2], ylim=[0, 18]); 42 | figure.save("box-stack-bandwidth.svg") 43 | 44 | figure, axes = article.small(); 45 | axes.scatter(data[0], data[2], label="heuristic", color=article.colors[1]); 46 | axes.set(xlim=[0, 10.2], ylim=[-200, 5]); 47 | figure.save("box-stack-peak.svg") 48 | 49 | -------------------------------------------------------------------------------- /tests/envelopes/cubic-lfo.py: -------------------------------------------------------------------------------- 1 | import article 2 | import numpy 3 | from numpy import fft 4 | 5 | columns, data = article.readCsv("cubic-lfo-example.csv") 6 | 7 | figure, axes = article.medium(); 8 | for i in range(1, len(columns)): 9 | axes.plot(data[0], data[i], label=columns[i]); 10 | figure.save("cubic-lfo-example.svg", legend_loc="upper center") 11 | 12 | #### 13 | 14 | columns, freqData = article.readCsv("cubic-lfo-spectrum-freq.csv") 15 | columns, depthData = article.readCsv("cubic-lfo-spectrum-depth.csv") 16 | 17 | def energyToDb(energy): 18 | db = 10*numpy.log10(energy + 1e-100) 19 | db -= max(db) 20 | return db; 21 | 22 | figure, (freqAxes, depthAxes) = article.short(1, 2); 23 | for i in range(1, len(columns)): 24 | freqAxes.plot(freqData[0], energyToDb(freqData[i])); 25 | depthAxes.plot(depthData[0], energyToDb(depthData[i]), label=columns[i]); 26 | freqAxes.set(title="frequency variation", ylim=[-65, 5], ylabel="dB (normalised)", xlabel="relative frequency", xlim=[0, 2]) 27 | depthAxes.set(title="depth variation", ylim=[-65, 5], ylabel="dB (normalised)", xlabel="relative frequency", xlim=[0, 2]) 28 | figure.save("cubic-lfo-spectrum.svg", legend_loc="lower left") 29 | 30 | figure, axes = article.small(); 31 | for i in [1]: 32 | axes.plot(freqData[0], energyToDb(freqData[1]), label=columns[i]); 33 | axes.set(ylim=[-80, 5], ylabel="dB (normalised)", xlabel="frequency (relative to LFO speed)", xlim=[0, 10], xticks=range(1, 10, 2)) 34 | figure.save("cubic-lfo-spectrum-pure.svg") 35 | 36 | ### 37 | 38 | columns, data = article.readCsv("cubic-lfo-changes.csv") 39 | 40 | figure, axes = article.medium(); 41 | axes.fill_between(data[0], data[3], data[4], alpha=0.1, color=article.colors[0]) 42 | axes.plot(data[0], data[1], label="regular"); 43 | axes.plot(data[0], data[2], label="random"); 44 | axes.set(yticks=range(-2, 3), xlim=[0, max(data[0])], ylabel="LFO output", xlabel="samples"); 45 | figure.save("cubic-lfo-changes.svg", legend_loc="lower center") 46 | 47 | #### 48 | -------------------------------------------------------------------------------- /tests/envelopes/peak-hold.py: -------------------------------------------------------------------------------- 1 | import article 2 | import numpy 3 | from numpy import fft 4 | 5 | figure, axes = article.short(); 6 | 7 | columns, data = article.readCsv("peak-hold.csv") 8 | for i in range(1, len(columns)): 9 | axes.plot(data[0], data[i], label=columns[i]); 10 | axes.set(xlabel="time") 11 | figure.save("peak-hold.svg") 12 | -------------------------------------------------------------------------------- /tests/envelopes/plain-plots.py: -------------------------------------------------------------------------------- 1 | import article 2 | import numpy 3 | from numpy import fft 4 | 5 | def plainPlot(name, legend_loc="best", ylim=[None, None]): 6 | figure, axes = article.short(); 7 | columns, data = article.readCsv("%s.csv"%name) 8 | for i in range(1, len(columns)): 9 | axes.plot(data[0], data[i], label=columns[i]); 10 | axes.set(xlabel="time", ylim=ylim) 11 | figure.save("%s.svg"%name, legend_loc=legend_loc) 12 | 13 | plainPlot("box-filter-example") 14 | plainPlot("peak-decay-linear", legend_loc="upper center", ylim=[0,10]) 15 | plainPlot("peak-decay-linear-cascade", legend_loc="upper center", ylim=[0,10]) 16 | -------------------------------------------------------------------------------- /tests/fft/00-fft.py: -------------------------------------------------------------------------------- 1 | import article 2 | import numpy 3 | import matplotlib 4 | 5 | def ampToDb(amp): 6 | return 20*numpy.log10(numpy.abs(amp) + 1e-100) 7 | 8 | figure, (rmsAxes, peakAxes) = article.wide(1, 2) 9 | rmsAxes.set(title="RMS") 10 | peakAxes.set(title="Peak") 11 | 12 | def plotType(name, axes, labels=True): 13 | columns, data = article.readCsv("fft-errors-%s.csv"%name) 14 | 15 | for i in range(1, len(data)): 16 | label = columns[i] if labels else None 17 | axes.plot(data[0], ampToDb(data[i]), label=label) 18 | 19 | axes.set(ylabel="aliasing (dB)", xlabel="FFT size", xscale='log', ylim=[-350, 0]) 20 | 21 | axes.set_xticks([2, 16, 128, 1024, 8192, 65536]) 22 | axes.set_xlim([2, None]) 23 | axes.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter()) 24 | axes.get_xaxis().set_tick_params(which='minor', size=0) 25 | axes.get_xaxis().set_tick_params(which='minor', width=0) 26 | 27 | plotType("rms", rmsAxes, True) 28 | plotType("peak", peakAxes, False) 29 | 30 | figure.save("fft-errors.svg", legend_loc='center') 31 | -------------------------------------------------------------------------------- /tests/fft/fft-errors-numpy.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | sizes = [] 4 | errorRms = [] 5 | errorPeak = [] 6 | 7 | def testSize(size): 8 | harmonics = list(range(size)) 9 | if size > 100: 10 | harmonics = numpy.random.choice(harmonics, 20) 11 | 12 | errorSum2 = 0 13 | expectedSum2 = 0 14 | peak = 0 15 | 16 | for harmonic in harmonics: 17 | phases = ((numpy.arange(size)*harmonic)%size)*2*numpy.pi/size 18 | input = numpy.cos(phases) + 1j*numpy.sin(phases) 19 | spectrum = numpy.fft.fft(input) 20 | 21 | expected = numpy.zeros(size) 22 | expected[harmonic] = size 23 | diff = spectrum - expected 24 | 25 | errorSum2 += numpy.average(numpy.abs(diff)**2) 26 | expectedSum2 += numpy.average(numpy.abs(expected)**2) 27 | peak = max(peak, max(numpy.abs(diff))) 28 | 29 | sizes.append(size) 30 | eRms = (errorSum2/len(harmonics))**0.5 31 | xRms = (expectedSum2/len(harmonics))**0.5 32 | errorRms.append(eRms/xRms) 33 | errorPeak.append(peak/xRms) 34 | 35 | for size in range(1, 16): 36 | testSize(size) 37 | 38 | size = 16 39 | while size <= 2**16: 40 | testSize(size) 41 | testSize(size*5/4) 42 | testSize(size*3/2) 43 | size *= 2 44 | 45 | #### Plot the results 46 | 47 | import matplotlib 48 | import article 49 | 50 | def ampToDb(amp): 51 | return 20*numpy.log10(numpy.abs(amp) + 1e-100) 52 | 53 | figure, axes = article.medium() 54 | 55 | axes.plot(sizes, ampToDb(errorPeak), label="peak") 56 | axes.plot(sizes, ampToDb(errorRms), label="RMS") 57 | 58 | axes.set(ylabel="aliasing (dB)", xlabel="FFT size", xscale='log', ylim=[-350, 0]) 59 | 60 | axes.set_xticks([2, 16, 128, 1024, 8192, 65536]) 61 | axes.set_xlim([2, None]) 62 | axes.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter()) 63 | axes.get_xaxis().set_tick_params(which='minor', size=0) 64 | axes.get_xaxis().set_tick_params(which='minor', width=0) 65 | 66 | figure.save("fft-errors-numpy.svg") 67 | 68 | -------------------------------------------------------------------------------- /tests/filters/00-highpass-lowpass.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include "fft.h" 7 | #include "../common.h" 8 | 9 | #include "./filter-tests.h" 10 | 11 | template 12 | void testReset(Test &&test) { 13 | signalsmith::filters::BiquadStatic filter; 14 | filter.lowpass(0.25); 15 | 16 | constexpr int length = 10; 17 | std::vector outA(length), outB(length), outNoReset(length); 18 | 19 | filter(1e10); 20 | for (int i = 0; i < length; ++i) { 21 | outNoReset[i] = filter(i); 22 | } 23 | filter.reset(); 24 | for (int i = 0; i < length; ++i) { 25 | outA[i] = filter(i); 26 | } 27 | filter(1e10); 28 | filter.reset(); 29 | for (int i = 0; i < length; ++i) { 30 | outB[i] = filter(i); 31 | } 32 | 33 | if (outA != outB) return test.fail("Reset didn't clear properly"); 34 | if (outA == outNoReset) return test.fail("something weird's going on"); 35 | } 36 | TEST("Filter reset") { 37 | testReset(test.prefix("double")); 38 | testReset(test.prefix("float")); 39 | } 40 | 41 | // Should be Butterworth when we don't specify a bandwidth 42 | template 43 | void testButterworth(Test &&test, double freq, signalsmith::filters::BiquadDesign design=signalsmith::filters::BiquadDesign::bilinear) { 44 | signalsmith::filters::BiquadStatic filter; 45 | 46 | double zeroIsh = 1e-5; // -100dB 47 | std::complex one = 1; 48 | bool isBilinear = (design == signalsmith::filters::BiquadDesign::bilinear); 49 | 50 | { 51 | filter.lowpass(freq, design); 52 | auto spectrum = getSpectrum(filter); 53 | int nyquistIndex = (int)spectrum.size()/2; 54 | 55 | if (std::abs(spectrum[0] - one) > zeroIsh) return test.fail("1 at 0: ", spectrum[0]); 56 | 57 | // -3dB at critical point 58 | double criticalDb = ampToDb(interpSpectrum(spectrum, freq)); 59 | double expectedDb = ampToDb(std::sqrt(0.5)); 60 | double difference = std::abs(criticalDb - expectedDb); 61 | if (isBilinear || freq < 0.25) { 62 | if (difference > 0.001) { 63 | writeSpectrum(spectrum, "fail-butterworth-spectrum"); 64 | test.log(criticalDb, " != ", expectedDb); 65 | return test.fail("Butterworth critical frequency (lowpass)"); 66 | } 67 | } else { 68 | // Slightly looser limits 69 | if (difference > 0.01) return test.fail("Butterworth critical frequency (lowpass)"); 70 | } 71 | if (isBilinear) { 72 | if (std::abs(spectrum[nyquistIndex]) > zeroIsh) return test.fail("0 at Nyquist: ", spectrum[nyquistIndex]); 73 | } 74 | 75 | if (!isMonotonic(spectrum, -1)) { 76 | writeSpectrum(spectrum, "fail-butterworth-spectrum"); 77 | return test.fail("lowpass not monotonic"); 78 | } 79 | } 80 | 81 | { 82 | filter.highpass(freq, design); 83 | auto spectrum = getSpectrum(filter); 84 | int nyquistIndex = (int)spectrum.size()/2; 85 | 86 | // -3dB at critical point 87 | double criticalDb = ampToDb(interpSpectrum(spectrum, freq)); 88 | double expectedDb = ampToDb(std::sqrt(0.5)); 89 | double difference = std::abs(criticalDb - expectedDb); 90 | if (isBilinear || freq < 0.1) { 91 | if (difference > 0.001) return test.fail("Butterworth critical frequency (highpass)"); 92 | } else { 93 | // Slightly looser limits 94 | if (difference > 0.01) return test.fail("Butterworth critical frequency (highpass)"); 95 | } 96 | if (std::abs(spectrum[0]) > zeroIsh) return test.fail("0 at 0: ", spectrum[0]); 97 | if (isBilinear) { 98 | if (std::abs(spectrum[nyquistIndex] - one) > zeroIsh) return test.fail("1 at Nyquist: ", spectrum[nyquistIndex]); 99 | } 100 | 101 | if (!isMonotonic(spectrum, 1)) { 102 | writeSpectrum(spectrum, "fail-butterworth-spectrum"); 103 | return test.fail("highpass not monotonic"); 104 | } 105 | } 106 | 107 | // Wider bandwidth is just softer, still monotonic 108 | { 109 | filter.lowpass(freq, 1.91, design); 110 | auto spectrum = getSpectrum(filter); 111 | if (!isMonotonic(spectrum, -1)) { 112 | writeSpectrum(spectrum, "fail-butterworth-spectrum"); 113 | return test.fail("lowpass octave=1.91 should still be monotonic"); 114 | } 115 | } 116 | { 117 | filter.highpass(freq, 1.91, design); 118 | auto spectrum = getSpectrum(filter); 119 | if (!isMonotonic(spectrum, 1)) { 120 | writeSpectrum(spectrum, "fail-butterworth-spectrum"); 121 | return test.fail("highpass octave=1.91 should still be monotonic"); 122 | } 123 | } 124 | // Narrower bandwidth has a slight bump 125 | if (isBilinear || freq < 0.1) { 126 | filter.lowpass(freq, 1.89, design); 127 | auto spectrum = getSpectrum(filter); 128 | if (isMonotonic(spectrum, -1)) { 129 | writeSpectrum(spectrum, "fail-butterworth-spectrum"); 130 | return test.fail("lowpass octave=1.89 should not be monotonic"); 131 | } 132 | } 133 | if (isBilinear || freq < 0.01) { 134 | filter.highpass(freq, 1.89, design); 135 | auto spectrum = getSpectrum(filter); 136 | if (isMonotonic(spectrum, 1)) { 137 | writeSpectrum(spectrum, "fail-butterworth-spectrum"); 138 | return test.fail("highpass octave=1.89 should not be monotonic"); 139 | } 140 | } 141 | } 142 | 143 | TEST("Butterworth filters") { 144 | for (int i = 0; test.success && i < 10; ++i) { 145 | double f = test.random(0.02, 0.48); 146 | std::string n = std::to_string(f); 147 | testButterworth(test.prefix("double@" + n), f); 148 | if (test.success) testButterworth(test.prefix("float@" + n), f); 149 | 150 | signalsmith::filters::BiquadDesign design = signalsmith::filters::BiquadDesign::vicanek; 151 | if (test.success) testButterworth(test.prefix("double-vicanek@" + n), f, design); 152 | if (test.success) testButterworth(test.prefix("float-vicanek@" + n), f, design); 153 | } 154 | } 155 | 156 | -------------------------------------------------------------------------------- /tests/filters/01-bandpass.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include "fft.h" 7 | #include "../common.h" 8 | 9 | #include "./filter-tests.h" 10 | 11 | template 12 | void testBandpass(Test &&test, double freq, double octaves=1.2) { 13 | signalsmith::filters::BiquadStatic filter; 14 | 15 | filter.bandpass(freq, octaves, true); 16 | Spectrum spectrum = getSpectrum(filter); 17 | 18 | double criticalDb = ampToDb(interpSpectrum(spectrum, freq)); 19 | if (std::abs(criticalDb) > 0.001) { 20 | test.log(criticalDb); 21 | return test.fail("0 at critical"); 22 | } 23 | if (std::abs(spectrum[0]) > 0.001) { 24 | writeSpectrum(spectrum, "fail-spectrum"); 25 | return test.fail("spectrum[0] == 0: ", spectrum[0]); 26 | } 27 | int nyquistIndex = spectrum.size()/2; 28 | if (std::abs(spectrum[nyquistIndex]) > 0.001) { 29 | writeSpectrum(spectrum, "fail-spectrum"); 30 | return test.fail("spectrum[Nyquist] == 0: ", spectrum[0]); 31 | } 32 | if (!isMonotonic(spectrum, 0, freq)) { 33 | writeSpectrum(spectrum, "fail-spectrum"); 34 | return test.fail("monotonic below"); 35 | } 36 | if (!isMonotonic(spectrum, 0.5, freq)) { 37 | writeSpectrum(spectrum, "fail-spectrum"); 38 | return test.fail("monotonic above"); 39 | } 40 | 41 | double bandLowFreq = freq*std::pow(0.5, octaves/2); 42 | double db = ampToDb(interpSpectrum(spectrum, bandLowFreq, -1)); 43 | if (db > -3.01029995664) { 44 | writeSpectrum(spectrum, "fail-spectrum"); 45 | return test.fail("lower bandwidth boundary: ", db, " below f=", bandLowFreq); 46 | } 47 | db = ampToDb(interpSpectrum(spectrum, bandLowFreq, 1)); 48 | if (db < -3.01029995664) { 49 | writeSpectrum(spectrum, "fail-spectrum"); 50 | return test.fail("lower bandwidth boundary: ", db, " above f=", bandLowFreq); 51 | } 52 | } 53 | 54 | TEST("Bandpass") { 55 | // testBandpass(test.prefix("double@0.05"), 0.05); 56 | // if (test.success) testBandpass(test.prefix("float@0.05"), 0.05); 57 | if (test.success) testBandpass(test.prefix("double@0.2"), 0.2); 58 | if (test.success) testBandpass(test.prefix("float@0.2"), 0.2); 59 | } 60 | -------------------------------------------------------------------------------- /tests/filters/02-responses.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include "fft.h" 7 | #include "../common.h" 8 | 9 | #include "./filter-tests.h" 10 | 11 | template 12 | void testResponse(Test &test, Filter &filter, double accuracy=1e-6) { 13 | auto spectrum = getSpectrum(filter); 14 | 15 | for (int r = 0; r < 10; ++r) { 16 | int i = int(std::floor(test.random(0, spectrum.size()/2))); 17 | double f = i*1.0/spectrum.size(); 18 | std::complex predicted = filter.response(f); 19 | std::complex actual = spectrum[i]; 20 | 21 | TEST_ASSERT(std::abs(predicted - actual) < accuracy) 22 | 23 | double dbPredicted = filter.responseDb(f); 24 | double dbActual = 10*std::log10(std::norm(actual)); 25 | if (dbPredicted > -100) { 26 | TEST_ASSERT(std::abs(dbPredicted - dbActual) < accuracy) 27 | } 28 | } 29 | } 30 | 31 | TEST("Responses") { 32 | signalsmith::filters::BiquadStatic filter; 33 | if (test.success) testResponse(test, filter); 34 | 35 | filter.lowpass(test.random(0.01, 0.49), test.random(0.5, 4)); 36 | if (test.success) testResponse(test, filter); 37 | 38 | filter.highpass(test.random(0.01, 0.49), test.random(0.5, 4)); 39 | if (test.success) testResponse(test, filter); 40 | 41 | filter.bandpass(test.random(0.01, 0.49), test.random(0.5, 4)); 42 | if (test.success) testResponse(test, filter); 43 | 44 | filter.notch(test.random(0.01, 0.49), test.random(0.5, 4)); 45 | if (test.success) testResponse(test, filter); 46 | 47 | filter.peak(test.random(0.01, 0.49), test.random(0.25, 4), test.random(0.5, 4)); 48 | if (test.success) testResponse(test, filter); 49 | 50 | filter.highShelf(test.random(0.01, 0.49), test.random(0.25, 4), test.random(0.5, 4)); 51 | if (test.success) testResponse(test, filter); 52 | 53 | filter.lowShelf(test.random(0.01, 0.49), test.random(0.25, 4), test.random(0.5, 4)); 54 | if (test.success) testResponse(test, filter); 55 | } 56 | -------------------------------------------------------------------------------- /tests/filters/03-gain.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include "../common.h" 7 | 8 | TEST("Gain") { 9 | signalsmith::filters::BiquadStatic filter, filter2; 10 | float accuracy = 1e-4; 11 | 12 | for (int t = 0; t < 6; ++t) { 13 | if (t == 0) { 14 | filter.lowpass(test.random(0, 0.5), test.random(4, 12)); 15 | } else if (t == 1) { 16 | filter.highpass(test.random(0, 0.5), test.random(4, 12)); 17 | } else if (t == 2) { 18 | filter.notch(test.random(0, 0.5), test.random(4, 12)); 19 | } else { 20 | filter.bandpass(test.random(0, 0.5), test.random(0.1, 10), test.random(4, 12)); 21 | } 22 | 23 | double gain = test.random(0.1, 5); 24 | filter2 = filter; 25 | filter2.addGain(gain); 26 | 27 | for (int i = 0; i < 10; ++i) { 28 | float f = test.random(0, 0.5); 29 | TEST_APPROX(std::abs(filter.response(f))*gain, std::abs(filter2.response(f)), accuracy); 30 | } 31 | } 32 | } 33 | 34 | TEST("Gain") { 35 | signalsmith::filters::BiquadStatic filter, filter2; 36 | for (int t = 0; t < 6; ++t) { 37 | if (t == 0) { 38 | filter.lowpass(test.random(0, 0.5), test.random(4, 12)); 39 | } else if (t == 1) { 40 | filter.highpass(test.random(0, 0.5), test.random(4, 12)); 41 | } else if (t == 2) { 42 | filter.notch(test.random(0, 0.5), test.random(4, 12)); 43 | } else { 44 | filter.bandpass(test.random(0, 0.5), test.random(0.1, 10), test.random(4, 12)); 45 | } 46 | 47 | double gain = test.random(-30, 30); 48 | filter2 = filter; 49 | filter2.addGainDb(gain); 50 | 51 | for (int i = 0; i < 10; ++i) { 52 | float f = test.random(0, 0.5); 53 | auto expected = filter.responseDb(f) + gain; 54 | auto actual = filter2.responseDb(f); 55 | if (expected > -60) { 56 | TEST_APPROX(expected, actual, 0.01); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/filters/04-allpass.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include "fft.h" 7 | #include "../common.h" 8 | 9 | template 10 | void testAllpass(Test &&test, double freq, double octaves=1.2) { 11 | signalsmith::filters::BiquadStatic filter; 12 | 13 | auto design = (test.randomInt(0, 1) == 0) 14 | ? signalsmith::filters::BiquadDesign::bilinear 15 | : signalsmith::filters::BiquadDesign::oneSided; 16 | filter.allpass(freq, octaves, design); 17 | 18 | for (int r = 0; r < 100; ++r) { 19 | double f = test.random(0, 0.5); 20 | auto db = filter.responseDb(f); 21 | TEST_APPROX(db, 0, 0.01); 22 | } 23 | 24 | // Phase is 180deg at critical freq 25 | auto critical = filter.response(freq); 26 | double accuracy = (freq >= 0.05 && freq < 0.45) ? 0.001 : 0.01; 27 | TEST_APPROX(filter.responseDb(freq), 0, 0.01); 28 | TEST_APPROX(critical, Sample(-1), accuracy); 29 | 30 | if (freq < 0.1 || design == signalsmith::filters::BiquadDesign::oneSided) { 31 | double accuracy = (freq >= 0.01) ? 0.1 : 0.01; 32 | auto lowFreq = freq*std::pow(0.5, octaves*0.5); 33 | std::complex expected{0, -1}; 34 | auto actual = filter.response(lowFreq); 35 | TEST_APPROX(actual, expected, accuracy); 36 | } 37 | auto highFreq = freq*std::pow(2, octaves*0.5); 38 | if (highFreq < 0.1) { 39 | double accuracy = (freq >= 0.01) ? 0.1 : 0.01; 40 | std::complex expected{0, 1}; 41 | auto actual = filter.response(highFreq); 42 | TEST_APPROX(actual, expected, accuracy); 43 | } 44 | } 45 | 46 | TEST("Allpass") { 47 | for (int r = 0; r < 1000; ++r) { 48 | double freq = test.random(0.001, 0.49); 49 | if (test.success) testAllpass(test.prefix("double@" + std::to_string(freq)), freq); 50 | if (test.success) testAllpass(test.prefix("float@" + std::to_string(freq)), freq); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/filters/05-shelves.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include "../common.h" 7 | 8 | TEST("Shelf (bandwidth)") { 9 | using Filter = signalsmith::filters::BiquadStatic; 10 | for (int r = 0; r < 100; ++r) { 11 | double freq = test.random(0.001, 0.49); 12 | double db = test.random(-10, 10); 13 | double octaves = test.random(0.5, 3); 14 | 15 | Filter highShelf, lowShelf; 16 | highShelf.highShelfDb(freq, db, octaves); 17 | lowShelf.lowShelfDb(freq, db, octaves); 18 | 19 | TEST_APPROX(highShelf.responseDb(0), 0, 0.01); 20 | TEST_APPROX(lowShelf.responseDb(0), db, 0.01); 21 | TEST_APPROX(highShelf.responseDb(freq), db/2, 0.01); 22 | TEST_APPROX(lowShelf.responseDb(freq), db/2, 0.01); 23 | TEST_APPROX(highShelf.responseDb(0.5), db, 0.01); 24 | TEST_APPROX(lowShelf.responseDb(0.5), 0, 0.01); 25 | } 26 | } 27 | 28 | TEST("Shelf (max slope)") { 29 | using Filter = signalsmith::filters::BiquadStatic; 30 | using Design = signalsmith::filters::BiquadDesign; 31 | for (int r = 0; r < 100; ++r) { 32 | double freq = test.random(0.01, 0.49); 33 | double db = test.random(-10, 10); 34 | while (std::abs(db) < 0.1) db = test.random(-10, 10); 35 | 36 | auto isMonotonic = [&](const Filter &filter, int direction) -> bool { 37 | double accuracy = 1e-6; 38 | double min = filter.responseDb(0), max = min; 39 | for (int i = 0; i < 1000; ++i) { 40 | double f = (i + test.random(0, 1))/2000; 41 | double rDb = filter.responseDb(f); 42 | 43 | if (direction > 0) { 44 | if (rDb < min - accuracy) { 45 | return false; 46 | } 47 | } else { 48 | if (rDb > max + accuracy) { 49 | return false; 50 | } 51 | } 52 | min = std::min(min, rDb); 53 | max = std::max(min, rDb); 54 | } 55 | return true; 56 | }; 57 | 58 | Filter highShelf, lowShelf; 59 | 60 | // Monotonic with default values 61 | highShelf.highShelfDb(freq, db, highShelf.defaultBandwidth, Design::bilinear); 62 | lowShelf.lowShelfDbQ(freq, db, lowShelf.defaultQ, Design::bilinear); 63 | if (db > 0) { 64 | TEST_ASSERT(isMonotonic(highShelf, 1)); 65 | TEST_ASSERT(isMonotonic(lowShelf, -1)); 66 | } else { 67 | TEST_ASSERT(isMonotonic(highShelf, -1)); 68 | TEST_ASSERT(isMonotonic(lowShelf, 1)); 69 | } 70 | 71 | // Loosening bandwidth is still monotonic 72 | highShelf.highShelfDb(freq, db, highShelf.defaultBandwidth*1.05, Design::bilinear); 73 | lowShelf.lowShelfDbQ(freq, db, lowShelf.defaultQ*0.95, Design::bilinear); 74 | if (db > 0) { 75 | TEST_ASSERT(isMonotonic(highShelf, 1)); 76 | TEST_ASSERT(isMonotonic(lowShelf, -1)); 77 | } else { 78 | TEST_ASSERT(isMonotonic(highShelf, -1)); 79 | TEST_ASSERT(isMonotonic(lowShelf, 1)); 80 | } 81 | 82 | // Narrower bandwidth / higher Q makes it overshoot 83 | highShelf.highShelfDb(freq, db, highShelf.defaultBandwidth*0.95, Design::bilinear); 84 | lowShelf.lowShelfDbQ(freq, db, lowShelf.defaultQ*1.05, Design::bilinear); 85 | if (db > 0) { 86 | TEST_ASSERT(!isMonotonic(highShelf, 1)); 87 | TEST_ASSERT(!isMonotonic(lowShelf, -1)); 88 | } else { 89 | TEST_ASSERT(!isMonotonic(highShelf, -1)); 90 | Plot2D plot(600, 400); 91 | auto &line = plot.line(); 92 | for (double f = 0; f < 0.5; f += 0.001) { 93 | line.add(f, lowShelf.responseDb(f)); 94 | } 95 | plot.write("tmp.svg"); 96 | TEST_ASSERT(!isMonotonic(lowShelf, 1)); 97 | } 98 | 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /tests/filters/06-spec-equivalence.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include 7 | 8 | TEST("Filter dB/Q methods") { 9 | using Filter = signalsmith::filters::BiquadStatic; 10 | using Design = signalsmith::filters::BiquadDesign; 11 | 12 | auto testDesign = [&](Design design, std::string name) { 13 | Test outerTest = test.prefix(name); 14 | 15 | #define FILTER_METHOD_Q(METHOD) \ 16 | { \ 17 | Test test = outerTest.prefix(#METHOD); \ 18 | double freq = test.random(0, 0.5); \ 19 | double octaves = test.random(0.1, 4); \ 20 | double q = 0.5/std::sinh(octaves*std::log(2)/2); \ 21 | Filter filter, filterQ; \ 22 | filter.METHOD(freq, octaves, design); \ 23 | filterQ.METHOD##Q(freq, q, design); \ 24 | for (int r = 0; r < 10; ++r) { \ 25 | double f = test.random(0, 0.5); \ 26 | double fDb = filter.responseDb(f); \ 27 | double fqDb = filterQ.responseDb(f); \ 28 | if (fDb > -60 && fqDb > -60) { \ 29 | TEST_APPROX(fDb, fqDb, 0.01); \ 30 | } \ 31 | } \ 32 | } 33 | FILTER_METHOD_Q(lowpass); 34 | FILTER_METHOD_Q(highpass); 35 | FILTER_METHOD_Q(bandpass); 36 | FILTER_METHOD_Q(notch); 37 | FILTER_METHOD_Q(allpass); 38 | #undef FILTER_METHOD_Q 39 | 40 | #define FILTER_METHOD_DB_Q(METHOD) \ 41 | { \ 42 | Test test = outerTest.prefix(#METHOD); \ 43 | double freq = test.random(0, 0.5); \ 44 | double octaves = test.random(0.1, 4); \ 45 | double q = 0.5/std::sinh(octaves*std::log(2)/2); \ 46 | double db = test.random(-30, 30); \ 47 | double gain = std::pow(10, db*0.05); \ 48 | Filter filter, filterQ, filterDb, filterDbQ; \ 49 | filter.METHOD(freq, gain, octaves, design); \ 50 | filterQ.METHOD##Q(freq, gain, q, design); \ 51 | filterDb.METHOD##Db(freq, db, octaves, design); \ 52 | filterDbQ.METHOD##DbQ(freq, db, q, design); \ 53 | for (int r = 0; r < 10; ++r) { \ 54 | double f = test.random(0, 0.5); \ 55 | double fDb = filter.responseDb(f); \ 56 | double fqDb = filterQ.responseDb(f); \ 57 | double fDbDb = filterDb.responseDb(f); \ 58 | double fDbQDb = filterDbQ.responseDb(f); \ 59 | if (fDb > -60 && fqDb > -60) { \ 60 | TEST_APPROX(fDb, fqDb, 0.01); \ 61 | TEST_APPROX(fDb, fDbDb, 0.01); \ 62 | TEST_APPROX(fDb, fDbQDb, 0.01); \ 63 | } \ 64 | } \ 65 | } 66 | FILTER_METHOD_DB_Q(peak); 67 | FILTER_METHOD_DB_Q(highShelf); 68 | FILTER_METHOD_DB_Q(lowShelf); 69 | #undef FILTER_METHOD_DB_Q 70 | }; 71 | 72 | testDesign(Design::bilinear, "bilinear"); 73 | // We don't test "cookbook" because that's the one with the weird Q/bandwidth relationship 74 | testDesign(Design::oneSided, "oneSided"); 75 | testDesign(Design::vicanek, "vicanek"); 76 | } 77 | -------------------------------------------------------------------------------- /tests/filters/07-accuracy.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include 7 | 8 | TEST("Double/float accuracy") { 9 | using FilterDouble = signalsmith::filters::BiquadStatic; 10 | using FilterFloat = signalsmith::filters::BiquadStatic; 11 | using Design = signalsmith::filters::BiquadDesign; 12 | 13 | int repeats = 100; 14 | 15 | double totalError2 = 0; 16 | int totalErrorCounter = 0; 17 | 18 | auto testDesign = [&](Design design, std::string name, double errorLimit) { 19 | Test outerTest = test.prefix(name); 20 | 21 | auto testAccuracy = [&](FilterDouble &filterDouble, FilterFloat &filterFloat, double &diffTotal2, double &outTotal2) { 22 | filterDouble.reset(); 23 | filterFloat.reset(); 24 | double zeroLimit = errorLimit; 25 | int endI = 100; 26 | for (int i = 0; i < endI; ++i) { 27 | double v = (i == 0); 28 | double outDouble = filterDouble(v), outFloat = filterFloat(v); 29 | if (std::abs(outDouble) > zeroLimit) endI = std::max(i*2, endI); // continue as long as the tail does 30 | double diff = outDouble - outFloat; 31 | diffTotal2 += diff*diff; 32 | outTotal2 += outDouble*outDouble; 33 | if (std::to_string(diffTotal2) == "nan") { \ 34 | /* make public in `filters.h` if you get here 35 | TEST_EXPR(filterDouble.a1); \ 36 | TEST_EXPR(filterFloat.a1); \ 37 | TEST_EXPR(filterDouble.a2); \ 38 | TEST_EXPR(filterFloat.a2); \ 39 | TEST_EXPR(filterDouble.b0); \ 40 | TEST_EXPR(filterFloat.b0); \ 41 | TEST_EXPR(filterDouble.b1); \ 42 | TEST_EXPR(filterFloat.b1); \ 43 | TEST_EXPR(filterDouble.b2); \ 44 | TEST_EXPR(filterFloat.b2); \ 45 | TEST_EXPR(filterDouble.x1); \ 46 | TEST_EXPR(filterFloat.x1); \ 47 | TEST_EXPR(filterDouble.x2); \ 48 | TEST_EXPR(filterFloat.x2); \ 49 | TEST_EXPR(i); \ 50 | TEST_EXPR(outDouble); \ 51 | TEST_EXPR(outFloat); \ 52 | TEST_EXPR(diffTotal2); \ 53 | TEST_EXPR(outTotal2); \ 54 | */ 55 | abort(); \ 56 | } \ 57 | } 58 | }; 59 | 60 | #define FILTER_METHOD(METHOD) \ 61 | { \ 62 | Test test = outerTest.prefix(#METHOD); \ 63 | double diffTotal2 = 0; \ 64 | double outTotal2 = 0; \ 65 | for (int repeat = 0; repeat < repeats; ++repeat) { \ 66 | double freq = 0.5*(repeat + 0.5)/repeats; \ 67 | double octaves = test.random(0.1, 4); \ 68 | FilterDouble filterDouble; \ 69 | FilterFloat filterFloat; \ 70 | filterDouble.METHOD(freq, octaves, design); \ 71 | filterFloat.METHOD(freq, octaves, design); \ 72 | testAccuracy(filterDouble, filterFloat, diffTotal2, outTotal2); \ 73 | } \ 74 | double rmsError = std::sqrt(diffTotal2/outTotal2); \ 75 | if (rmsError >= errorLimit) { \ 76 | TEST_EXPR(rmsError); \ 77 | return test.fail("float/double error too high"); \ 78 | } \ 79 | totalError2 += rmsError*rmsError; \ 80 | ++totalErrorCounter; \ 81 | } 82 | FILTER_METHOD(lowpass); 83 | FILTER_METHOD(highpass); 84 | FILTER_METHOD(bandpass); 85 | FILTER_METHOD(notch); 86 | FILTER_METHOD(allpass); 87 | #undef FILTER_METHOD 88 | 89 | #define FILTER_METHOD_DB(METHOD) \ 90 | { \ 91 | Test test = outerTest.prefix(#METHOD); \ 92 | double diffTotal2 = 0; \ 93 | double outTotal2 = 0; \ 94 | for (int repeat = 0; repeat < repeats; ++repeat) { \ 95 | double freq = 0.5*(repeat + 0.5)/repeats; \ 96 | double octaves = test.random(0.5, 4); \ 97 | double db = test.random(-30, 30); \ 98 | FilterDouble filterDouble; \ 99 | FilterFloat filterFloat; \ 100 | filterDouble.METHOD##Db(freq, db, octaves, design); \ 101 | filterFloat.METHOD##Db(freq, db, octaves, design); \ 102 | testAccuracy(filterDouble, filterFloat, diffTotal2, outTotal2); \ 103 | } \ 104 | double rmsError = std::sqrt(diffTotal2/outTotal2); \ 105 | if (rmsError >= errorLimit) { \ 106 | TEST_EXPR(rmsError); \ 107 | return test.fail("float/double error too high"); \ 108 | } \ 109 | totalError2 += rmsError*rmsError; \ 110 | ++totalErrorCounter; \ 111 | } 112 | FILTER_METHOD_DB(peak); 113 | FILTER_METHOD_DB(highShelf); 114 | FILTER_METHOD_DB(lowShelf); 115 | #undef FILTER_METHOD_DB 116 | }; 117 | 118 | testDesign(Design::bilinear, "bilinear", 2e-4); 119 | testDesign(Design::cookbook, "cookbook", 2e-4); 120 | testDesign(Design::oneSided, "oneSided", 2e-4); 121 | testDesign(Design::vicanek, "vicanek", 2e-4); 122 | 123 | double totalRmsError = std::sqrt(totalError2/totalErrorCounter); 124 | TEST_EXPR(totalRmsError); 125 | } 126 | -------------------------------------------------------------------------------- /tests/filters/filter-tests.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using Spectrum = std::vector>; 5 | 6 | static inline double ampToDb(double amp) { 7 | return 20*std::log10(std::max(amp, 1e-100)); 8 | } 9 | static inline double dbToAmp(double db) { 10 | return std::pow(10, db*0.05); 11 | } 12 | 13 | template 14 | Spectrum getSpectrum(Filter &filter, double impulseLimit=1e-10, int minLength=256, int maxLength=65536) { 15 | filter.reset(); 16 | Spectrum impulse, spectrum; 17 | 18 | // Size depends on impulse length, to guarantee sufficient frequency resolution 19 | int sample = 0; 20 | int belowLimitCounter = 0; 21 | while (sample < minLength || belowLimitCounter <= sample*0.9 /*overshoot by 10x for padding */) { 22 | double v = filter((sample == 0) ? 1 : 0); 23 | impulse.push_back(v); 24 | auto mag = std::abs(v); 25 | if (mag < impulseLimit) { 26 | ++belowLimitCounter; 27 | } else { 28 | belowLimitCounter = 0; 29 | } 30 | ++sample; 31 | if (sample >= maxLength) break; 32 | } 33 | 34 | int pow2 = 1; 35 | while (pow2 < sample) pow2 *= 2; 36 | while (sample < pow2) { 37 | impulse.push_back(filter(0)); 38 | ++sample; 39 | } 40 | 41 | signalsmith::fft::FFT fft(pow2); 42 | spectrum.resize(pow2); 43 | fft.fft(impulse, spectrum); 44 | return spectrum; 45 | } 46 | 47 | static inline double interpSpectrum(Spectrum spectrum, double freq, int roundDirection = 0) { 48 | double index = freq*spectrum.size(); 49 | int intFreqLow = index; 50 | int intFreqHigh = intFreqLow + 1; 51 | if (roundDirection < 0) return std::abs(spectrum[intFreqLow]); 52 | if (roundDirection > 0) return std::abs(spectrum[intFreqHigh]); 53 | 54 | double ratio = index - intFreqLow; 55 | if (intFreqHigh >= (int)spectrum.size()) { 56 | --intFreqLow; 57 | --intFreqHigh; 58 | } 59 | double energyLow = std::norm(spectrum[intFreqLow]); 60 | double energyHigh = std::norm(spectrum[intFreqHigh]); 61 | return std::sqrt(energyLow + (energyHigh - energyLow)*ratio); 62 | } 63 | 64 | static inline void writeSpectrum(Spectrum spectrum, std::string name) { 65 | CsvWriter csv(name); 66 | csv.line("freq", "dB", "phase", "group delay"); 67 | 68 | double prevPhase = 0, prevMag = 0; 69 | for (size_t i = 0; i <= spectrum.size()/2; ++i) { 70 | auto bin = spectrum[i]; 71 | double mag = std::abs(bin); 72 | double db = 20*std::log10(mag); 73 | double phase = std::arg(bin); 74 | double phaseDiff = (prevPhase - phase); 75 | if (mag < 1e-10 || prevMag < 1e-10) { 76 | phaseDiff = 0; 77 | } else if (phaseDiff > M_PI) { 78 | phaseDiff -= 2*M_PI; 79 | } else if (phaseDiff <= -M_PI) { 80 | phaseDiff += 2*M_PI; 81 | } 82 | double groupDelay = phaseDiff*spectrum.size()/(2*M_PI); 83 | prevMag = mag; 84 | prevPhase = phase; 85 | csv.line(i*1.0/spectrum.size(), db, phase, groupDelay); 86 | } 87 | } 88 | 89 | static inline bool isMonotonic(const Spectrum &spectrum, double from=0, double to=0.5, double thresholdDb=0.000001) { 90 | int direction = (from < to) ? 1 : -1; 91 | if (std::abs(from) >= 1) { 92 | direction = from; 93 | from = 0; 94 | to = 0.5; 95 | } 96 | // Monotonically increasing in the specified direction 97 | int minIndex = std::ceil(std::min(from, to)*spectrum.size()); 98 | int maxIndex = std::floor(std::max(from, to)*spectrum.size()); 99 | int start = (direction > 0) ? minIndex : maxIndex; 100 | int end = (direction > 0) ? maxIndex + 1 : minIndex - 1; 101 | 102 | double thresholdRatio = dbToAmp(-thresholdDb); 103 | 104 | double maxMag = std::abs(spectrum[start]); 105 | for (int i = start; i != end; i += direction) { 106 | double mag = std::abs(spectrum[i]); 107 | if (maxMag > 1e-6 && mag < maxMag*thresholdRatio) { // only care if it's above -120dB 108 | //std::cout << "not monotonic @" << i << " (" << i*1.0/spectrum.size() << ")\n"; 109 | return false; 110 | } 111 | maxMag = std::max(mag, maxMag); 112 | } 113 | return true; 114 | } 115 | -------------------------------------------------------------------------------- /tests/filters/plots.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "filters.h" 5 | 6 | #include "fft.h" 7 | #include "../common.h" 8 | 9 | #include "./filter-tests.h" 10 | 11 | TEST("Filter design plots") { 12 | auto drawShape = [&](int shape, std::string name) { 13 | Figure figure; 14 | int plotCounter = 0; 15 | bool isPeak = (shape == 3), isAllpass = (shape == 5), isShelf = (shape == 6 || shape == 7); 16 | bool singlePlot = (isPeak || isAllpass || isShelf); 17 | auto drawDesign = [&](signalsmith::filters::BiquadDesign design, std::string designName) { 18 | int plotColumn = plotCounter++; 19 | int plotRow = 0; 20 | auto &plot = figure(plotColumn, plotRow + 1).plot(200, singlePlot ? 100 : 150); 21 | auto &plotFocus = singlePlot ? plot : figure(plotColumn, plotRow).plot(200, 50); 22 | auto drawLine = [&](double freq) { 23 | signalsmith::filters::BiquadStatic filter; 24 | if (shape == 0) { 25 | filter.lowpass(freq, design); 26 | } else if (shape == 1) { 27 | filter.highpass(freq, design); 28 | } else if (shape == 2) { 29 | filter.bandpass(freq, 1.66, design); 30 | } else if (shape == 3) { 31 | filter.peak(freq, 4, 1.66, design); 32 | } else if (shape == 4) { 33 | filter.notch(freq, 1.66, design); 34 | } else if (shape == 5) { 35 | filter.allpass(freq, 1.66, design); 36 | } else if (shape == 6) { 37 | filter.highShelfDb(freq, 12, filter.defaultBandwidth, design); 38 | } else { 39 | filter.lowShelfDbQ(freq, 12, filter.defaultQ, design); 40 | } 41 | 42 | auto &line = plot.line(); 43 | auto &lineFocus = singlePlot ? line : plotFocus.line(); 44 | int freqCount = 16384; 45 | for (int fi = 1; fi < freqCount; ++fi) { 46 | double f = fi*0.5/(freqCount - 1); 47 | auto response = filter.response(f); 48 | if (isAllpass) { 49 | auto phase = std::arg(response); 50 | if (phase > 0) phase -= 2*M_PI; 51 | line.add(f, phase); 52 | } else { 53 | line.add(f, ampToDb(std::abs(response))); 54 | if (!singlePlot) lineFocus.add(f, ampToDb(std::abs(response))); 55 | } 56 | } 57 | }; 58 | drawLine(0.001*std::sqrt(10)); 59 | drawLine(0.01); 60 | drawLine(0.01*std::sqrt(10)); 61 | drawLine(0.1); 62 | drawLine(0.1*std::sqrt(10)); 63 | drawLine(0.49); 64 | 65 | // Draw top label as title 66 | plotFocus.newX().flip().label(designName); 67 | if (isPeak || isShelf) { 68 | plot.y.linear(-2, 14).minors(0, 6, 12); 69 | } else if (isAllpass) { 70 | plot.y.linear(-2*M_PI, 0).minor(0, "0").minor(-M_PI, u8"π").minor(-0.5*M_PI, "").minor(-2*M_PI, u8"-2π").minor(-1.5*M_PI, ""); 71 | } else { 72 | plot.y.linear(-90, 1).minors(0, -12, -24, -48, -72); 73 | } 74 | plot.x.range(std::log, 0.001, 0.5).minors(0.001, 0.01, 0.1, 0.5) 75 | .minor(0.001*std::sqrt(10), "").minor(0.01*std::sqrt(10), "").minor(0.1*std::sqrt(10), "") 76 | .label("freq"); 77 | if (!singlePlot) { 78 | plotFocus.y.linear(-6, 0).minor(-3, "").minors(0, -6); 79 | plotFocus.x.range(std::log, 0.001, 0.5).minor(0.001, "").minor(0.01, "").minor(0.1, "").minor(0.5, "") 80 | .minor(0.001*std::sqrt(10), "").minor(0.01*std::sqrt(10), "").minor(0.1*std::sqrt(10), ""); 81 | } 82 | 83 | if (plotColumn == 0) { 84 | plot.y.label(isAllpass ? "phase" : "dB"); 85 | plotFocus.y.label(isAllpass ? "phase" : "dB"); 86 | } else { 87 | plot.y.blankLabels(); 88 | plotFocus.y.blankLabels(); 89 | } 90 | }; 91 | drawDesign(signalsmith::filters::BiquadDesign::bilinear, "bilinear"); 92 | drawDesign(signalsmith::filters::BiquadDesign::cookbook, "cookbook"); 93 | drawDesign(signalsmith::filters::BiquadDesign::oneSided, "oneSided"); 94 | drawDesign(signalsmith::filters::BiquadDesign::vicanek, "vicanek"); 95 | 96 | figure.write("filters-" + name + ".svg"); 97 | }; 98 | drawShape(0, "lowpass"); 99 | drawShape(1, "highpass"); 100 | drawShape(2, "bandpass"); 101 | drawShape(3, "peak"); 102 | drawShape(4, "notch"); 103 | drawShape(5, "allpass"); 104 | drawShape(6, "high-shelf"); 105 | drawShape(7, "low-shelf"); 106 | return test.pass(); 107 | } 108 | -------------------------------------------------------------------------------- /tests/mix/cheap-fade.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "../common.h" 5 | #include "dsp/mix.h" 6 | 7 | #include 8 | #include 9 | 10 | TEST("Cheap cross-fade") { 11 | float to, from; 12 | signalsmith::mix::cheapEnergyCrossfade(0.0, to, from); 13 | TEST_APPROX(to, 0, 1e-6); 14 | TEST_APPROX(from, 1, 1e-6); 15 | signalsmith::mix::cheapEnergyCrossfade(1, to, from); 16 | TEST_APPROX(to, 1, 1e-6); 17 | TEST_APPROX(from, 0, 1e-6); 18 | signalsmith::mix::cheapEnergyCrossfade(0.5, to, from); 19 | TEST_APPROX(to, from, 1e-5); 20 | TEST_APPROX(to, std::sqrt(0.5), 0.01); 21 | TEST_APPROX(from, std::sqrt(0.5), 0.01); 22 | 23 | for (double x = 0; x < 1; x += 0.01) { 24 | float to, from; 25 | signalsmith::mix::cheapEnergyCrossfade(x, to, from); 26 | double energy = to*to + from*from; 27 | TEST_APPROX(energy, 1, 0.011 /* 1.1% error */); 28 | 29 | TEST_ASSERT(to > -0.00001); 30 | TEST_ASSERT(from > -0.00001); 31 | TEST_ASSERT(to < 1.004); 32 | TEST_ASSERT(from < 1.004); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/mix/mixing-matrix.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "../common.h" 5 | #include "dsp/mix.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | template 12 | void testHadamardTyped(Test test) { 13 | using Hadamard = signalsmith::mix::Hadamard; 14 | for (size_t i = 0; i < size; ++i) { 15 | std::array input, unscaled, scaled; 16 | for (size_t j = 0; j < size; ++j) { 17 | input[j] = (j == i); 18 | } 19 | unscaled = scaled = input; 20 | Hadamard::unscaledInPlace(unscaled); 21 | Hadamard::inPlace(scaled); 22 | for (size_t j = 0; j < size; ++j) { 23 | // Unscaled produces ±1 24 | TEST_EQUAL(std::abs(unscaled[j]), Sample(1)); 25 | // Scaled is same, but scaled 26 | TEST_EQUAL(scaled[j], unscaled[j]*Hadamard::scalingFactor()); 27 | } 28 | } 29 | 30 | // Linearity 31 | std::array randomA, randomB, randomC, randomAB; 32 | Sample energyA = 0; 33 | for (size_t j = 0; j < size; ++j) { 34 | randomA[j] = test.random(-10, 10); 35 | randomB[j] = test.random(-10, 10); 36 | randomC[j] = test.random(-10, 10); 37 | randomAB[j] = randomA[j] + randomB[j]; 38 | energyA += randomA[j]*randomA[j]; 39 | } 40 | std::array randomACopy = randomA; 41 | Hadamard::inPlace(randomA); 42 | Hadamard::inPlace(randomB.data()); 43 | Hadamard::inPlace(randomAB); 44 | 45 | Sample energyOutA = 0; 46 | double accuracy = 0.0001; 47 | for (size_t j = 0; j < size; ++j) { 48 | TEST_APPROX(randomA[j] + randomB[j], randomAB[j], accuracy); 49 | energyOutA += randomA[j]*randomA[j]; 50 | } 51 | TEST_APPROX(energyA, energyOutA, 0.01); 52 | 53 | // Dynamic size 54 | const signalsmith::mix::Hadamard hadamard(size); 55 | TEST_EQUAL(hadamard.scalingFactor(), Hadamard::scalingFactor()); 56 | hadamard.inPlace(randomACopy); 57 | for (size_t j = 0; j < size; ++j) { 58 | TEST_APPROX(randomACopy[j], randomA[j], 1e-5); 59 | } 60 | } 61 | 62 | template 63 | void testHadamard(Test test) { 64 | testHadamardTyped(test.prefix("double")); 65 | testHadamardTyped(test.prefix("float")); 66 | } 67 | 68 | TEST("Hadamard") { 69 | testHadamard<0>(test.prefix("0")); 70 | testHadamard<1>(test.prefix("1")); 71 | testHadamard<2>(test.prefix("2")); 72 | testHadamard<4>(test.prefix("4")); 73 | testHadamard<8>(test.prefix("8")); 74 | testHadamard<16>(test.prefix("16")); 75 | testHadamard<32>(test.prefix("32")); 76 | } 77 | 78 | template 79 | void testHouseholderTyped(Test test) { 80 | using Householder = signalsmith::mix::Householder; 81 | TEST_EQUAL(Householder::scalingFactor(), Sample(1)); 82 | 83 | // Linearity 84 | std::array randomA, randomB, randomAB; 85 | Sample energyA = 0; 86 | for (size_t j = 0; j < size; ++j) { 87 | randomA[j] = test.random(-10, 10); 88 | randomB[j] = test.random(-10, 10); 89 | randomAB[j] = randomA[j] + randomB[j]; 90 | energyA += randomA[j]*randomA[j]; 91 | } 92 | std::array randomACopy = randomA; 93 | Householder::inPlace(randomA); 94 | Householder::inPlace(randomB.data()); 95 | Householder::inPlace(randomAB); 96 | 97 | Sample energyOutA = 0; 98 | double accuracy = 0.0001; 99 | for (size_t j = 0; j < size; ++j) { 100 | TEST_APPROX(randomA[j] + randomB[j], randomAB[j], accuracy); 101 | energyOutA += randomA[j]*randomA[j]; 102 | } 103 | TEST_APPROX(energyA, energyOutA, 0.01); 104 | 105 | // Dynamic size 106 | const signalsmith::mix::Householder householder(size); 107 | TEST_EQUAL(householder.scalingFactor(), Householder::scalingFactor()); 108 | householder.inPlace(randomACopy); 109 | for (size_t j = 0; j < size; ++j) { 110 | TEST_APPROX(randomACopy[j], randomA[j], 1e-5); 111 | } 112 | } 113 | 114 | template 115 | void testHouseholder(Test test) { 116 | testHouseholderTyped(test.prefix("double")); 117 | testHouseholderTyped(test.prefix("float")); 118 | testHouseholderTyped, size>(test.prefix("std::complex")); 119 | testHouseholderTyped, size>(test.prefix("std::complex")); 120 | } 121 | 122 | TEST("Householder") { 123 | testHouseholder<0>(test.prefix("0")); 124 | testHouseholder<1>(test.prefix("1")); 125 | testHouseholder<2>(test.prefix("2")); 126 | testHouseholder<3>(test.prefix("3")); 127 | testHouseholder<4>(test.prefix("4")); 128 | testHouseholder<5>(test.prefix("5")); 129 | testHouseholder<6>(test.prefix("6")); 130 | } 131 | -------------------------------------------------------------------------------- /tests/mix/stereo-multi.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | 4 | #include "../common.h" 5 | #include "dsp/mix.h" 6 | 7 | #include 8 | #include 9 | 10 | template 11 | void testStereoTyped(Test test) { 12 | const signalsmith::mix::StereoMultiMixer mixer; 13 | 14 | std::array stereo{ 15 | Sample(test.random(-10, 10)), 16 | Sample(test.random(-10, 10)) 17 | }; 18 | std::array stereoCopy = stereo; 19 | Sample inputEnergy = stereo[0]*stereo[0] + stereo[1]*stereo[1]; 20 | std::array multi; 21 | 22 | mixer.stereoToMulti(stereo, multi); 23 | // Upmix preserves energy for pairs 24 | for (int i = 0; i < size; i += 2) { 25 | Sample energy = multi[i]*multi[i] + multi[i + 1]*multi[i + 1]; 26 | TEST_APPROX(energy, inputEnergy, 0.0001); 27 | } 28 | mixer.multiToStereo(multi, stereo); 29 | // When the results are in-phase, we should scale by `.scalingFactor1()` 30 | TEST_APPROX(stereo[0]*mixer.scalingFactor1(), stereoCopy[0], 0.0001); 31 | TEST_APPROX(stereo[1]*mixer.scalingFactor1(), stereoCopy[1], 0.0001); 32 | 33 | // Downmix preserves energy from pairs 34 | for (int pairStart = 0; pairStart < size; pairStart += 2) { 35 | for (auto &s : multi) s = 0; 36 | multi[pairStart] = test.random(-10, 10); 37 | multi[pairStart + 1] = test.random(-10, 10); 38 | Sample inputEnergy = multi[pairStart]*multi[pairStart] + multi[pairStart + 1]*multi[pairStart + 1]; 39 | 40 | mixer.multiToStereo(multi, stereo); 41 | Sample outputEnergy = stereo[0]*stereo[0] + stereo[1]*stereo[1]; 42 | TEST_APPROX(outputEnergy, inputEnergy, 0.0001); 43 | } 44 | } 45 | 46 | template 47 | void testStereo(Test test) { 48 | testStereoTyped(test.prefix("double")); 49 | testStereoTyped(test.prefix("float")); 50 | } 51 | 52 | TEST("StereoMultiMix") { 53 | testStereo<2>(test.prefix("2")); 54 | testStereo<4>(test.prefix("4")); 55 | testStereo<6>(test.prefix("6")); 56 | testStereo<8>(test.prefix("8")); 57 | testStereo<10>(test.prefix("10")); 58 | testStereo<20>(test.prefix("20")); 59 | } 60 | -------------------------------------------------------------------------------- /tests/perf/00-utils.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | #include 4 | 5 | #include "../common.h" 6 | #include "perf.h" 7 | 8 | #include 9 | 10 | template 11 | void testComplex(Test &test) { 12 | using Complex = std::complex; 13 | auto rand = [&](){return Sample(test.random(-100, 100));}; 14 | 15 | int repeats = 1000; 16 | for (int r = 0; r < repeats; ++r) { 17 | Complex a = {rand(), rand()}; 18 | Complex b = {rand(), rand()}; 19 | 20 | if (a*b != signalsmith::perf::mul(a, b)) return test.fail("multiplication"); 21 | if (a*std::conj(b) != signalsmith::perf::mul(a, b)) return test.fail("multiplication"); 22 | } 23 | } 24 | 25 | TEST("Complex multiplcation") { 26 | testComplex(test); 27 | testComplex(test); 28 | } 29 | -------------------------------------------------------------------------------- /tests/perf/01-lagrange-methods.cpp: -------------------------------------------------------------------------------- 1 | // from the shared library 2 | #include 3 | #include 4 | 5 | #include "delay.h" 6 | #include "../common.h" 7 | 8 | #include 9 | #include 10 | 11 | /*////// Graveyard of Lagrange implementations //////*/ 12 | 13 | template 14 | struct LagrangeMulAddBasic { 15 | static constexpr int inputLength = n + 1; 16 | static constexpr int latency = (n - 1)/2; 17 | 18 | std::array invDivisors; 19 | 20 | LagrangeMulAddBasic() { 21 | for (int j = 0; j <= n; ++j) { 22 | double divisor = 1; 23 | for (int k = 0; k < j; ++k) divisor *= (j - k); 24 | for (int k = j + 1; k <= n; ++k) divisor *= (j - k); 25 | invDivisors[j] = 1/divisor; 26 | } 27 | } 28 | 29 | template 30 | Sample fractional(const Data &data, Sample fractional) const { 31 | std::array sums; 32 | 33 | Sample x = fractional + latency; 34 | 35 | Sample forwardFactor = 1; 36 | sums[0] = data[0]; 37 | for (int i = 1; i <= n; ++i) { 38 | forwardFactor *= x - (i - 1); 39 | sums[i] = forwardFactor*data[i]; 40 | } 41 | 42 | Sample backwardsFactor = 1; 43 | Sample result = sums[n]*invDivisors[n]; 44 | for (int i = n - 1; i >= 0; --i) { 45 | backwardsFactor *= x - (i + 1); 46 | result += sums[i]*invDivisors[i]*backwardsFactor; 47 | } 48 | return result; 49 | } 50 | }; 51 | 52 | namespace _franck_impl { 53 | template 54 | struct ProductRange { 55 | using Array = std::array; 56 | static constexpr int mid = (low + high)/2; 57 | using Left = ProductRange; 58 | using Right = ProductRange; 59 | 60 | Left left; 61 | Right right; 62 | 63 | const Sample total; 64 | ProductRange(Sample x) : left(x), right(x), total(left.total*right.total) {} 65 | 66 | template 67 | Sample calculateResult(Sample extraFactor, const Data &data, const Array &invFactors) { 68 | return left.calculateResult(extraFactor*right.total, data, invFactors) 69 | + right.calculateResult(extraFactor*left.total, data, invFactors); 70 | } 71 | }; 72 | template 73 | struct ProductRange { 74 | using Array = std::array; 75 | 76 | const Sample total; 77 | ProductRange(Sample x) : total(x - index) {} 78 | 79 | template 80 | Sample calculateResult(Sample extraFactor, const Data &data, const Array &invFactors) { 81 | return extraFactor*data[index]*invFactors[index]; 82 | } 83 | }; 84 | } 85 | 86 | template 87 | struct LagrangeMulAddFranck { 88 | static constexpr int inputLength = n + 1; 89 | static constexpr int latency = (n - 1)/2; 90 | 91 | using Array = std::array; 92 | Array invDivisors; 93 | 94 | LagrangeMulAddFranck() { 95 | for (int j = 0; j <= n; ++j) { 96 | double divisor = 1; 97 | for (int k = 0; k < j; ++k) divisor *= (j - k); 98 | for (int k = j + 1; k <= n; ++k) divisor *= (j - k); 99 | invDivisors[j] = 1/divisor; 100 | } 101 | } 102 | 103 | template 104 | Sample fractional(const Data &data, Sample fractional) const { 105 | constexpr int mid = n/2; 106 | using Left = _franck_impl::ProductRange; 107 | using Right = _franck_impl::ProductRange; 108 | 109 | Sample x = fractional + latency; 110 | 111 | Left left(x); 112 | Right right(x); 113 | 114 | return left.calculateResult(right.total, data, invDivisors) + right.calculateResult(left.total, data, invDivisors); 115 | } 116 | }; 117 | 118 | template 119 | struct LagrangeBarycentric { 120 | static constexpr int inputLength = n + 1; 121 | static constexpr int latency = (n - 1)/2; 122 | 123 | std::array weights; 124 | 125 | LagrangeBarycentric() { 126 | for (int j = 0; j <= n; ++j) { 127 | double divisor = 1; 128 | for (int k = 0; k < j; ++k) divisor *= (j - k); 129 | for (int k = j + 1; k <= n; ++k) divisor *= (j - k); 130 | weights[j] = 1/divisor; 131 | } 132 | } 133 | 134 | template 135 | Sample fractional(const Data &data, Sample fractional) const { 136 | if (fractional == 0 || fractional == 1) return data[latency + fractional]; 137 | 138 | Sample x = fractional + latency; 139 | // We can special-case index 0 140 | Sample component0 = weights[0]/x; 141 | Sample componentTotal = component0; 142 | Sample resultTotal = component0*data[0]; 143 | for (int i = 1; i <= n; ++i) { 144 | Sample component = weights[i]/(x - i); 145 | componentTotal += component; 146 | resultTotal += component*data[i]; 147 | } 148 | return resultTotal/componentTotal; 149 | } 150 | }; 151 | 152 | /*////// Testing logic //////*/ 153 | 154 | template 155 | static double measureInterpolator(Test &test, bool testAgainstReference) { 156 | using InterpolatorReference = signalsmith::delay::InterpolatorLagrangeN; 157 | 158 | std::vector buffer(1024); 159 | for (size_t i = 0; i < buffer.size(); ++i) { 160 | buffer[i] = test.random(-10, 10); 161 | } 162 | 163 | int testBlock = 100000; 164 | std::vector delayTimes(testBlock); 165 | std::vector result(testBlock); 166 | for (int i = 0; i < testBlock; ++i) { 167 | delayTimes[i] = test.random(0, 1); 168 | } 169 | 170 | Interpolator interpolator; 171 | if (testAgainstReference) { 172 | InterpolatorReference reference; // validated by other tests 173 | for (double d : delayTimes) { 174 | double expected = reference.fractional(buffer, d); 175 | double actual = interpolator.fractional(buffer, d); 176 | if (!test.closeEnough(actual, expected, "actual ~= expected", 1e-4)) { 177 | std::cout << actual << " != " << expected << "\n"; 178 | return -1e10; 179 | } 180 | } 181 | } 182 | 183 | int trials = 100; 184 | Stopwatch stopwatch{false}; 185 | for (int t = 0; t < trials; ++t) { 186 | stopwatch.startLap(); 187 | for (int i = 0; i < testBlock; ++i) { 188 | result[i] = interpolator.fractional(buffer, delayTimes[i]); 189 | } 190 | stopwatch.lap(); 191 | } 192 | double lapTime = stopwatch.optimistic(); 193 | std::cout << "\t" << n << ":\t" << lapTime << "\n"; 194 | return lapTime; 195 | } 196 | 197 | template class Interpolator> 198 | std::vector measureInterpolatorFamily(Test &test, std::string name, bool testAgainstReference) { 199 | std::cout << name << ":\n"; 200 | std::vector result; 201 | #define PERF_TEST(n) \ 202 | result.push_back(measureInterpolator>(test, testAgainstReference)); 203 | PERF_TEST(3) 204 | PERF_TEST(5) 205 | PERF_TEST(7) 206 | PERF_TEST(9) 207 | PERF_TEST(11) 208 | PERF_TEST(13) 209 | PERF_TEST(15) 210 | PERF_TEST(17) 211 | PERF_TEST(19) 212 | return result; 213 | } 214 | 215 | template 216 | using KaiserSincN = signalsmith::delay::InterpolatorKaiserSincN; // default minPhase argument 217 | 218 | template 219 | struct PerformanceResults { 220 | Test &test; 221 | struct Pair { 222 | std::string name; 223 | std::vector speeds; 224 | }; 225 | std::vector pairs; 226 | 227 | PerformanceResults(Test &test) : test(test) {} 228 | 229 | template class Interpolator> 230 | void add(std::string name, bool testAgainstReference=true) { 231 | if (!test.success) return; 232 | std::vector speeds = measureInterpolatorFamily(test, name, testAgainstReference); 233 | pairs.push_back({name, speeds}); 234 | } 235 | 236 | void runAll(std::string csvName) { 237 | add("current"); 238 | // add("mul/add basic"); 239 | add("Franck"); 240 | add("barycentric"); 241 | add("kaiser-sinc", false); 242 | 243 | CsvWriter csv(csvName); 244 | csv.write("N"); 245 | for (auto &pair : pairs) csv.write(pair.name); 246 | csv.line(); 247 | 248 | for (size_t i = 0; i < pairs[0].speeds.size(); ++i) { 249 | csv.write(3 + 2*i); 250 | for (auto &pair : pairs) csv.write(pair.speeds[i]); 251 | csv.line(); 252 | } 253 | } 254 | }; 255 | 256 | TEST("Performance: Lagrange interpolation (double)") { 257 | PerformanceResults results(test); 258 | results.runAll("performance-lagrange-interpolation-double"); 259 | } 260 | TEST("Performance: Lagrange interpolation (float)") { 261 | PerformanceResults results(test); 262 | results.runAll("performance-lagrange-interpolation-float"); 263 | } 264 | -------------------------------------------------------------------------------- /tests/perf/perf-lagrange.py: -------------------------------------------------------------------------------- 1 | from numpy import * 2 | from numpy.fft import rfft 3 | 4 | import article 5 | 6 | for type in ["double", "float"]: 7 | columns, data = article.readCsv("performance-lagrange-interpolation-%s.csv"%type) 8 | 9 | figure, axes = article.medium() 10 | for i in range(1, len(columns)): 11 | axes.plot(data[0], data[i], label=columns[i]) 12 | axes.set(xlabel="order", ylabel="computation time", ylim=[0, None], xlim=[min(data[0]), max(data[0])], xticks=range(int(min(data[0])), int(max(data[0])) + 1, 2)); 13 | figure.save("performance-lagrange-interpolation-%s.svg"%type) 14 | -------------------------------------------------------------------------------- /tests/rates/00-kaiser-sinc.cpp: -------------------------------------------------------------------------------- 1 | #include "rates.h" 2 | #include "fft.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // from the shared library 9 | #include 10 | #include "../common.h" 11 | 12 | TEST("Kaiser-sinc pass/stop specification") { 13 | int length = 33; 14 | int padTo = 2048; 15 | 16 | std::vector buffer; 17 | 18 | signalsmith::fft::FFT fft(padTo); 19 | std::vector> spectrum(padTo); 20 | 21 | Figure figure; 22 | auto &timePlot = figure(0, 0).plot(350, 125); 23 | auto &freqPlot = figure(0, 1).plot(350, 135); 24 | timePlot.x.linear(0, length - 1).label("index").minors(length - 1).major((length - 1)/2); 25 | timePlot.y.linear(-0.2, 1).label("signal").major(0).minor(1); 26 | freqPlot.x.linear(0, 0.5).label("frequency").major(0).minors(0.1, 0.2, 0.3, 0.4, 0.5); 27 | freqPlot.y.linear(-160, 10).label("dB").minors(-150, -120, -90, -60, -30, 0); 28 | auto &legend = timePlot.legend(1, 1); 29 | auto plotKernel = [&](double passFreq, double stopFreq) { 30 | char label[256]; 31 | std::snprintf(label, 256, "%.1f to %.1f", passFreq, stopFreq); 32 | auto &timeLine = timePlot.line(); 33 | auto &freqLine = freqPlot.line(); 34 | legend.line(timeLine, label); 35 | 36 | buffer.resize(length); 37 | signalsmith::rates::fillKaiserSinc(buffer, length, passFreq, stopFreq); 38 | for (int i = 0; i < length; ++i) { 39 | timeLine.add(i, buffer[i]); 40 | } 41 | 42 | buffer.resize(padTo); 43 | fft.fft(buffer, spectrum); 44 | for (int f = 0; f <= padTo/2; ++f) { 45 | double scaledF = f*1.0/padTo; 46 | double energy = std::norm(spectrum[f]); 47 | double db = 10*std::log10(energy + 1e-300); 48 | freqLine.add(scaledF, db); 49 | } 50 | }; 51 | plotKernel(0.1, 0.2); 52 | plotKernel(0.1, 0.3); 53 | plotKernel(0.1, 0.4); 54 | //plotKernel(0.4, 0.5); 55 | 56 | figure.write("rates-kaiser-sinc.svg"); 57 | return test.pass(); 58 | } 59 | 60 | TEST("Kaiser-sinc centre specification") { 61 | std::vector lengths = {10, 30, 95}; 62 | int padTo = 2048; 63 | double centreFreq = 0.25; 64 | 65 | std::vector buffer; 66 | 67 | signalsmith::fft::FFT fft(padTo); 68 | std::vector> spectrum(padTo); 69 | 70 | Figure figure; 71 | auto &freqPlot = figure.plot(350, 150); 72 | freqPlot.x.linear(0, 0.5).label("frequency").major(0).minor(0.5).minor(centreFreq); 73 | freqPlot.y.linear(-150, 10).label("dB").minors(-150, -120, -90, -60, -30, 0); 74 | auto &legend = freqPlot.legend(0, 0); 75 | for (auto &length : lengths) { 76 | char label[256]; 77 | std::snprintf(label, 256, "N = %i", length); 78 | auto &freqLine = freqPlot.line(); 79 | legend.add(freqLine.styleIndex, label, true, true); 80 | auto &fillLine = freqPlot.fill(freqLine.styleIndex); 81 | 82 | buffer.resize(length); 83 | signalsmith::rates::fillKaiserSinc(buffer, length, centreFreq); 84 | 85 | buffer.resize(padTo); 86 | fft.fft(buffer, spectrum); 87 | double halfWidth = 0.45/std::sqrt(length); 88 | fillLine.add(centreFreq - halfWidth, -300); 89 | for (int f = 0; f <= padTo/2; ++f) { 90 | double scaledF = f*1.0/padTo; 91 | double energy = std::norm(spectrum[f]); 92 | double db = 10*std::log10(energy + 1e-300); 93 | freqLine.add(scaledF, db); 94 | if (scaledF >= centreFreq - halfWidth && scaledF <= centreFreq + halfWidth) { 95 | fillLine.add(scaledF, db); 96 | } 97 | } 98 | fillLine.add(centreFreq + halfWidth, -300); 99 | } 100 | 101 | figure.write("rates-kaiser-sinc-heuristic.svg"); 102 | return test.pass(); 103 | } 104 | -------------------------------------------------------------------------------- /tests/spectral/01-windowed-fft.cpp: -------------------------------------------------------------------------------- 1 | #include "spectral.h" 2 | 3 | // common test stuff (including plots) 4 | #include "../common.h" 5 | 6 | // from the shared library 7 | #include 8 | #include 9 | #include 10 | 11 | TEST("Windowed FFT: flat window function") { 12 | int fftSize = 256; 13 | 14 | signalsmith::spectral::WindowedFFT fft; 15 | fft.setSize(fftSize, [](double) { 16 | return 1.0; 17 | }); 18 | 19 | int harmonic = 5; 20 | std::vector time(fftSize); 21 | for (int i = 0; i < fftSize; ++i) { 22 | double phase = 2*M_PI*(harmonic + 0.5)*i/fftSize; 23 | time[i] = std::sin(phase)*2; 24 | } 25 | std::vector> freq(fftSize/2); 26 | 27 | fft.fft(time, freq); 28 | 29 | test.closeEnough(freq[harmonic].real(), 0.0f, "real", 1e-4); 30 | test.closeEnough(freq[harmonic].imag(), -1.0f*fftSize, "imag", 1e-4); 31 | } 32 | 33 | double windowHann(double x) { 34 | return 0.5 - 0.5*std::cos(2*M_PI*x); 35 | } 36 | 37 | TEST("Windowed FFT: Hann") { 38 | int fftSize = 336; /// 2^4 * 3 * 7 - not a "fast size", but not too slow either 39 | 40 | signalsmith::spectral::WindowedFFT fft; 41 | fft.setSize(fftSize, windowHann, 0); 42 | 43 | const std::vector &window = fft.window(); 44 | TEST_ASSERT((int)window.size() == fftSize); 45 | TEST_ASSERT((int)window.size() == fft.size()); 46 | for (int i = 0; i < fftSize; ++i) { 47 | float expected = windowHann(i/(float)fftSize); 48 | test.closeEnough(window[i], expected, "Hann window", 1e-6); 49 | } 50 | 51 | int harmonic = 5; 52 | std::vector time(fftSize); 53 | for (int i = 0; i < fftSize; ++i) { 54 | double phase = 2*M_PI*(harmonic + 0.5)*i/fftSize; 55 | time[i] = std::cos(phase)*2; 56 | } 57 | std::vector> freq(fftSize/2); 58 | 59 | fft.fft(time, freq); 60 | 61 | test.closeEnough(freq[harmonic - 1].real(), -0.25*fftSize, "n-1", 1e-4); 62 | test.closeEnough(freq[harmonic].real(), 0.5*fftSize, "n", 1e-4); 63 | test.closeEnough(freq[harmonic + 1].real(), -0.25*fftSize, "n+1", 1e-4); 64 | } 65 | 66 | TEST("Inverse, with windowing") { 67 | int fftSize = 256; 68 | 69 | signalsmith::spectral::WindowedFFT fft; 70 | double offset = 0.25; 71 | fft.setSize(fftSize, windowHann, offset); 72 | 73 | std::vector time(fftSize), output(fftSize); 74 | for (int i = 0; i < fftSize; ++i) { 75 | time[i] = test.random(-1.0, 1.0); 76 | } 77 | std::vector> freq(fftSize/2); 78 | 79 | fft.fft(time, freq); 80 | fft.ifft(freq, output); 81 | 82 | for (int i = 0; i < fftSize; ++i) { 83 | double hann = windowHann((i + offset)/fftSize); 84 | if (!test.closeEnough(output[i], time[i]*hann*hann, "double-Hann windowed result", 1e-10)) return; 85 | } 86 | } 87 | 88 | TEST("Windowed FFT: time-domain rotation") { 89 | int fftSize = 256; 90 | int rotate = 101; 91 | 92 | signalsmith::spectral::WindowedFFT fft; 93 | fft.setSize(fftSize); 94 | auto &window = fft.window(); 95 | 96 | signalsmith::spectral::WindowedFFT rotatedFft; 97 | rotatedFft.setSize(fftSize, rotate); 98 | 99 | int harmonic = 5; 100 | std::vector time(fftSize), rotatedTime(fftSize), inverseTime(fftSize); 101 | for (int i = 0; i < fftSize; ++i) { 102 | double phase = 2*M_PI*(harmonic + 0.5)*i/fftSize; 103 | time[i] = std::sin(phase)*2; 104 | } 105 | for (int i = 0; i < fftSize; ++i) { 106 | int i2 = (i + rotate)%fftSize; 107 | rotatedTime[i] = time[i2]; 108 | rotatedTime[i] *= window[i2]; 109 | if (i > fftSize - rotate) rotatedTime[i] *= -1; 110 | } 111 | 112 | std::vector> freq(fftSize/2), rotatedFreq(fftSize/2); 113 | 114 | fft.fftRaw(rotatedTime, freq); 115 | rotatedFft.fft(time, rotatedFreq); 116 | 117 | for (int b = 0; b < fftSize/2; ++b) { 118 | test.closeEnough(freq[b], rotatedFreq[b], "fft(rotatedTime) should equal rotatedFft(time)"); 119 | } 120 | 121 | fft.ifftRaw(freq, inverseTime); 122 | for (auto &s : inverseTime) s /= fftSize; // the raw one has no scaling 123 | for (int i = 0; i < fftSize; ++i) { 124 | test.closeEnough(inverseTime[i], rotatedTime[i], "unrotated inverse should match rotated input", 1e-4); 125 | } 126 | 127 | rotatedFft.ifft(freq, inverseTime); 128 | for (int i = 0; i < fftSize; ++i) { 129 | test.closeEnough(inverseTime[i], time[i]*window[i]*window[i], "rotated inverse should match unrotated input", 1e-4); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/spectral/03-stft-processor.cpp: -------------------------------------------------------------------------------- 1 | #include "spectral.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "../common.h" 10 | 11 | TEST("STFT processor") { 12 | constexpr int channels = 3; 13 | constexpr int windowSize = 511; 14 | constexpr int interval = 256; 15 | 16 | int inputLength = 10.5*interval; 17 | signalsmith::delay::MultiBuffer inputBuffer(channels, inputLength); 18 | signalsmith::delay::MultiBuffer outputBuffer(channels, inputLength + windowSize); 19 | // Fill input with random data 20 | for (int c = 0; c < channels; ++c) { 21 | auto channel = inputBuffer[c]; 22 | for (int i = 0; i < inputLength; ++i) { 23 | channel[i] = test.random(-1, 1); 24 | } 25 | } 26 | 27 | struct MySTFT : public signalsmith::spectral::ProcessSTFT { 28 | using signalsmith::spectral::ProcessSTFT::ProcessSTFT; 29 | 30 | int spectrumCount = 0; 31 | void processSpectrum(int) { 32 | ++spectrumCount; 33 | } 34 | }; 35 | MySTFT stft(channels, channels, windowSize, interval, inputLength + windowSize); 36 | 37 | /* window size rounds up, then gets halved */ 38 | TEST_ASSERT(stft.fftSize() == 512); 39 | TEST_ASSERT(stft.bands() == 256); 40 | TEST_ASSERT(stft.interval() == 256); 41 | 42 | for (int i = 0; i < inputLength; ++i) { 43 | for (int c = 0; c < channels; ++c) { 44 | stft.input[c][i] = inputBuffer[c][i]; 45 | } 46 | stft.ensureValid(i); 47 | outputBuffer.at(i) = stft.at(i); 48 | } 49 | 50 | TEST_ASSERT(stft.spectrumCount >= 10); 51 | TEST_ASSERT(stft.spectrumCount <= 11); 52 | 53 | int latency = stft.latency(); 54 | TEST_ASSERT(latency >= windowSize - 1 && latency <= windowSize + interval); 55 | // Input is passed through unchanged, with latency 56 | for (int i = latency + windowSize/* first blocks will be missing */; i < inputLength; ++i) { 57 | for (int c = 0; c < channels; ++c) { 58 | TEST_ASSERT(test.closeEnough(inputBuffer[c][i - latency], outputBuffer[c][i], "outputs match", 1e-4)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/windows/01-approx-confined-gaussian.cpp: -------------------------------------------------------------------------------- 1 | #include "../common.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "./window-stats.h" 8 | 9 | TEST("Window examples (ACG)") { 10 | Figure figure; 11 | auto &timePlot = figure(0, 0).plot(350, 150); 12 | timePlot.x.linear(0, 1).ticks(0, 1); 13 | timePlot.y.linear(0, 1).major(0).minor(1); 14 | auto &legend = timePlot.legend(1.5, 1); 15 | 16 | int size = 2048; 17 | auto addSigma = [&](double sigma, std::string label) { 18 | auto &line = timePlot.line(); 19 | signalsmith::windows::ApproximateConfinedGaussian acg(sigma); 20 | std::vector window(size); 21 | acg.fill(window, size); 22 | for (int i = 0; i < size; ++i) { 23 | line.add((i + 0.5)/size, window[i]); 24 | } 25 | legend.add(line, label); 26 | }; 27 | addSigma(0.5, "σ=1/2"); 28 | addSigma(0.25, "σ=1/4"); 29 | addSigma(0.125, "σ=1/8"); 30 | addSigma(0.0625, "σ=1/16"); 31 | 32 | figure.write("windows-acg-examples.svg"); 33 | return test.pass(); 34 | } 35 | 36 | /* 37 | // Should be close to 1dB of optimal sigma value, according to our weird homebrewed metric below 38 | double optimalAcgHeuristic(double bandwidth) { 39 | return 0.3/std::sqrt(bandwidth);// + 0.5/(bandwidth*bandwidth*bandwidth*bandwidth); 40 | } 41 | TEST("Optimal ACG search") { 42 | using Confined = signalsmith::windows::ApproximateConfinedGaussian; 43 | auto score = [&](double sigma, double bandwidth) { 44 | auto confined = Confined(sigma); 45 | auto stats = measureWindow(confined, bandwidth); 46 | double energyRatio = stats.sideEnergy/(stats.mainEnergy + 1e-100); 47 | double peakRatio = stats.sidePeak/(stats.mainPeak + 1e-100); 48 | return energyRatio + peakRatio*0.1/bandwidth; // weird homebrewed metric 49 | }; 50 | auto findGoodSigma = [&](double bandwidth) { 51 | double bestSigma = 0; 52 | double bestScore = 1e100; 53 | double step = 1e-3; 54 | bool foundNewBest = true; 55 | for (double s = step; s < 0.5 || (s < 10 && foundNewBest); s = std::max(s + step, s*1.01)) { 56 | double candidateScore = score(s, bandwidth); 57 | bool foundNewBest = candidateScore < bestScore; 58 | if (foundNewBest) { 59 | bestSigma = s; 60 | bestScore = candidateScore; 61 | } 62 | } 63 | return bestSigma; 64 | }; 65 | 66 | Plot2D plot(500, 250); 67 | plot.y.linear(0, 2).major(0).minors(1, 2); 68 | plot.x.linear(0, 1).minors(0, 1).ticks(0.3, 0.35, 0.4); 69 | auto &line = plot.line(); 70 | auto &line2 = plot.line(); 71 | for (double b = 1; b <= 1000; b = std::max(b + 0.01, b*1.05)) { 72 | double sigma = findGoodSigma(b); 73 | double heuristic = Confined::bandwidthToSigma(b); 74 | test.log("bandwidth: ", b, " -> ", sigma, "\t", score(sigma, b)/score(heuristic, b)); 75 | line.add(1/b, sigma); 76 | line2.add(1/b, heuristic); 77 | plot.write("tmp-window-acg-good.svg"); 78 | } 79 | 80 | plot.write("tmp-window-acg-good.svg"); 81 | } 82 | */ 83 | 84 | TEST("Window comparison (ACG)") { 85 | using Confined = signalsmith::windows::ApproximateConfinedGaussian; 86 | 87 | Plot2D bandwidthPeakPlot, bandwidthEnergyPlot, bandwidthEnbwPlot; 88 | auto _1 = bandwidthPeakPlot.writeLater("windows-acg-sidelobe-peaks.svg"); 89 | auto _2 = bandwidthEnergyPlot.writeLater("windows-acg-sidelobe-energy.svg"); 90 | auto _3 = bandwidthEnbwPlot.writeLater("windows-acg-enbw.svg"); 91 | 92 | bandwidthPeakPlot.x.linear(1, 10).label("bandwidth"); 93 | bandwidthPeakPlot.y.linear(-80, 0).label("side/main peaks (dB)"); 94 | for (int i = 1; i <= 10; ++i) bandwidthPeakPlot.x.minor(i); 95 | for (int i = -80; i <= 0; i += 20) bandwidthPeakPlot.y.minor(i); 96 | bandwidthEnergyPlot.x.copyFrom(bandwidthPeakPlot.x); 97 | bandwidthEnergyPlot.y.copyFrom(bandwidthPeakPlot.y).label("side/main energy (dB)"); 98 | bandwidthEnbwPlot.x.linear(0.1, 22).major(0.1, "").minors(5, 10, 15, 20).label("bandwidth"); 99 | bandwidthEnbwPlot.y.label("ENBW").linear(0, 3.5).major(0).minors(1, 2, 3); 100 | 101 | auto &linePeakPlain = bandwidthPeakPlot.line(); 102 | auto &lineEnergyPlain = bandwidthEnergyPlot.line(); 103 | auto &lineEnbwPlain = bandwidthEnbwPlot.line(); 104 | auto &linePeakForced = bandwidthPeakPlot.line(); 105 | auto &lineEnergyForced = bandwidthEnergyPlot.line(); 106 | auto &lineEnbwForced = bandwidthEnbwPlot.line(); 107 | bandwidthPeakPlot.legend(1, 1).add(linePeakPlain, "natural").add(linePeakForced, "forced P-R"); 108 | bandwidthEnergyPlot.legend(1, 1).add(lineEnergyPlain, "natural").add(lineEnergyForced, "forced P-R"); 109 | bandwidthEnbwPlot.legend(1, 0).add(lineEnbwPlain, "natural").add(lineEnbwForced, "forced P-R"); 110 | 111 | for (double b = 0.1; b < 22; b += 0.1) { 112 | auto confined = Confined::withBandwidth(b); 113 | auto stats = measureWindow(confined, b); 114 | auto statsForced = measureWindow(confined, b, true); 115 | 116 | { 117 | double energyDb = energyToDb(stats.sideEnergy/(stats.mainEnergy + 1e-100)); 118 | lineEnergyPlain.add(b, energyDb); 119 | 120 | double peakDb = ampToDb(stats.sidePeak/(stats.mainPeak + 1e-100)); 121 | linePeakPlain.add(b, peakDb); 122 | } 123 | { 124 | double energyDb = energyToDb(statsForced.sideEnergy/(statsForced.mainEnergy + 1e-100)); 125 | lineEnergyForced.add(b, energyDb); 126 | 127 | double peakDb = ampToDb(statsForced.sidePeak/(statsForced.mainPeak + 1e-100)); 128 | linePeakForced.add(b, peakDb); 129 | } 130 | 131 | lineEnbwPlain.add(b, stats.enbw); 132 | lineEnbwForced.add(b, statsForced.enbw); 133 | } 134 | return test.pass(); 135 | } 136 | -------------------------------------------------------------------------------- /tests/windows/kaiser-plots.py: -------------------------------------------------------------------------------- 1 | from numpy import * 2 | from numpy.fft import rfft 3 | 4 | import article 5 | 6 | ###### Kaiser windows with neat ratios, and spectral analysis 7 | 8 | def getSpectrum(x, oversample=64): 9 | padded = concatenate((x, zeros(len(x)*(oversample - 1)))) 10 | 11 | spectrum = rfft(padded) 12 | db = 20*log10(abs(spectrum) + 1e-30) 13 | db -= db[0] 14 | bins = arange(len(spectrum))*(1.0/oversample) 15 | return bins, db 16 | 17 | for name in ["kaiser-windows", "kaiser-windows-heuristic", "kaiser-windows-heuristic-pr"]: 18 | columns, data = article.readCsv("%s.csv"%name) 19 | figure, (timeAxes, freqAxes) = article.medium(2) 20 | for i in range(1, len(columns)): 21 | timeAxes.plot(data[0], data[i], label="%sx bandwidth"%columns[i]) 22 | bins, db = getSpectrum(data[i]) 23 | freqAxes.plot(bins, db) 24 | timeAxes.set(ylim=[-0.1, 1.1]) 25 | freqAxes.set(ylim=[-100, 1], xlim=[0, 6], xlabel="bin", ylabel="dB") 26 | figure.save("%s.svg"%name) 27 | -------------------------------------------------------------------------------- /tests/windows/window-stats.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "fft.h" 5 | #include "windows.h" 6 | 7 | static double ampToDb(double energy) { 8 | return 20*std::log10(energy + 1e-100); 9 | } 10 | static double energyToDb(double energy) { 11 | return 10*std::log10(energy + 1e-100); 12 | } 13 | 14 | struct SidelobeStats { 15 | double mainPeak = 0, sidePeak = 0; 16 | double mainEnergy = 0, sideEnergy = 0; 17 | double ratioPeak = 0, ratioEnergy = 0; 18 | double enbw = 0; 19 | }; 20 | template 21 | SidelobeStats measureWindow(const Window &windowObj, double bandwidth, bool forcePR=false) { 22 | SidelobeStats result; 23 | 24 | int length = 256; 25 | int oversample = 64; 26 | signalsmith::fft::RealFFT realFft(length*oversample); 27 | std::vector window(length*oversample, 0); 28 | 29 | windowObj.fill(window, length); // Leave the rest as zero padding (oversamples the frequency domain) 30 | if (forcePR) { 31 | signalsmith::windows::forcePerfectReconstruction(window, length, length/bandwidth); 32 | } 33 | 34 | double sum = 0, sum2 = 0; 35 | for (auto s : window) { 36 | sum += s; 37 | sum2 += s*s; 38 | } 39 | result.enbw = length*sum2/(sum*sum); 40 | 41 | std::vector> spectrum(window.size()/2); 42 | realFft.fft(window, spectrum); 43 | 44 | for (size_t b = 0; b < spectrum.size(); ++b) { 45 | double freq = b*1.0/oversample; 46 | double energy = std::norm(spectrum[b]); 47 | double abs = std::sqrt(energy); 48 | if (freq <= bandwidth*0.5) { 49 | result.mainPeak = std::max(abs, result.mainPeak); 50 | result.mainEnergy += energy; 51 | } else { 52 | result.sidePeak = std::max(abs, result.sidePeak); 53 | result.sideEnergy += energy; 54 | } 55 | } 56 | result.ratioPeak = result.sidePeak/(result.mainPeak + 1e-100); 57 | result.ratioEnergy = result.sideEnergy/(result.mainEnergy + 1e-100); 58 | return result; 59 | } 60 | -------------------------------------------------------------------------------- /util/console-colours.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _CONSOLE_COLOURS_H 3 | #define _CONSOLE_COLOURS_H 4 | 5 | #include 6 | 7 | namespace Console { 8 | std::string Reset = "\x1b[0m"; 9 | std::string Bright = "\x1b[1m"; 10 | std::string Dim = "\x1b[2m"; 11 | std::string Underscore = "\x1b[4m"; 12 | std::string Blink = "\x1b[5m"; 13 | std::string Reverse = "\x1b[7m"; 14 | std::string Hidden = "\x1b[8m"; 15 | 16 | namespace Foreground { 17 | std::string Black = "\x1b[30m"; 18 | std::string Red = "\x1b[31m"; 19 | std::string Green = "\x1b[32m"; 20 | std::string Yellow = "\x1b[33m"; 21 | std::string Blue = "\x1b[34m"; 22 | std::string Magenta = "\x1b[35m"; 23 | std::string Cyan = "\x1b[36m"; 24 | std::string White = "\x1b[37m"; 25 | } 26 | 27 | namespace Background { 28 | std::string Black = "\x1b[40m"; 29 | std::string Red = "\x1b[41m"; 30 | std::string Green = "\x1b[42m"; 31 | std::string Yellow = "\x1b[43m"; 32 | std::string Blue = "\x1b[44m"; 33 | std::string Magenta = "\x1b[45m"; 34 | std::string Cyan = "\x1b[46m"; 35 | std::string White = "\x1b[47m"; 36 | } 37 | 38 | using namespace Foreground; 39 | } 40 | 41 | #endif -------------------------------------------------------------------------------- /util/csv-writer.h: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_CSV_WRITER 2 | #define UTIL_CSV_WRITER 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class CsvWriter { 9 | std::ofstream csvFile; 10 | bool newLine = true; 11 | 12 | template 13 | void writeValue(V &&v) { 14 | std::stringstream strstr; 15 | strstr << v; 16 | std::string str = strstr.str(); 17 | bool needsQuote = false; 18 | for (unsigned i = 0; i < str.size(); ++i) { 19 | if (str[i] == ',') { 20 | needsQuote = true; 21 | break; 22 | } 23 | } 24 | if (needsQuote) { 25 | csvFile << "\""; 26 | for (unsigned i = 0; i < str.size(); ++i) { 27 | if (str[i] == '"') { 28 | csvFile << '"'; 29 | } 30 | csvFile << str[i]; 31 | } 32 | csvFile << "\""; 33 | } else { 34 | csvFile << v; 35 | } 36 | } 37 | 38 | void writeValue(const double &v) { 39 | csvFile << v; 40 | } 41 | void writeValue(const float &v) { 42 | csvFile << v; 43 | } 44 | public: 45 | CsvWriter(std::string name) : csvFile(name + ".csv") {} 46 | 47 | CsvWriter & write() { 48 | return *this; 49 | } 50 | template 51 | CsvWriter & write(const First &v, Args ...args) { 52 | if (!newLine) csvFile << ","; 53 | newLine = false; 54 | writeValue(v); 55 | return write(args...); 56 | } 57 | template 58 | CsvWriter & line(T... t) { 59 | write(t...); 60 | csvFile << "\n"; 61 | newLine = true; 62 | return *this; 63 | } 64 | }; 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /util/stopwatch.h: -------------------------------------------------------------------------------- 1 | #ifndef SIGNALSMITH_STOPWATCH_UTIL_H 2 | #define SIGNALSMITH_STOPWATCH_UTIL_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef WINDOWS // completely untested! 9 | # include 10 | class Stopwatch { 11 | using Time = __int64; 12 | inline Time now() { 13 | LARGE_INTEGER result; 14 | QueryPerformanceCounter(&result); 15 | return result.QuadPart; 16 | } 17 | static double timeToSeconds(double t) { 18 | LARGE_INTEGER freq; 19 | QueryPerformanceFrequency(&freq); 20 | return t/double(freq); 21 | } 22 | #else 23 | # include 24 | class Stopwatch { 25 | using Time = std::clock_t; 26 | inline Time now() { 27 | return std::clock(); 28 | } 29 | static double timeToSeconds(double t) { 30 | return t/double(CLOCKS_PER_SEC); 31 | } 32 | #endif 33 | 34 | std::atomic