├── FUNDING.yml ├── setup.py ├── pyproject.toml ├── LICENSE ├── tests └── test.py ├── notebooks ├── noFFT_python.py ├── noFFT_utils.py ├── Chromas.ipynb ├── Spectrograms.ipynb └── MFCCs.ipynb ├── README.md ├── .gitignore └── src ├── ResonatorBank.hpp ├── resonate.cpp └── ResonatorBank.cpp /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [alexandrefrancois] 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Available at setup time due to pyproject.toml 2 | from pybind11.setup_helpers import Pybind11Extension, build_ext 3 | from setuptools import setup, find_packages 4 | 5 | ext_modules = [ 6 | Pybind11Extension( 7 | "noFFT", 8 | ["src/resonate.cpp", "src/ResonatorBank.cpp"], 9 | ), 10 | ] 11 | 12 | setup( 13 | name="noFFT", 14 | version="0.0.1", 15 | author="Alexandre R.J. Francois", 16 | author_email="alexandrefrancois@gmail.com", 17 | url="https://github.com/alexandrefrancois/noFFT", 18 | description="A reference implementation of the Resonate algorithm in C++ for Python", 19 | long_description="", 20 | packages=find_packages(), 21 | ext_modules=ext_modules, 22 | extras_require={"test": "pytest"}, 23 | cmdclass={"build_ext": build_ext}, 24 | zip_safe=False, 25 | python_requires=">=3.8", 26 | ) 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [project] 3 | name = "noFFT" 4 | version = "0.0.1" 5 | authors = [ 6 | { name="Alexandre R.J. Francois", email="alexandrefrancois@gmail.com" }, 7 | ] 8 | description = "A reference implementation of the Resonate algorithm in C++ for Python." 9 | readme = "README.md" 10 | requires-python = ">=3.8" 11 | classifiers = [ 12 | "Programming Language :: Python :: 3", 13 | "Operating System :: MacOS", 14 | ] 15 | license = {file = "LICENSE"} 16 | 17 | [project.urls] 18 | Homepage = "https://github.com/alexandrefrancois/noFFT" 19 | Issues = "https://github.com/alexandrefrancois/noFFT/issues" 20 | 21 | 22 | [build-system] 23 | requires = [ 24 | "setuptools>=61.0", 25 | "pybind11>=2.10.0", 26 | ] 27 | build-backend = "setuptools.build_meta" 28 | 29 | [tool.cibuildwheel] 30 | test-command = "python {project}/tests/test.py" 31 | test-skip = "*universal2:arm64" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Alexandre R.J. Francois 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from noFFT import resonate 3 | 4 | 5 | fmin = 32.70 6 | n_bins = 84 7 | bins_per_octave = 12 8 | 9 | sr = 44100.0 # in Hz 10 | duration = 0.1 # in s 11 | n_points = int(sr * duration) 12 | 13 | # Sinusoid input signal 14 | ifreq = 440 15 | signal = np.cos(2 * np.pi * ifreq * np.linspace(0.0, duration, num=n_points)) 16 | 17 | float_y = np.array(signal, dtype=np.float32) 18 | # print(float_y.dtype) 19 | 20 | freqs = fmin * 2.0 ** (np.r_[0:n_bins] / float(bins_per_octave)) 21 | float_fs = np.array(freqs, dtype=np.float32) 22 | 23 | # alphas = 0.001 * np.ones_like(freqs) 24 | alphas = 1 - np.exp(-(1 / sr) * freqs / np.log10(freqs)) 25 | float_as = np.array(alphas, dtype=np.float32) 26 | 27 | hop_length = 256 28 | float_R = resonate(float_y, sr, float_fs, float_as, float_as, hop_length) 29 | 30 | R = np.array(float_R, dtype=np.float64) 31 | R = R.reshape((-1, n_bins)).T 32 | 33 | # shape of R: 34 | # each slice of size twoNumResonators has 35 | # numResonators Re values and numResonators Im values (split complex) 36 | # print(R.shape) 37 | # print(R.dtype) 38 | 39 | assert R.shape[0] == n_bins 40 | assert R.shape[1] == int(2 * n_points / hop_length) 41 | -------------------------------------------------------------------------------- /notebooks/noFFT_python.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # Compute a resonator bank outputs from a single frequency sinusoidal input signal (impulse) 4 | # ifreq: input signal frequency 5 | # rfreqs: resonant frequencies for each resonator 6 | # alphas: EMWA parameters for each resonator 7 | # betas: EMWA parameters for smoothing step 8 | # sr: sampling rate 9 | # duration: duration of synthesyzed signal 10 | def resonate_python(ifreq, rfreqs, alphas, betas, sr, duration): 11 | numpoints = int(sr*duration) 12 | signal = np.cos(2*np.pi*ifreq * np.linspace(0.0, duration, num=numpoints)) 13 | nfreqs = rfreqs.shape[0] 14 | 15 | # Oscillator values 16 | start = np.zeros(nfreqs) 17 | stop = duration * np.ones(nfreqs) 18 | linsp = np.linspace(start=start, stop=stop, num=numpoints) 19 | omegasm = -2*np.pi*rfreqs * linsp 20 | 21 | # components of the resonator complex value, initialized to e^(i*omega) 22 | # this corresponds to the phasor in the online implementation 23 | cosines = (signal * np.cos(omegasm).T).T 24 | sines = (signal * np.sin(omegasm).T).T 25 | 26 | # initial value 0 27 | cosines[0] = 0.0 28 | sines[0] = 0.0 29 | 30 | # smoothed output 31 | outputs = np.zeros(cosines.shape) 32 | angles = np.zeros(cosines.shape) 33 | 34 | omas = 1.0 - alphas 35 | omb = 1.0 - betas 36 | 37 | # only do one loop and compute powers 38 | for i in range(1, numpoints): 39 | cosines[i] = omas * cosines[i-1] + alphas * cosines[i] 40 | sines[i] = omas * sines[i-1] + alphas * sines[i] 41 | # output is powers 42 | outputs[i] = omb * outputs[i-1] + betas * (cosines[i] * cosines[i] + sines[i] * sines[i]) 43 | angles[i] = np.atan2(sines[i], cosines[i]) 44 | 45 | # print("outputs shape:", outputs.shape) 46 | 47 | # max power is 0.25 - max amplitude is 0.5 48 | return outputs, angles 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # noFFT 2 | 3 | A reference implementation of the [Resonate](https://alexandrefrancois.org/Resonate) 4 | algorithm in C++ for Python, 5 | using [pybind11](https://pybind11.readthedocs.io/en/stable/). 6 | The C++ code uses the Accelerate framework and will only work on Mac/iOS platforms. 7 | 8 | This is a crude version to demonstrate the capabilities of the algorithm through Jupyter notebooks. 9 | 10 | The goal is to turn this into a proper Python package - contributions welcome! 11 | 12 | Author: Alexandre R.J. François 13 | 14 | ## Installation 15 | 16 | - download, checkout or clone this repository 17 | - pip install . 18 | 19 | - see notebooks and additional Python functions for usage 20 | 21 | ## Resonate Functions 22 | 23 | The C++ code implements a resonator bank class similar to that provided in the 24 | [Oscillators Swift package](https://github.com/alexandrefrancois/Oscillators) 25 | with vectorized update per sample. 26 | 27 | Python functions: 28 | - `resonate`: wraps creating the bank with the parameters provided and running the updates for an input signal. 29 | - `resonate_wrapper`: computes a resonator bank outputs from an input signal, using the C++ implementation. 30 | - `resonate_python`: computes a resonator bank outputs from a single frequency sinusoidal input signal (impulse). The loop over samples is done in Python, so much slower than the C++ counterpart. 31 | 32 | 33 | ## Jupyter Notebooks 34 | 35 | - **SpectralAnalysisExperiments**: code to analyze and plot resonator and resonator bank properties. 36 | - **Spectrograms**: code to compute and plot spectrograms of audio signals, using [Librosa](https://librosa.org) 37 | - **Chromas**: code to compute and plot chromas and chromagrams on audio signals, using [Librosa](https://librosa.org) 38 | - **MFCCs**: code to compute and plot mel frequency scale spectrograms and chromagrams on audio signals, using [Librosa](https://librosa.org) 39 | 40 | 41 | ## License 42 | 43 | MIT License 44 | 45 | Copyright (c) 2025 Alexandre R.J. Francois 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Using https://github.com/github/gitignore/blob/master/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | 139 | # Cython debug symbols 140 | cython_debug/ 141 | 142 | 143 | ### OSX ### 144 | # General 145 | .DS_Store 146 | .AppleDouble 147 | .LSOverride 148 | 149 | # Test data 150 | audiofiles/test_* 151 | data/test_* -------------------------------------------------------------------------------- /notebooks/noFFT_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from noFFT import resonate 3 | 4 | 5 | def log_frequencies(fmin=32.7, n_freqs=84, freqs_per_octave=12): 6 | return fmin * 2.0 ** (np.r_[0:n_freqs] / float(freqs_per_octave)) 7 | 8 | def alphas_heuristic(frequencies, sr, k = 1): 9 | return 1 - np.exp(- (1/sr) * frequencies / (k * np.log10(1+frequencies))) 10 | 11 | 12 | # Compute a resonator bank outputs from an input signal 13 | # Uses C++ implementation of update - betas = alphas 14 | # y: input signal 15 | # sr: sampling rate 16 | # frequencies: resonant frequencies for each resonator 17 | # alphas: EMWA parameters for each resonator 18 | # hop_length: number of samples between ech output sample 19 | def resonate_wrapper(y, sr, frequencies, alphas, hop_length=1, output_type='powers'): 20 | float_y = np.array(y, dtype=np.float32) 21 | float_fs = np.array(frequencies, dtype=np.float32) 22 | float_as = np.array(alphas, dtype=np.float32) 23 | 24 | # betas = alphas 25 | float_Rsc = resonate(float_y, sr, float_fs, float_as, float_as, hop_length) 26 | 27 | Rsc = np.array(float_Rsc, dtype=np.float64) 28 | nfreqs = frequencies.shape[0] 29 | Rsc = Rsc.reshape((-1, (2 * nfreqs))) 30 | # compute complex values in the right shape 31 | re = Rsc[...,:nfreqs] 32 | im = Rsc[...,nfreqs:] 33 | 34 | if output_type == 'powers': 35 | # max powers is 0.25 36 | return re * re + im * im 37 | if output_type == 'amplitudes': 38 | # max amplitude is 0.5 39 | return np.sqrt(re * re + im * im) 40 | 41 | # default: return complex vector 42 | Rcx = re + im * 1j 43 | Rcx = Rcx 44 | return Rcx 45 | 46 | # Compute a resonator bank outputs from a single frequency sinusoidal input signal (impulse) 47 | # Uses C++ implementation of update - betas = alphas 48 | # ifreq: input signal frequency 49 | # rfreqs: resonant frequencies for each resonator 50 | # alphas: EMWA parameters for each resonator 51 | # sr: sampling rate 52 | # duration: duration of synthesyzed signal 53 | def frequency_response(ifreq, rfreqs, alphas, sr, duration, output_type = 'powers'): 54 | # make input signal: single frequency step 55 | numpoints = int(sr*duration) 56 | signal = np.cos(2*np.pi*ifreq * np.linspace(0.0, duration, num=numpoints)) 57 | hop_length = 1 58 | 59 | return resonate_wrapper( 60 | y=signal, sr=sr, frequencies=rfreqs, alphas=alphas, hop_length=hop_length, output_type=output_type) 61 | 62 | 63 | # Compute a resonator bank's equalizer coefficients by performing a frequency sweep 64 | # Calls frequency_response for each resonator frequency 65 | # Uses C++ implementation of update - betas = alphas 66 | # frequencies: resonant frequencies for each resonator 67 | # alphas: EMWA parameters for each resonator 68 | # sr: sampling rate 69 | # duration: duration of synthesyzed signal 70 | def frequency_sweep(frequencies, alphas, sr): 71 | taus = -1.0 / (np.log(1 - alphas) * sr) 72 | areas_p = np.zeros_like(frequencies) 73 | for idx, ifreq in enumerate(frequencies): 74 | duration = 40 * taus[idx] 75 | r = frequency_response(ifreq=ifreq, rfreqs=frequencies, alphas=alphas, sr=sr, duration=duration, output_type='powers') 76 | areas_p[idx] = np.sum(r[-1], axis=0) 77 | return 0.25 / np.sqrt(areas_p) 78 | 79 | -------------------------------------------------------------------------------- /src/ResonatorBank.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 Alexandre R. J. Francois 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #ifndef ResonatorBank_hpp 26 | #define ResonatorBank_hpp 27 | 28 | #include 29 | 30 | namespace resonate 31 | { 32 | 33 | class ResonatorBank 34 | { 35 | private: 36 | float m_sampleRate; 37 | size_t m_numResonators; 38 | float *m_frequencies; 39 | float *m_alphas; 40 | float *m_omAlphas; 41 | float *m_betas; 42 | float *m_omBetas; 43 | 44 | size_t m_twoNumResonators; 45 | 46 | /// Accumulated resonance values, non-interlaced real (cos) | imaginary (sin) parts 47 | float *m_rPtr; 48 | /// Smoothed accumulated resonance values, non-interlaced real (cos) | imaginary (sin) parts 49 | float *m_rrPtr; 50 | 51 | /// Phasors 52 | float *m_zPtr; 53 | /// Phasor multipliers 54 | float *m_wPtr; 55 | 56 | /// hold sample value * alphas 57 | float *m_alphasSample; 58 | 59 | /// Squared magnitudes buffer (ntermediate calculations) 60 | float *m_smPtr; 61 | /// Reverse square root buffer (intermediate calculations) 62 | float *m_rsqrtPtr; 63 | 64 | public: 65 | ResonatorBank &operator=(const ResonatorBank &) = delete; 66 | ResonatorBank(const ResonatorBank &) = delete; 67 | 68 | ResonatorBank(size_t numResonators, const float *frequencies, const float *alphas, const float *betas, float sampleRate); 69 | ~ResonatorBank(); 70 | 71 | float sampleRate() { return m_sampleRate; } 72 | size_t numResonators() { return m_numResonators; } 73 | float frequencyValue(size_t index); 74 | float alphaValue(size_t index); 75 | void setAllAlphas(float alpha); 76 | float timeConstantValue(size_t index); 77 | 78 | void getComplex(float *dest, size_t size); 79 | void getPowers(float *dest, size_t size); 80 | void getAmplitudes(float *dest, size_t size); 81 | 82 | void update(const float sample); 83 | void update(const std::vector &samples); 84 | void update(const float *frameData, size_t frameLength, size_t sampleStride); 85 | void update(const float *frameData, size_t frameLength, size_t sampleStride, float *powers, float *amplitudes); 86 | 87 | void stabilize(); 88 | }; 89 | 90 | } // resonate 91 | 92 | #endif /* ResonatorBank_hpp */ 93 | -------------------------------------------------------------------------------- /src/resonate.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 Alexandre R. J. Francois 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include 26 | #include 27 | 28 | #include "./ResonatorBank.hpp" 29 | 30 | namespace py = pybind11; 31 | 32 | // #include 33 | 34 | namespace resonate 35 | { 36 | py::array_t resonate(py::array_t input, float samplingRate, py::array_t freqs, py::array_t alphas, py::array_t betas, int hopLength) 37 | { 38 | py::buffer_info buf = input.request(), fs = freqs.request(), as = alphas.request(), bs = betas.request(); 39 | 40 | if (buf.ndim != 1 || fs.ndim != 1 || as.ndim != 1 || bs.ndim != 1) 41 | throw std::runtime_error("Number of dimensions must be one"); 42 | 43 | if (fs.size != as.size) 44 | throw std::runtime_error("Frequencies and alphas must have same size"); 45 | 46 | if (as.size != bs.size) 47 | throw std::runtime_error("Alphas and betas must have same size"); 48 | 49 | size_t numSamples = buf.size; 50 | // std::cout << "Num samples: " << numSamples << std::endl; 51 | 52 | size_t numResonators = fs.size; 53 | // std::cout << "Num resonators: " << numResonators << std::endl; 54 | size_t twoNumResonators = 2 * numResonators; 55 | 56 | size_t numSlices = int(numSamples / hopLength); 57 | // std::cout << "Num slices: " << numSlices << std::endl; 58 | 59 | auto result = py::array_t(numSlices * twoNumResonators); // we are returning complex values 60 | py::buffer_info rs = result.request(); 61 | 62 | const float *frequenciesPtr = static_cast(fs.ptr); 63 | const float *alphasPtr = static_cast(as.ptr); 64 | const float *betasPtr = static_cast(bs.ptr); 65 | float *resultPtr = static_cast(rs.ptr); 66 | 67 | const float *inputPtr = static_cast(buf.ptr); 68 | 69 | ResonatorBank bank(numResonators, frequenciesPtr, alphasPtr, betasPtr, samplingRate); 70 | 71 | for (size_t idx = 0; idx < numSlices; idx++) 72 | { 73 | size_t inputOffset = idx * hopLength; 74 | bank.update(inputPtr + inputOffset, hopLength, 1); 75 | size_t resultOffset = idx * twoNumResonators; 76 | bank.getComplex(resultPtr + resultOffset, twoNumResonators); 77 | } 78 | 79 | // shape of results: 80 | // each slice of size twoNumResonators has 81 | // numResonators Re values and numResonators Im values (split complex) 82 | 83 | return result; 84 | } 85 | } 86 | 87 | PYBIND11_MODULE(noFFT, m) 88 | { 89 | m.def("resonate", &resonate::resonate, "resonate"); 90 | } 91 | -------------------------------------------------------------------------------- /notebooks/Chromas.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Chroma and Chromagrams\n", 8 | "\n", 9 | "Alexandre R.J. Francois" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "from matplotlib import colormaps as mcm\n", 20 | "import matplotlib.pyplot as plt\n", 21 | "import librosa\n", 22 | "import soundfile as sf\n", 23 | "\n", 24 | "from noFFT_utils import log_frequencies, alphas_heuristic, resonate_wrapper" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "#https://librosa.org/doc/main/recordings.html\n", 34 | "\n", 35 | "\n", 36 | "# y, sr = sf.read(librosa.ex(\"brahms\"), frames=100000)\n", 37 | "y, sr = sf.read(librosa.ex(\"vibeace\"))\n", 38 | "# y, sr = sf.read(librosa.ex(\"sweetwaltz\"))\n", 39 | "# y, sr = sf.read(librosa.ex(\"libri1\"))\n", 40 | "# y, sr = sf.read(librosa.ex(\"libri2\"))\n", 41 | "# y, sr = sf.read(librosa.ex(\"libri3\"))\n", 42 | "# y, sr = sf.read(librosa.ex(\"robin\"))\n", 43 | "\n", 44 | "librosa.display.waveshow(y, sr=sr)\n", 45 | "print(y.shape)\n", 46 | "print(y.dtype)\n", 47 | "\n", 48 | "# float_y = np.array(y, dtype=np.float32)\n", 49 | "# print(float_y.dtype)" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "fmin = 32.70\n", 59 | "n_freqs = 84\n", 60 | "freqs_per_octave = 12\n", 61 | "frequencies = log_frequencies(fmin=fmin, n_freqs=n_freqs, freqs_per_octave=freqs_per_octave)\n", 62 | "alphas = alphas_heuristic(frequencies, sr=sr, k=1)\n", 63 | "hop_length = 1\n", 64 | "dhl = 512 # hop length for display\n", 65 | "\n", 66 | "# print(frequencies)" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "R = resonate_wrapper(y=y, sr=sr, frequencies=frequencies, alphas=alphas, hop_length=hop_length, output_type='powers')\n", 76 | "print(R.shape, R.dtype)\n", 77 | "\n", 78 | "R_db = librosa.power_to_db(R.T, ref=np.max)\n", 79 | "\n", 80 | "# Single spectrogram\n", 81 | "librosa.display.specshow(\n", 82 | " R_db[:, ::dhl],\n", 83 | " sr=sr,\n", 84 | " fmin = frequencies[0],\n", 85 | " hop_length=dhl,\n", 86 | " y_axis=\"cqt_hz\",\n", 87 | " x_axis=\"time\",\n", 88 | ")\n" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "# chromagram\n", 98 | "\n", 99 | "# print(R.shape)\n", 100 | "# print(R)\n", 101 | "\n", 102 | "K = R.copy()\n", 103 | "# print(K.shape)\n", 104 | "# print(K)\n", 105 | "\n", 106 | "# print(K[0].shape)\n", 107 | "# print(K[10].shape)\n", 108 | "\n", 109 | "numChroma = 12\n", 110 | "numOctaves = int(K.shape[-1] / numChroma)\n", 111 | "# print(numChroma, numOctaves)\n", 112 | "\n", 113 | "C = K.reshape(K.shape[0], numOctaves, numChroma)\n", 114 | "# print(C.shape)\n", 115 | "\n", 116 | "C = C.sum(axis=1).T\n", 117 | "# print(D.shape, D)\n", 118 | "\n", 119 | "C = librosa.util.normalize(C, norm=np.inf, axis=-2)\n", 120 | "\n", 121 | "# print(C)\n", 122 | "# print(freqs[0:12])\n", 123 | "\n", 124 | "# Single spectrogram\n", 125 | "fig, ax = plt.subplots(figsize=(8, 2), dpi=100)\n", 126 | "librosa.display.specshow(\n", 127 | " C[:,::dhl],\n", 128 | " sr=sr,\n", 129 | " hop_length=dhl,\n", 130 | " fmin = frequencies[0],\n", 131 | " bins_per_octave=12,\n", 132 | " y_axis=\"chroma\",\n", 133 | " x_axis=\"s\",\n", 134 | " ax=ax,\n", 135 | ")\n", 136 | "ax.set(title=\"Resonate Chromagram\")" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "# Librosa Chroma\n", 146 | "\n", 147 | "# librosa.feature.chroma_stft(*, y=None, sr=22050, S=None, norm=inf, n_fft=2048, hop_length=512, win_length=None, window='hann', center=True, pad_mode='constant', tuning=None, n_chroma=12, **kwargs)\n", 148 | "chroma_stft = librosa.feature.chroma_stft(y=y, sr=sr, n_fft=4096, hop_length=dhl)\n", 149 | "print(chroma_stft.shape)\n", 150 | "\n", 151 | "# librosa.feature.chroma_cqt(*, y=None, sr=22050, C=None, hop_length=512, fmin=None, norm=inf, threshold=0.0, tuning=None, n_chroma=12, n_octaves=7, window=None, bins_per_octave=36, cqt_mode='full')\n", 152 | "chroma_cq = librosa.feature.chroma_cqt(y=y, sr=sr, hop_length=dhl, bins_per_octave=12)\n", 153 | "print(chroma_cq.shape)\n", 154 | "\n", 155 | "fig, ax = plt.subplots(nrows=2, sharex=True, figsize=(8, 5), dpi=100)\n", 156 | "librosa.display.specshow(chroma_stft, hop_length=dhl, y_axis='chroma', x_axis='s', ax=ax[0])\n", 157 | "ax[0].set(title='chroma_stft')\n", 158 | "ax[0].label_outer()\n", 159 | "img = librosa.display.specshow(chroma_cq, hop_length=dhl, y_axis='chroma', x_axis='s', ax=ax[1])\n", 160 | "ax[1].set(title='chroma_cqt')\n", 161 | "fig.colorbar(img, ax=ax)\n" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": null, 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "# Resonate Chroma\n", 171 | "\n", 172 | "# S = np.abs(librosa.stft(y))**2\n", 173 | "# S = np.abs(librosa.stft(y, n_fft=4096))**2\n", 174 | "# chroma_stft = librosa.feature.chroma_stft(S=S, sr=sr, n_chroma=12, n_fft=4096)\n", 175 | "\n", 176 | "chromaR = librosa.feature.chroma_cqt(C=R.T, sr=sr, bins_per_octave=12)\n", 177 | "\n", 178 | "print(sr, R.T.shape, chromaR.shape)\n", 179 | "\n", 180 | "fig, ax = plt.subplots(nrows=2, sharex=True, figsize=(8, 5), dpi=100)\n", 181 | "# img = librosa.display.specshow(librosa.power_to_db(S, ref=np.max), y_axis='log', x_axis='time', ax=ax[0])\n", 182 | "# img = librosa.display.specshow(librosa.power_to_db(R, ref=np.max), y_axis='log', x_axis='time', ax=ax[0])\n", 183 | "librosa.display.specshow(\n", 184 | " librosa.power_to_db(R.T[:,::dhl], ref=np.max),\n", 185 | " sr=sr,\n", 186 | " hop_length=dhl,\n", 187 | " y_axis=\"cqt_hz\",\n", 188 | " x_axis=\"s\",\n", 189 | " ax=ax[0]\n", 190 | ")\n", 191 | "fig.colorbar(img, ax=[ax[0]])\n", 192 | "ax[0].label_outer()\n", 193 | "\n", 194 | "img = librosa.display.specshow(\n", 195 | " chromaR[:,::dhl],\n", 196 | " sr=sr,\n", 197 | " hop_length=dhl,\n", 198 | " y_axis='chroma',\n", 199 | " x_axis='s',\n", 200 | " ax=ax[1])\n", 201 | "fig.colorbar(img, ax=[ax[1]])\n" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": null, 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [ 210 | "fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(8, 8), dpi=300)\n", 211 | "librosa.display.specshow(chroma_stft, hop_length=dhl, y_axis='chroma', x_axis='s', ax=ax[0])\n", 212 | "ax[0].set(title='Librosa chroma_stft')\n", 213 | "ax[0].label_outer()\n", 214 | "img = librosa.display.specshow(chroma_cq, hop_length=dhl, y_axis='chroma', x_axis='s', ax=ax[1])\n", 215 | "ax[1].set(title='Librosa chroma_cqt')\n", 216 | "ax[1].label_outer()\n", 217 | "img = librosa.display.specshow(\n", 218 | " chromaR[:,::dhl],\n", 219 | " sr=sr,\n", 220 | " hop_length=dhl,\n", 221 | " y_axis='chroma',\n", 222 | " x_axis='s',\n", 223 | " ax=ax[2])\n", 224 | "ax[2].set(title='Resonate chroma')\n", 225 | "fig.colorbar(img, ax=ax)\n" 226 | ] 227 | } 228 | ], 229 | "metadata": { 230 | "kernelspec": { 231 | "display_name": ".venv", 232 | "language": "python", 233 | "name": "python3" 234 | }, 235 | "language_info": { 236 | "codemirror_mode": { 237 | "name": "ipython", 238 | "version": 3 239 | }, 240 | "file_extension": ".py", 241 | "mimetype": "text/x-python", 242 | "name": "python", 243 | "nbconvert_exporter": "python", 244 | "pygments_lexer": "ipython3", 245 | "version": "3.13.2" 246 | } 247 | }, 248 | "nbformat": 4, 249 | "nbformat_minor": 2 250 | } 251 | -------------------------------------------------------------------------------- /notebooks/Spectrograms.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Spectrograms\n", 8 | "\n", 9 | "Alexandre R.J. Francois" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "from matplotlib import colormaps as mcm\n", 20 | "import matplotlib.pyplot as plt\n", 21 | "import librosa\n", 22 | "import soundfile as sf\n", 23 | "\n", 24 | "from noFFT_utils import log_frequencies, alphas_heuristic, resonate_wrapper, frequency_sweep\n" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "#https://librosa.org/doc/main/recordings.html\n", 34 | "\n", 35 | "\n", 36 | "# y, sr = sf.read(librosa.ex(\"brahms\"))\n", 37 | "y, sr = sf.read(librosa.ex(\"vibeace\"))\n", 38 | "# y, sr = sf.read(librosa.ex(\"sweetwaltz\"))\n", 39 | "# y, sr = sf.read(librosa.ex(\"libri1\"))\n", 40 | "# y, sr = sf.read(librosa.ex(\"libri2\"))\n", 41 | "# y, sr = sf.read(librosa.ex(\"libri3\"))\n", 42 | "# y, sr = sf.read(librosa.ex(\"robin\"))\n", 43 | "print(sr)\n", 44 | "\n", 45 | "librosa.display.waveshow(y, sr=sr)\n", 46 | "print(y.shape)\n", 47 | "print(y.dtype)\n", 48 | "\n", 49 | "# float_y = np.array(y, dtype=np.float32)\n", 50 | "# print(float_y.dtype)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "fmin = 32.70\n", 60 | "n_freqs = 100\n", 61 | "freqs_per_octave = 12\n", 62 | "\n", 63 | "frequencies = log_frequencies(fmin=fmin, n_freqs=n_freqs, freqs_per_octave=freqs_per_octave)\n", 64 | "if frequencies[-1] > sr / 2:\n", 65 | " print(\"Some frequencies higher than sampling rate / 2\")\n", 66 | "\n", 67 | "alphas = alphas_heuristic(frequencies, sr=sr, k=1)\n", 68 | "hop_length = 1\n", 69 | "dhl = 512 # hop length for display\n", 70 | "\n", 71 | "# print(frequencies.shape, frequencies)" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "Rcx = resonate_wrapper(y=y, sr=sr, frequencies=frequencies, alphas=alphas, hop_length=hop_length, output_type='complex')\n", 81 | "Rcx = Rcx.T\n", 82 | "print(Rcx.shape, Rcx.dtype)\n", 83 | "\n", 84 | "# compute powers\n", 85 | "R_pows = np.abs(Rcx) ** 2\n", 86 | "print(R_pows.shape, R_pows.dtype)\n", 87 | "\n", 88 | "R_db = librosa.power_to_db(R_pows, ref=np.max)\n", 89 | "\n", 90 | "# Single spectrogram\n", 91 | "fig, ax = plt.subplots(figsize=(8, 3), dpi=300)\n", 92 | "img = librosa.display.specshow(\n", 93 | " R_db[:, ::dhl],\n", 94 | " sr=sr,\n", 95 | " fmin = frequencies[0],\n", 96 | " hop_length=dhl,\n", 97 | " y_axis=\"cqt_hz\",\n", 98 | " x_axis=\"s\",\n", 99 | " ax=ax\n", 100 | ")\n", 101 | "fig.colorbar(img, ax=ax, format=\"%+2.f dB\")\n" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "# Equilizer coefficients\n", 111 | "# Frequency sweep\n", 112 | "\n", 113 | "eq = frequency_sweep(frequencies=frequencies, alphas=alphas, sr=sr)\n", 114 | "# print(eq.shape, eq)\n", 115 | "\n", 116 | "print(eq.shape, eq)" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "# Equalized spectrograms\n", 126 | "\n", 127 | "# Req = (eq * Rcx.T).T\n", 128 | "# Req_pows = np.abs(Req) ** 2\n", 129 | "\n", 130 | "Req_pows = (eq * R_pows.T).T\n", 131 | "\n", 132 | "Req_db = librosa.power_to_db(Req_pows, ref=np.max)\n", 133 | "\n", 134 | "# print(R_db.shape, Req_db.shape)\n", 135 | "\n", 136 | "diff = ((1-eq) * Rcx.T).T\n", 137 | "diff_pows = np.abs(diff) ** 2\n", 138 | "diff_db = librosa.power_to_db(diff_pows, ref=np.max)\n", 139 | "\n", 140 | "start = None\n", 141 | "end = None\n", 142 | "\n", 143 | "# spectrograms\n", 144 | "fig, ax = plt.subplots(nrows=2, sharex=True, figsize=(8, 8), dpi=300)\n", 145 | "img = librosa.display.specshow(\n", 146 | " R_db[:, start:end:dhl],\n", 147 | " sr=sr,\n", 148 | " fmin = frequencies[0],\n", 149 | " hop_length=dhl,\n", 150 | " bins_per_octave=freqs_per_octave,\n", 151 | " x_axis=\"time\",\n", 152 | " y_axis=\"cqt_hz\",\n", 153 | " ax=ax[0],\n", 154 | " # vmin=-80,\n", 155 | " # vmax=0,\n", 156 | ")\n", 157 | "fig.colorbar(img, ax=[ax[0]], format=\"%+2.f dB\")\n", 158 | "ax[0].set(title='Raw spectrogram')\n", 159 | "ax[0].label_outer()\n", 160 | "img = librosa.display.specshow(\n", 161 | " Req_db[:, start:end:dhl],\n", 162 | " sr=sr,\n", 163 | " fmin = frequencies[0],\n", 164 | " hop_length=dhl,\n", 165 | " bins_per_octave=freqs_per_octave,\n", 166 | " x_axis=\"time\",\n", 167 | " y_axis=\"cqt_hz\",\n", 168 | " ax=ax[1],\n", 169 | " # vmin=-40,\n", 170 | " # vmax=0,\n", 171 | ")\n", 172 | "fig.colorbar(img, ax=[ax[1]], format=\"%+2.f dB\")\n", 173 | "ax[1].set(title=\"Equalized spectrogram\")\n" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "# Librosa CQT spectrogram\n", 183 | "\n", 184 | "C = librosa.cqt(\n", 185 | " y=y,\n", 186 | " sr=sr,\n", 187 | " hop_length=dhl,\n", 188 | " fmin=fmin,\n", 189 | " n_bins=n_freqs,\n", 190 | " bins_per_octave=freqs_per_octave,\n", 191 | ")\n", 192 | "\n", 193 | "Cmods = np.abs(C)\n", 194 | "C_db = librosa.amplitude_to_db(Cmods, ref=np.max)\n", 195 | "\n", 196 | "print(C_db.shape)\n", 197 | "\n", 198 | "detnh = 50\n", 199 | "detns = detnh*dhl\n", 200 | "\n", 201 | "fig, ax = plt.subplots(nrows=2, ncols=2, sharex='col', figsize=(8, 6), dpi=300)\n", 202 | "img = librosa.display.specshow(\n", 203 | " C_db[:,:],\n", 204 | " sr=sr,\n", 205 | " hop_length=dhl,\n", 206 | " fmin=fmin,\n", 207 | " bins_per_octave=freqs_per_octave,\n", 208 | " y_axis=\"cqt_hz\",\n", 209 | " x_axis=\"s\",\n", 210 | " ax=ax[0][0],\n", 211 | ")\n", 212 | "ax[0][0].set(title=\"CQT\")\n", 213 | "ax[0][0].label_outer()\n", 214 | "img = librosa.display.specshow(\n", 215 | " C_db[:,0:detnh],\n", 216 | " sr=sr,\n", 217 | " hop_length=dhl,\n", 218 | " fmin=fmin,\n", 219 | " bins_per_octave=freqs_per_octave,\n", 220 | " y_axis=\"cqt_hz\",\n", 221 | " x_axis=\"s\",\n", 222 | " ax=ax[0][1],\n", 223 | ")\n", 224 | "ax[0][1].set(title=\"CQT (detail)\")\n", 225 | "ax[0][1].label_outer()\n", 226 | "\n", 227 | "librosa.display.specshow(\n", 228 | " R_db[:, ::dhl],\n", 229 | " sr=sr,\n", 230 | " hop_length=dhl,\n", 231 | " fmin=fmin,\n", 232 | " bins_per_octave=freqs_per_octave,\n", 233 | " y_axis=\"cqt_hz\",\n", 234 | " x_axis=\"s\",\n", 235 | " ax=ax[1][0],\n", 236 | ")\n", 237 | "ax[1][0].set(title=\"Resonate\")\n", 238 | "ax[1][0].label_outer()\n", 239 | "librosa.display.specshow(\n", 240 | " R_db[:, 0:detns:dhl],\n", 241 | " sr=sr,\n", 242 | " hop_length=dhl,\n", 243 | " fmin=fmin,\n", 244 | " bins_per_octave=freqs_per_octave,\n", 245 | " y_axis=\"cqt_hz\",\n", 246 | " x_axis=\"s\",\n", 247 | " ax=ax[1][1],\n", 248 | ")\n", 249 | "ax[1][1].set(title=\"Resonate (detail)\")\n", 250 | "fig.colorbar(img, ax=ax, format=\"%+2.f dB\")\n" 251 | ] 252 | } 253 | ], 254 | "metadata": { 255 | "kernelspec": { 256 | "display_name": ".venv", 257 | "language": "python", 258 | "name": "python3" 259 | }, 260 | "language_info": { 261 | "codemirror_mode": { 262 | "name": "ipython", 263 | "version": 3 264 | }, 265 | "file_extension": ".py", 266 | "mimetype": "text/x-python", 267 | "name": "python", 268 | "nbconvert_exporter": "python", 269 | "pygments_lexer": "ipython3", 270 | "version": "3.13.2" 271 | } 272 | }, 273 | "nbformat": 4, 274 | "nbformat_minor": 2 275 | } 276 | -------------------------------------------------------------------------------- /src/ResonatorBank.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 Alexandre R. J. Francois 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "ResonatorBank.hpp" 26 | 27 | #include 28 | 29 | using namespace resonate; 30 | 31 | constexpr float PI = 3.14159265358979323846; 32 | constexpr float twoPi = 2.0 * PI; 33 | 34 | ResonatorBank::ResonatorBank(size_t numResonators, const float *frequencies, const float *alphas, const float *betas, float sampleRate) 35 | : m_sampleRate(sampleRate), m_numResonators(numResonators), m_twoNumResonators(2 * numResonators) 36 | { 37 | 38 | constexpr float zero = 0.0f; 39 | constexpr float one = 1.0f; 40 | constexpr float minusOne = -1.0f; 41 | 42 | // initialize from passed frequencies 43 | m_frequencies = new float[m_numResonators]; 44 | memcpy(m_frequencies, frequencies, m_numResonators * sizeof(float)); 45 | 46 | // These must be 2 * numResonators size 47 | m_alphas = new float[m_twoNumResonators]; 48 | memcpy(m_alphas, alphas, m_numResonators * sizeof(float)); 49 | memcpy(m_alphas + m_numResonators, alphas, m_numResonators * sizeof(float)); 50 | 51 | m_omAlphas = new float[m_twoNumResonators]; 52 | vDSP_vfill(&one, m_omAlphas, 1, m_twoNumResonators); 53 | vDSP_vsmsa(m_alphas, 1, &minusOne, &one, m_omAlphas, 1, m_twoNumResonators); 54 | 55 | m_betas = new float[m_twoNumResonators]; 56 | memcpy(m_betas, betas, m_numResonators * sizeof(float)); 57 | memcpy(m_betas + m_numResonators, betas, m_numResonators * sizeof(float)); 58 | 59 | m_omBetas = new float[m_twoNumResonators]; 60 | vDSP_vfill(&one, m_omBetas, 1, m_twoNumResonators); 61 | vDSP_vsmsa(m_betas, 1, &minusOne, &one, m_omBetas, 1, m_twoNumResonators); 62 | 63 | // setup resonators 64 | m_rPtr = new float[m_twoNumResonators]; 65 | vDSP_vfill(&zero, m_rPtr, 1, m_twoNumResonators); 66 | 67 | m_rrPtr = new float[m_twoNumResonators]; 68 | vDSP_vfill(&zero, m_rrPtr, 1, m_twoNumResonators); 69 | 70 | m_zPtr = new float[m_twoNumResonators]; 71 | vDSP_vfill(&one, m_zPtr, 1, m_numResonators); 72 | vDSP_vfill(&zero, m_zPtr + m_numResonators, 1, m_numResonators); 73 | 74 | float twoPiOverSampleRate = -twoPi / m_sampleRate; 75 | m_wPtr = new float[m_twoNumResonators]; 76 | vDSP_vfill(&twoPiOverSampleRate, m_wPtr, 1, m_twoNumResonators); 77 | 78 | DSPSplitComplex W = {m_wPtr, m_wPtr + m_numResonators}; 79 | // multiply 2 * PI / sampleRate by frequency for each resonator 80 | vDSP_vmul(W.realp, 1, 81 | m_frequencies, 1, 82 | W.realp, 1, 83 | m_numResonators); 84 | vDSP_vmul(W.imagp, 1, 85 | m_frequencies, 1, 86 | W.imagp, 1, 87 | m_numResonators); 88 | 89 | // then calculate cos and sin 90 | int count = static_cast(m_numResonators); 91 | vvcosf(W.realp, W.realp, &count); 92 | vvsinf(W.imagp, W.imagp, &count); 93 | 94 | m_alphasSample = new float[m_twoNumResonators]; 95 | m_smPtr = new float[m_numResonators]; 96 | m_rsqrtPtr = new float[m_numResonators]; 97 | } 98 | 99 | ResonatorBank::~ResonatorBank() 100 | { 101 | delete[] m_frequencies; 102 | delete[] m_alphas; 103 | delete[] m_omAlphas; 104 | delete[] m_betas; 105 | delete[] m_omBetas; 106 | delete[] m_rPtr; 107 | delete[] m_rrPtr; 108 | delete[] m_zPtr; 109 | delete[] m_wPtr; 110 | delete[] m_alphasSample; 111 | delete[] m_smPtr; 112 | delete[] m_rsqrtPtr; 113 | } 114 | 115 | float ResonatorBank::frequencyValue(size_t index) 116 | { 117 | if (index >= m_numResonators) 118 | { 119 | throw std::out_of_range("Bad index passed to frequencyValue()"); 120 | } 121 | return m_frequencies[index]; 122 | } 123 | 124 | float ResonatorBank::alphaValue(size_t index) 125 | { 126 | if (index >= m_numResonators) 127 | { 128 | throw std::out_of_range("Bad index passed to alphaValue()"); 129 | } 130 | return m_alphas[index]; 131 | } 132 | 133 | float ResonatorBank::timeConstantValue(size_t index) 134 | { 135 | if (index >= m_numResonators) 136 | { 137 | throw std::out_of_range("Bad index passed to timeConstantValue()"); 138 | } 139 | // if alpha is 0 then time constant should be infinity... 140 | return m_alphas[index] > 0.0 ? 1.0 / (m_sampleRate * m_alphas[index]) : 0.0f; 141 | } 142 | 143 | void ResonatorBank::getComplex(float *dest, size_t size) 144 | { 145 | size_t numBytesToCopy = m_twoNumResonators * sizeof(float); 146 | if (size < m_twoNumResonators) 147 | { 148 | throw std::out_of_range("Buffer passed to copyComplex() is not large enough"); 149 | } 150 | memcpy(dest, m_rrPtr, numBytesToCopy); 151 | } 152 | 153 | void ResonatorBank::getPowers(float *dest, size_t size) 154 | { 155 | if (size < m_numResonators) 156 | { 157 | throw std::out_of_range("Buffer passed to getPowers() is not large enough"); 158 | } 159 | DSPSplitComplex R = {m_rrPtr, m_rrPtr + m_numResonators}; 160 | vDSP_zvmags(&R, 1, dest, 1, m_numResonators); 161 | } 162 | 163 | void ResonatorBank::getAmplitudes(float *dest, size_t size) 164 | { 165 | if (size < m_numResonators) 166 | { 167 | throw std::out_of_range("Buffer passed to getAmplitudes() is not large enough"); 168 | } 169 | DSPSplitComplex R = {m_rrPtr, m_rrPtr + m_numResonators}; 170 | vDSP_zvmags(&R, 1, dest, 1, m_numResonators); 171 | int count = static_cast(m_numResonators); 172 | vvsqrtf(dest, dest, &count); 173 | } 174 | 175 | void ResonatorBank::update(const float sample) 176 | { 177 | vDSP_vsmul(m_alphas, 1, &sample, m_alphasSample, 1, m_twoNumResonators); 178 | 179 | // resonator 180 | vDSP_vmma(m_rPtr, 1, 181 | m_omAlphas, 1, 182 | m_zPtr, 1, 183 | m_alphasSample, 1, 184 | m_rPtr, 1, 185 | m_twoNumResonators); 186 | 187 | // Smoothing with betas 188 | vDSP_vmma(m_rrPtr, 1, 189 | m_omBetas, 1, 190 | m_rPtr, 1, 191 | m_betas, 1, 192 | m_rrPtr, 1, 193 | m_twoNumResonators); 194 | 195 | // phasor 196 | DSPSplitComplex Z = {m_zPtr, m_zPtr + m_numResonators}; 197 | DSPSplitComplex W = {m_wPtr, m_wPtr + m_numResonators}; 198 | vDSP_zvmul(&Z, 1, 199 | &W, 1, 200 | &Z, 1, 201 | m_numResonators, 202 | 1); 203 | } 204 | 205 | void ResonatorBank::update(const std::vector &samples) 206 | { 207 | for (float sample : samples) 208 | { 209 | update(sample); 210 | } 211 | stabilize(); // this is overkill but necessary 212 | } 213 | 214 | /// Process a frame of samples. 215 | /// Apply stabilization (norm correction) at the end 216 | /// Compute amplitudes (phasor magnitudes) at the end 217 | void ResonatorBank::update(const float *frameData, size_t frameLength, size_t sampleStride) 218 | { 219 | for (int i = 0; i < frameLength; i += sampleStride) 220 | { 221 | update(frameData[i]); 222 | } 223 | stabilize(); // this is overkill but necessary 224 | } 225 | 226 | /// Process a frame of samples. 227 | /// Apply stabilization (norm correction) at the end 228 | /// Compute amplitudes (phasor magnitudes) at the end 229 | void ResonatorBank::update(const float *frameData, size_t frameLength, size_t sampleStride, float *powers, float *amplitudes) 230 | { 231 | for (int i = 0; i < frameLength; i += sampleStride) 232 | { 233 | update(frameData[i]); 234 | } 235 | stabilize(); // this is overkill but necessary 236 | } 237 | 238 | /// Apply norm correction to phasor. 239 | /// This can be done every few hundreds (?) of iterations 240 | void ResonatorBank::stabilize() 241 | { 242 | DSPSplitComplex Z = {m_zPtr, m_zPtr + m_numResonators}; 243 | vDSP_zvmags(&Z, 1, m_smPtr, 1, m_numResonators); 244 | // use reciprocal square root 245 | int count = static_cast(m_numResonators); 246 | vvrsqrtf(m_rsqrtPtr, m_smPtr, &count); 247 | vDSP_zrvmul(&Z, 1, m_rsqrtPtr, 1, &Z, 1, m_numResonators); 248 | } 249 | -------------------------------------------------------------------------------- /notebooks/MFCCs.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Mel Spectrogram and MFCCs\n", 8 | "\n", 9 | "Alexandre R.J. Francois" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "from matplotlib import colormaps as mcm\n", 20 | "import matplotlib.pyplot as plt\n", 21 | "import librosa\n", 22 | "import soundfile as sf\n", 23 | "from IPython.display import Audio \n", 24 | "\n", 25 | "from noFFT import resonate" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "#https://librosa.org/doc/main/recordings.html\n", 35 | "\n", 36 | "\n", 37 | "# y, sr = sf.read(librosa.ex(\"brahms\"), frames=500000)\n", 38 | "# y, sr = sf.read(librosa.ex(\"vibeace\"), frames=500000)\n", 39 | "# y, sr = sf.read(librosa.ex(\"sweetwaltz\"))\n", 40 | "y, sr = sf.read(librosa.ex(\"libri1\"))\n", 41 | "# y, sr = sf.read(librosa.ex(\"libri2\"))\n", 42 | "# y, sr = sf.read(librosa.ex(\"libri3\"))\n", 43 | "# y, sr = sf.read(librosa.ex(\"robin\"))\n", 44 | "\n", 45 | "print(sr)\n", 46 | "\n", 47 | "librosa.display.waveshow(y, sr=sr)\n", 48 | "print(y.shape)\n", 49 | "print(y.dtype)\n", 50 | "\n", 51 | "float_y = np.array(y, dtype=np.float32)\n", 52 | "print(float_y.dtype)\n", 53 | "\n", 54 | "Audio(data=y, rate=sr)\n" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "# Mel frequencies\n", 64 | "\n", 65 | "fmin = 0\n", 66 | "fmax = 8000.0\n", 67 | "n_mels=128\n", 68 | "\n", 69 | "freqs = librosa.mel_frequencies(n_mels=n_mels, fmin=fmin, fmax=fmax, htk=False)\n", 70 | "freqs[0] = freqs[1]\n", 71 | "# print(freqs)\n", 72 | "\n", 73 | "alphas = 1 - np.exp(-(1 / sr) * freqs / np.log10(1+freqs))\n", 74 | "alphas[np.isnan(alphas)] = 0\n", 75 | "# print(alphas)\n", 76 | "\n", 77 | "hop_length = 1\n", 78 | "dhl = 32\n" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "from noFFT_utils import resonate_wrapper, frequency_sweep\n", 88 | "\n", 89 | "Rcx = resonate_wrapper(y=y, sr=sr, frequencies=freqs, alphas=alphas, hop_length=hop_length, output_type='complex')\n", 90 | "Rcx = Rcx.T\n", 91 | "# print(Rcx.shape, Rcx.dtype)\n", 92 | "\n", 93 | "# compute powers\n", 94 | "R_pows = np.abs(Rcx) ** 2\n", 95 | "# print(R_pows.shape, R_pows.dtype)\n", 96 | "\n", 97 | "# Equalize\n", 98 | "eq = frequency_sweep(frequencies=freqs, alphas=alphas, sr=sr)\n", 99 | "Req_pows = (eq * R_pows.T).T\n", 100 | "\n", 101 | "R_db = librosa.power_to_db(Req_pows, ref=np.max)\n", 102 | "# print(R_db[:, ::dhl].shape)\n", 103 | "\n", 104 | "S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128, fmax=8000, hop_length=dhl)\n", 105 | "# print(S.shape)\n", 106 | "S_db = librosa.power_to_db(S,ref=np.max)\n", 107 | "\n", 108 | "fig, ax = plt.subplots(nrows=2, ncols=2, sharex='col', figsize=(8, 6), dpi=300)\n", 109 | "img = librosa.display.specshow(\n", 110 | " S_db,\n", 111 | " sr=sr,\n", 112 | " hop_length=dhl,\n", 113 | " x_axis='s',\n", 114 | " y_axis='mel',\n", 115 | " fmax=fmax,\n", 116 | " ax=ax[0][0])\n", 117 | "ax[0][0].set(title='Librosa Mel spectrogram')\n", 118 | "ax[0][0].label_outer()\n", 119 | "img = librosa.display.specshow(\n", 120 | " S_db[:,300:800],\n", 121 | " sr=sr,\n", 122 | " hop_length=dhl,\n", 123 | " x_axis='s',\n", 124 | " y_axis='mel',\n", 125 | " fmax=fmax,\n", 126 | " ax=ax[0][1])\n", 127 | "ax[0][1].set(title='Librosa (detail)')\n", 128 | "ax[0][1].label_outer()\n", 129 | "\n", 130 | "img = librosa.display.specshow(\n", 131 | " R_db[:, ::dhl],\n", 132 | " sr=sr,\n", 133 | " hop_length=dhl,\n", 134 | " x_axis=\"s\",\n", 135 | " y_axis=\"mel\",\n", 136 | " fmax=fmax,\n", 137 | " ax=ax[1][0],\n", 138 | ")\n", 139 | "ax[1][0].set(title=\"Resonate Mel spectrogram\")\n", 140 | "ax[1][1].label_outer()\n", 141 | "img = librosa.display.specshow(\n", 142 | " R_db[:, 9600:25600:dhl],\n", 143 | " sr=sr,\n", 144 | " hop_length=dhl,\n", 145 | " x_axis=\"s\",\n", 146 | " y_axis=\"mel\",\n", 147 | " fmax=fmax,\n", 148 | " ax=ax[1][1],\n", 149 | ")\n", 150 | "ax[1][1].set(title=\"Resonate (detail)\")\n", 151 | "fig.colorbar(img, ax=ax, format=\"%+2.f dB\")\n" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "# Librosa MFCC\n", 161 | "\n", 162 | "mfccs = librosa.feature.mfcc(S=S_db, n_mfcc=21)\n", 163 | "# mfccs = librosa.feature.mfcc(y=y, sr=sr)\n", 164 | "print(mfccs.shape)\n", 165 | "\n", 166 | "mfccs_r = librosa.feature.mfcc(S=R_db[:, ::dhl], ref=np.max, n_mfcc=21)\n", 167 | "print(mfccs_r.shape)\n", 168 | "\n", 169 | "fig, ax = plt.subplots(nrows=2, ncols=2, sharex='col', figsize=(8, 6), dpi=300)\n", 170 | "img = librosa.display.specshow(\n", 171 | " mfccs[1:],\n", 172 | " hop_length=dhl,\n", 173 | " x_axis='s',\n", 174 | " ax=ax[0][0])\n", 175 | "ax[0][0].set(title='Librosa MFCCs')\n", 176 | "ax[0][0].label_outer()\n", 177 | "img = librosa.display.specshow(\n", 178 | " mfccs[1:, 300:800],\n", 179 | " hop_length=dhl,\n", 180 | " x_axis='s',\n", 181 | " ax=ax[0][1])\n", 182 | "ax[0][1].set(title='Librosa MFCCs (detail)')\n", 183 | "ax[0][1].label_outer()\n", 184 | "img = librosa.display.specshow(\n", 185 | " mfccs_r[1:],\n", 186 | " hop_length=dhl,\n", 187 | " x_axis='s',\n", 188 | " ax=ax[1][0])\n", 189 | "ax[1][0].set(title='Resonate MFCCs')\n", 190 | "ax[1][0].label_outer()\n", 191 | "img = librosa.display.specshow(\n", 192 | " mfccs_r[1:, 300:800],\n", 193 | " hop_length=dhl,\n", 194 | " x_axis='s',\n", 195 | " ax=ax[1][1])\n", 196 | "ax[1][1].set(title='Resonate MFCCs (detail)')\n", 197 | "fig.colorbar(img, ax=ax, format=\"%+2.f dB\")\n", 198 | "\n", 199 | "\n", 200 | "\n" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "\n", 210 | "fig, ax = plt.subplots(nrows=2, sharex=True, figsize=(8, 8), dpi=300)\n", 211 | "img = librosa.display.specshow(mfccs[1:], hop_length=dhl, x_axis='s', ax=ax[0])\n", 212 | "fig.colorbar(img, ax=[ax[0]])\n", 213 | "ax[0].set(title='Librosa MFCC')\n", 214 | "ax[0].label_outer()\n", 215 | "img = librosa.display.specshow(mfccs_r[1:], hop_length=dhl, x_axis='s', ax=ax[1])\n", 216 | "fig.colorbar(img, ax=[ax[1]])\n", 217 | "ax[1].set(title='Resonate MFCC')" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": null, 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "fig, ax = plt.subplots(nrows=2, sharex=True, figsize=(8, 8), dpi=300)\n", 227 | "img = librosa.display.specshow(mfccs[1:, 300:800], hop_length=dhl, x_axis='s', ax=ax[0])\n", 228 | "fig.colorbar(img, ax=[ax[0]])\n", 229 | "ax[0].set(title='Librosa MFCC (detail)')\n", 230 | "ax[0].label_outer()\n", 231 | "img = librosa.display.specshow(mfccs_r[1:, 300:800], hop_length=dhl, x_axis='s', ax=ax[1])\n", 232 | "fig.colorbar(img, ax=[ax[1]])\n", 233 | "ax[1].set(title='Resonate MFCC (detail)')" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "from matplotlib import colormaps as mcm\n", 243 | "# list(colormaps)\n", 244 | "magma = mcm['magma']\n", 245 | "print(magma.colors)\n", 246 | "# coolwarm = colormaps['coolwarm']\n", 247 | "# print(coolwarm.colors)\n", 248 | "\n", 249 | "\n", 250 | "# get colormap values for Spectrogram\n", 251 | "\n", 252 | "map = mcm[\"coolwarm\"].resampled(256)\n", 253 | "\n", 254 | "colors = map(range(256))\n", 255 | "\n", 256 | "# intmap = np.round(map.colors[:, 0:3] * 255).astype(int)\n", 257 | "\n", 258 | "intmap = np.round(colors[:, 0:3] * 255).astype(int)\n", 259 | "\n", 260 | "np.savetxt(\n", 261 | " \"coolwarm_colormap_int.txt\",\n", 262 | " intmap,\n", 263 | " fmt=\"%d\",\n", 264 | " delimiter=\", \",\n", 265 | " newline=\"],\\n[\",\n", 266 | ")\n", 267 | "\n", 268 | "\n", 269 | "\n", 270 | "\n" 271 | ] 272 | } 273 | ], 274 | "metadata": { 275 | "kernelspec": { 276 | "display_name": ".venv", 277 | "language": "python", 278 | "name": "python3" 279 | }, 280 | "language_info": { 281 | "codemirror_mode": { 282 | "name": "ipython", 283 | "version": 3 284 | }, 285 | "file_extension": ".py", 286 | "mimetype": "text/x-python", 287 | "name": "python", 288 | "nbconvert_exporter": "python", 289 | "pygments_lexer": "ipython3", 290 | "version": "3.13.2" 291 | } 292 | }, 293 | "nbformat": 4, 294 | "nbformat_minor": 2 295 | } 296 | --------------------------------------------------------------------------------