├── dsp ├── include │ └── signalsmith-dsp │ │ ├── fft.h │ │ ├── mix.h │ │ ├── perf.h │ │ ├── curves.h │ │ ├── delay.h │ │ ├── filters.h │ │ ├── rates.h │ │ ├── windows.h │ │ ├── envelopes.h │ │ └── spectral.h ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── common.h ├── perf.h ├── rates.h └── mix.h ├── .gitignore ├── Doxyfile ├── tests ├── common.h ├── envelopes │ ├── peak-hold.py │ ├── plain-plots.py │ ├── 04-peak-decay.cpp │ ├── cubic-lfo.py │ ├── box-stack.py │ ├── 01-box-sum-average.cpp │ ├── 00-cubic-lfo.cpp │ ├── 02-box-stack.cpp │ └── 03-peak-hold.cpp ├── common │ └── version.cpp ├── perf │ ├── perf-lagrange.py │ ├── 00-utils.cpp │ └── 01-lagrange-methods.cpp ├── delay │ ├── 03-multi-delay.cpp │ ├── 00-buffer.cpp │ ├── 02-multi-buffer.cpp │ └── fractional-delay.py ├── windows │ ├── kaiser-plots.py │ ├── window-stats.h │ └── 01-approx-confined-gaussian.cpp ├── fft │ ├── 00-fft.py │ └── fft-errors-numpy.py ├── mix │ ├── cheap-fade.cpp │ ├── stereo-multi.cpp │ └── mixing-matrix.cpp ├── curves │ ├── 00-linear.cpp │ ├── 03-reciprocal-bark.cpp │ ├── 02-reciprocal.cpp │ └── 01-cubic-segments.cpp ├── filters │ ├── 03-gain.cpp │ ├── 04-allpass.cpp │ ├── 01-bandpass.cpp │ ├── 02-responses.cpp │ ├── 06-spec-equivalence.cpp │ ├── 05-shelves.cpp │ ├── filter-tests.h │ ├── plots.cpp │ ├── 07-accuracy.cpp │ └── 00-highpass-lowpass.cpp ├── spectral │ ├── 03-stft-processor.cpp │ └── 01-windowed-fft.cpp └── rates │ └── 00-kaiser-sinc.cpp ├── LICENSE.txt ├── benchmarks ├── fft │ ├── plots.py │ └── complex.cpp └── envelopes-peak-hold │ ├── plots.py │ ├── peak-hold.cpp │ ├── _previous │ ├── signalsmith-run-length-amortised.h │ └── signalsmith-constant-v1.h │ └── _others │ └── kvr-vadim-zavalishin.h ├── manual ├── filters │ └── responses │ │ ├── README.md │ │ ├── Makefile │ │ ├── wasm-api.h │ │ ├── index.html │ │ ├── main.cpp │ │ └── wasm-api.js └── fractional-delay.html ├── historical-docs.sh ├── util ├── console-colours.h ├── csv-writer.h ├── stopwatch.h └── test │ ├── benchmarks.h │ └── main.cpp ├── git-sub-branch ├── README.md ├── extra-style.js ├── version.py ├── index.html └── Makefile /dsp/include/signalsmith-dsp/fft.h: -------------------------------------------------------------------------------- 1 | #include "../../fft.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/mix.h: -------------------------------------------------------------------------------- 1 | #include "../../mix.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/perf.h: -------------------------------------------------------------------------------- 1 | #include "../../perf.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/curves.h: -------------------------------------------------------------------------------- 1 | #include "../../curves.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/delay.h: -------------------------------------------------------------------------------- 1 | #include "../../delay.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/filters.h: -------------------------------------------------------------------------------- 1 | #include "../../filters.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/rates.h: -------------------------------------------------------------------------------- 1 | #include "../../rates.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/windows.h: -------------------------------------------------------------------------------- 1 | #include "../../windows.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/envelopes.h: -------------------------------------------------------------------------------- 1 | #include "../../envelopes.h" 2 | -------------------------------------------------------------------------------- /dsp/include/signalsmith-dsp/spectral.h: -------------------------------------------------------------------------------- 1 | #include "../../spectral.h" 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /dsp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | 3 | add_library(signalsmith-dsp INTERFACE) 4 | target_include_directories(signalsmith-dsp INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) -------------------------------------------------------------------------------- /Doxyfile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = "Signalsmith Audio's DSP Library" 2 | PROJECT_NUMBER = 1.7.0 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 -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /tests/common/version.cpp: -------------------------------------------------------------------------------- 1 | #include "delay.h" // Anything that includes "common.h" should do 2 | 3 | SIGNALSMITH_DSP_VERSION_CHECK(1, 7, 0) 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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | echo "final clean test/doxygen" 33 | if make clean test doxygen 34 | then 35 | cp extra-style.js html/ 36 | else 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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, Sample errorLimit=1e-5) { 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 | { 21 | auto expected = a*b; 22 | auto actual = signalsmith::perf::mul(a, b); 23 | auto limit = errorLimit*(std::abs(expected) + Sample(1e-2)); 24 | if (std::abs(expected - actual) > limit) return test.fail("multiplication"); 25 | } 26 | { 27 | auto expected = a*std::conj(b); 28 | auto actual = signalsmith::perf::mul(a, b); 29 | auto limit = errorLimit*(std::abs(expected) + Sample(1e-2)); 30 | if (std::abs(expected - actual) > limit) return test.fail("conjugate multiplication"); 31 | } 32 | } 33 | } 34 | 35 | TEST("Complex multiplcation") { 36 | testComplex(test, 1e-6); 37 | testComplex(test, 1e-12); 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, 7, 0) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 7 22 | #define SIGNALSMITH_DSP_VERSION_PATCH 0 23 | #define SIGNALSMITH_DSP_VERSION_STRING "1.7.0" 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 == 7 && SIGNALSMITH_DSP_VERSION_PATCH == 0, "multiple versions of the Signalsmith DSP library"); 47 | #endif // include guard 48 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | 57 | TEST("Copying response/coeffs") { 58 | signalsmith::filters::BiquadStatic filterA, filterB; 59 | 60 | filterA.lowpassQ(0.2, 4); 61 | filterB(1); 62 | 63 | filterB.copyFrom(filterA); 64 | // Response is copied exactly 65 | for (float f = 0; f < 0.5; f += 0.01) { 66 | TEST_ASSERT(filterA.response(f) == filterB.response(f)); 67 | } 68 | // But not the state 69 | TEST_ASSERT(filterA(0) != filterB(0)); 70 | } 71 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | versionDefines = [ 2 | "#define SIGNALSMITH_DSP_VERSION_MAJOR ", 3 | "#define SIGNALSMITH_DSP_VERSION_MINOR ", 4 | "#define SIGNALSMITH_DSP_VERSION_PATCH " 5 | ] 6 | versionStringDefine = "#define SIGNALSMITH_DSP_VERSION_STRING \"%s\"" 7 | version = [0, 0, 0] 8 | 9 | # Read existing version from `#define`s 10 | with open("dsp/common.h") as commonH: 11 | for line in commonH: 12 | for i in range(3): 13 | if line.startswith(versionDefines[i]): 14 | version[i] = int(line[len(versionDefines[i]):]) 15 | 16 | startVersion = list(version) 17 | 18 | import sys 19 | if len(sys.argv) > 1: 20 | action = sys.argv[1] 21 | if action == "bump-patch": 22 | version[2] += 1 23 | elif action == "bump-minor": 24 | version[1] += 1 25 | version[2] = 0 26 | elif action == "bump-major": 27 | version[0] += 1 28 | version[1] = 0 29 | version[2] = 0 30 | else: 31 | print("Unrecognised action: " + action) 32 | exit(1) 33 | 34 | oldVersion = ".".join([str(x) for x in startVersion]) 35 | newVersion = ".".join([str(x) for x in version]) 36 | 37 | def fileReplace(filename, fromText, toText): 38 | text = "" 39 | with open(filename) as textfile: 40 | text = textfile.read() 41 | if len(text) == 0: 42 | return 43 | text = text.replace(fromText, toText) 44 | with open(filename, 'w') as textfile: 45 | textfile.write(text) 46 | 47 | for i in range(3): 48 | fileReplace("dsp/common.h", versionDefines[i] + str(startVersion[i]), versionDefines[i] + str(version[i])) 49 | fileReplace("dsp/common.h", versionStringDefine%oldVersion, versionStringDefine%newVersion) 50 | exactVersionCheck = "SIGNALSMITH_DSP_VERSION_MAJOR == %i && SIGNALSMITH_DSP_VERSION_MINOR == %i && SIGNALSMITH_DSP_VERSION_PATCH == %i"; 51 | fileReplace("dsp/common.h", 52 | exactVersionCheck%tuple(startVersion), 53 | exactVersionCheck%tuple(version)) 54 | 55 | fileReplace("dsp/README.md", 56 | "SIGNALSMITH_DSP_VERSION_CHECK(%i, %i, %i)"%tuple(startVersion), 57 | "SIGNALSMITH_DSP_VERSION_CHECK(%i, %i, %i)"%tuple(version)) 58 | 59 | fileReplace("Doxyfile", "PROJECT_NUMBER = " + oldVersion, "PROJECT_NUMBER = " + newVersion) 60 | fileReplace("tests/common/version.cpp", 61 | "SIGNALSMITH_DSP_VERSION_CHECK(%i, %i, %i)"%tuple(startVersion), 62 | "SIGNALSMITH_DSP_VERSION_CHECK(%i, %i, %i)"%tuple(version)) 63 | fileReplace("tests/common/version.cpp", 64 | "static_assert(signalsmith::version(%i, %i, %i)"%tuple(startVersion), 65 | "static_assert(signalsmith::version(%i, %i, %i)"%tuple(version)) 66 | 67 | print(newVersion) 68 | -------------------------------------------------------------------------------- /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