├── .github └── workflows │ └── test.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── benchmark ├── __init__.py └── test_reverb.py ├── extensions ├── __init__.py ├── climiter.pxd ├── cylimiter.cpp ├── cylimiter.pyx ├── limiter.cpp ├── limiter.h ├── reverb_rir.cpp └── reverb_rir.h ├── reinstall.sh ├── setup.py └── tests ├── __init__.py ├── test_limiter.py └── test_reverb_rir.py /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | run-test: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 18 | fail-fast: false 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | # see https://github.com/microsoft/setup-msbuild 24 | - name: Add msbuild to PATH 25 | if: startsWith(matrix.os, 'windows') 26 | uses: microsoft/setup-msbuild@v1.0.2 27 | 28 | - name: Setup Python 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | 33 | - name: Install dependencies 34 | shell: bash 35 | run: | 36 | pip install numpy Cython pytest pytest-benchmark 37 | 38 | - name: Install package 39 | shell: bash 40 | run: | 41 | python3 setup.py install --verbose 42 | 43 | - name: Test 44 | shell: bash 45 | run: | 46 | pytest tests 47 | 48 | - name: Benchmark 49 | shell: bash 50 | run: | 51 | pytest benchmark 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include extensions/* 3 | include tests/* 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cylimiter 2 | 3 | A small package with stateful audio limiter implementation in Cython. Since the limiter is stateful it is suitable for streaming audio processing. 4 | 5 | We're slowly growing into other effects beyond limiter, starting with streaming reverb in v0.4. 6 | 7 | ## What's new? 8 | 9 | ### v0.4: Added reverb with RIR 10 | 11 | ```python 12 | from cylimiter import ReverbRIR 13 | 14 | reverb = ReverbRIR() # default RIR 15 | # or user-provided RIR: 16 | # reverb = ReverbRIR([0.001, 0.021, 0.007, ...]) 17 | 18 | reverb.apply(audio) # makes a copy 19 | reverb.apply_inplace(audio) # no copies with np.array input 20 | 21 | # preserves context between calls for streaming applications 22 | for chunk in audio_source: 23 | reverb.apply_inplace(chunk) 24 | ``` 25 | 26 | 27 | 28 | ### Examples 29 | 30 | ```python 31 | import numpy as np 32 | from cylimiter import Limiter 33 | 34 | limiter = Limiter(attack=0.5, release=0.9, delay=100, threshold=0.9) 35 | chunk_size = 1200 # for streaming processing 36 | 37 | # Example of applying limiter in-place (more efficient) 38 | audio = np.random.randn(44100) * 10 39 | for i in range(0, 44100, chunk_size): 40 | chunk = audio[i : i + chunk_size] 41 | limiter.limit_inplace(chunk) 42 | # ... do sth with chunk 43 | 44 | # Example of applying limiter that copies the signal 45 | audio = np.random.randn(1, 44100) * 10 46 | audio_lim = limiter.limit(audio) 47 | 48 | # Reset the limiter to re-use it on other signals 49 | limiter.reset() 50 | ``` 51 | 52 | ## Installation 53 | 54 | From PyPI via pip: 55 | ```bash 56 | pip install cylimiter 57 | ``` 58 | 59 | From source: 60 | ```bash 61 | git clone https://github.com/pzelasko/cylimiter 62 | cd cylimiter 63 | pip install . 64 | ``` 65 | 66 | Re-generate C++ sources from Cython: 67 | ```bash 68 | cd extensions 69 | cython -3 --cplus *.pyx 70 | ``` 71 | 72 | ## Motivation 73 | 74 | I couldn't easily find a package that implements audio limiter in Python in a suitable way for streaming audio. The closest (and the main inspiration) is [this gist by @bastibe](https://gist.github.com/bastibe/747283c55aad66404046). Since the algorithm is auto-regressive, I figured C++ will be much more efficient than Python. 75 | -------------------------------------------------------------------------------- /benchmark/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pzelasko/cylimiter/3cce342466420f95e847c72f5e10603f43080443/benchmark/__init__.py -------------------------------------------------------------------------------- /benchmark/test_reverb.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from cylimiter import ReverbRIR 3 | 4 | 5 | def run(audio): 6 | effect = ReverbRIR() 7 | return effect.apply(audio) 8 | 9 | 10 | def test_reverb_speed(benchmark): 11 | # 10s of audio in -1, 1 range as fp32 12 | audio = ((np.random.rand(441000) - 0.5) * 2).astype(np.float32) 13 | 14 | out = benchmark(run, audio) 15 | 16 | # Check basic properties of the output 17 | assert out.shape == audio.shape 18 | assert (out != audio).any() 19 | -------------------------------------------------------------------------------- /extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pzelasko/cylimiter/3cce342466420f95e847c72f5e10603f43080443/extensions/__init__.py -------------------------------------------------------------------------------- /extensions/climiter.pxd: -------------------------------------------------------------------------------- 1 | #cython: language_level=3 2 | from libcpp.vector cimport vector 3 | from libcpp.string cimport string 4 | 5 | cdef extern from "limiter.h" nogil: 6 | 7 | cdef cppclass CLimiter: 8 | 9 | CLimiter(float, float, int, float) 10 | 11 | CLimiter read_from_string(string data) 12 | 13 | string write_to_string() const 14 | 15 | void apply_inplace(float * const, size_t) 16 | 17 | vector[float] apply(const float * const, size_t) 18 | 19 | void reset() 20 | 21 | 22 | cdef extern from "reverb_rir.h" nogil: 23 | 24 | cdef cppclass ReverbRIR: 25 | 26 | ReverbRIR(const float) 27 | 28 | ReverbRIR(const float * const, const size_t, const float) 29 | 30 | ReverbRIR read_from_string(string data) 31 | 32 | string write_to_string() const 33 | 34 | void apply_inplace(float * const, const size_t) 35 | 36 | vector[float] apply(const float * const, const size_t) 37 | 38 | void reset() 39 | -------------------------------------------------------------------------------- /extensions/cylimiter.pyx: -------------------------------------------------------------------------------- 1 | #cython: language_level=3 2 | import warnings 3 | from typing import Optional, Sequence, Union 4 | 5 | from libcpp.memory cimport unique_ptr 6 | 7 | from extensions.climiter cimport CLimiter 8 | from extensions.climiter cimport ReverbRIR as CReverbRIR 9 | import numpy as np 10 | 11 | 12 | cdef class Limiter: 13 | cdef unique_ptr[CLimiter] _ptr 14 | 15 | def __init__(self, attack: float = 0.9, release: float = 0.9995, delay: int = 40, threshold: float = 0.95): 16 | assert 0 < attack < 1, "Permitted attack value range is (0 - 1)." 17 | assert 0 < release < 1, "Permitted release value range is (0 - 1)." 18 | assert 0 < threshold, "threshold has to be a float greater than zero." 19 | assert isinstance(delay, int) and delay > 0, "Delay has to be an integer greater than zero." 20 | self._ptr.reset(new CLimiter(attack, release, delay, threshold)) 21 | 22 | def __setstate__(self, state: bytes) -> None: 23 | self.__init__() 24 | self._ptr.get().read_from_string(state) 25 | 26 | def __getstate__(self) -> bytes: 27 | return self.write_to_string() 28 | 29 | def write_to_string(self) -> bytes: 30 | return self._ptr.get().write_to_string() 31 | 32 | def apply_inplace(self, audio: np.ndarray) -> None: 33 | _check_array(audio) 34 | cdef float[:] audio_memview = audio 35 | self._ptr.get().apply_inplace(&audio_memview[0], audio_memview.shape[0]) 36 | 37 | def apply(self, audio) -> np.ndarray: 38 | audio_arr = np.copy(np.ascontiguousarray(audio, dtype=np.float32)) 39 | self.apply_inplace(audio_arr) 40 | return audio_arr 41 | 42 | def limit_inplace(self, audio: np.ndarray) -> None: 43 | self.apply_inplace(audio) 44 | 45 | def limit(self, audio) -> np.ndarray: 46 | return self.apply(audio) 47 | 48 | def reset(self) -> None: 49 | return self._ptr.get().reset() 50 | 51 | 52 | cdef class ReverbRIR: 53 | cdef unique_ptr[CReverbRIR] _ptr 54 | 55 | def __init__(self, rir: Optional[Union[Sequence[float], np.ndarray]] = None, mix: float = 1.0): 56 | assert 0.0 <= mix <= 1.0, "Argument 'mix' must be between 0 and 1." 57 | if rir is None: 58 | self._ptr.reset(new CReverbRIR(mix)) 59 | else: 60 | self._init_from_rir(rir, mix) 61 | 62 | def _init_from_rir(self, rir, mix: float): 63 | # There will be two copies total, but it shouldn't be too bad... 64 | rir = np.copy(np.ascontiguousarray(rir, dtype=np.float32)) 65 | cdef float[:] rir_memview = rir 66 | self._ptr.reset(new CReverbRIR(&rir_memview[0], rir_memview.shape[0], mix)) 67 | 68 | def __setstate__(self, state: bytes) -> None: 69 | self.__init__() 70 | self._ptr.get().read_from_string(state) 71 | 72 | def __getstate__(self) -> bytes: 73 | return self.write_to_string() 74 | 75 | def write_to_string(self) -> bytes: 76 | return self._ptr.get().write_to_string() 77 | 78 | def apply_inplace(self, audio: np.ndarray) -> None: 79 | _check_array(audio) 80 | cdef float[:] audio_memview = audio 81 | self._ptr.get().apply_inplace(&audio_memview[0], audio_memview.shape[0]) 82 | 83 | def apply(self, audio) -> np.ndarray: 84 | audio_arr = np.copy(np.ascontiguousarray(audio, dtype=np.float32)) 85 | self.apply_inplace(audio_arr) 86 | return audio_arr 87 | 88 | def reset(self) -> None: 89 | return self._ptr.get().reset() 90 | 91 | 92 | def _check_array(arr) -> None: 93 | assert isinstance(arr, np.ndarray), "For in-place operations, we only support Numpy arrays. Either convert your input to a 1D Numpy array, or use the non-in-place operation ('effect.apply(arr)')." 94 | assert arr.dtype == np.float32, "We only support np.float32 dtype for in-place operations." 95 | assert arr.ndim == 1, "The input array has to be single-dimensional (only mono audio is supported)." 96 | assert arr.flags['C_CONTIGUOUS'], "The input array has to be contiguous (you can use np.ascontiguousarray)." 97 | -------------------------------------------------------------------------------- /extensions/limiter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "limiter.h" 8 | 9 | using namespace std; 10 | 11 | CLimiter::CLimiter(float attack, float release, int delay, float threshold) 12 | : attack_{attack}, 13 | release_{release}, 14 | delay_{delay}, 15 | threshold_{threshold} { 16 | reset(); 17 | } 18 | 19 | void CLimiter::apply_inplace(float * const audio, const size_t num_samples) { 20 | for (size_t idx = 0; idx < num_samples; ++idx) { 21 | const auto sample = audio[idx]; 22 | delay_line_[delay_index_] = sample; 23 | delay_index_ = (delay_index_ + 1) % delay_; 24 | 25 | // calculate an envelope of the signal 26 | envelope_ = max(abs(sample), envelope_ * release_); 27 | 28 | float target_gain = 1.0f; 29 | if (envelope_ > threshold_) { 30 | target_gain = threshold_ / envelope_; 31 | } 32 | 33 | // have gain_ go towards a desired limiter gain 34 | gain_ = gain_* attack_ + target_gain * (1.0f - attack_); 35 | 36 | // limit the delayed signal 37 | audio[idx] = delay_line_[delay_index_] * gain_; 38 | } 39 | } 40 | 41 | vector CLimiter::apply(float const * const audio, const size_t num_samples) { 42 | vector out; 43 | copy(audio, audio + num_samples, back_inserter(out)); 44 | apply_inplace(out.data(), out.size()); 45 | return out; 46 | } 47 | 48 | void CLimiter::reset() { 49 | delay_index_ = 0; 50 | gain_ = 1.0f; 51 | envelope_ = 0.0f; 52 | delay_line_.resize(delay_); 53 | for (unsigned long i = 0; i < delay_line_.size(); ++i) { 54 | delay_line_[i] = 0.0f; 55 | } 56 | } 57 | 58 | void CLimiter::read_from_string(const string &data) { 59 | istringstream str{data}; 60 | str >> attack_; 61 | str >> release_; 62 | str >> delay_; 63 | str >> threshold_; 64 | str >> delay_index_; 65 | str >> envelope_; 66 | str >> gain_; 67 | float sample; 68 | delay_line_.clear(); 69 | while(str >> sample) { 70 | delay_line_.push_back(sample); 71 | } 72 | } 73 | 74 | string CLimiter::write_to_string() const { 75 | ostringstream str; 76 | const auto s = " "; 77 | str << attack_ << s << release_ << s << delay_ << s << threshold_ << s 78 | << delay_index_ << s << envelope_ << s << gain_ << s; 79 | for (const auto item : delay_line_) { 80 | str << item << s; 81 | } 82 | return str.str(); 83 | } 84 | -------------------------------------------------------------------------------- /extensions/limiter.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | 6 | class CLimiter { 7 | // We are maintaining a C-style API with pair of pointer + size 8 | // to be able to avoid memory copies from python/numpy/cython to C++ 9 | // and provide an inplace modification API for numpy arrays. 10 | public: 11 | CLimiter(float attack, float release, int delay, float threshold); 12 | 13 | void apply_inplace(float * const audio, const std::size_t num_samples); 14 | std::vector apply(float const * const audio, const std::size_t num_samples); 15 | void reset(); 16 | 17 | void read_from_string(const std::string &data); 18 | std::string write_to_string() const; 19 | 20 | // Mutable state 21 | private: 22 | std::vector delay_line_; 23 | int delay_index_; 24 | float envelope_; 25 | float gain_; 26 | 27 | // Settings 28 | private: 29 | float attack_; 30 | float release_; 31 | int delay_; 32 | float threshold_; 33 | }; -------------------------------------------------------------------------------- /extensions/reverb_rir.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "reverb_rir.h" 10 | 11 | using namespace std; 12 | 13 | 14 | static const array DEFAULT_RIR = {-3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 6.1035156e-05, -9.1552734e-05, 6.1035156e-05, -9.1552734e-05, 6.1035156e-05, -0.00012207031, 9.1552734e-05, -0.00012207031, 0.00012207031, -0.00015258789, 0.00012207031, -0.00018310547, 0.00018310547, -0.00024414062, 0.00024414062, -0.00033569336, 0.0004272461, -0.0007019043, 0.001739502, 0.046875, 0.0008544922, -0.0004272461, 0.00048828125, -0.0008239746, 0.002166748, 0.038726807, -0.000579834, 0.00045776367, -0.00039672852, 0.0002746582, -0.0002746582, 0.00018310547, -0.00021362305, 0.00015258789, -0.00015258789, 0.00012207031, -0.00012207031, 9.1552734e-05, -0.00012207031, 6.1035156e-05, -9.1552734e-05, 6.1035156e-05, -9.1552734e-05, 6.1035156e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 0.0, -3.0517578e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, -9.1552734e-05, 6.1035156e-05, -9.1552734e-05, 6.1035156e-05, -9.1552734e-05, 6.1035156e-05, -0.00012207031, 9.1552734e-05, -0.00012207031, 0.00012207031, -0.00015258789, 0.00015258789, -0.00021362305, 0.00021362305, -0.0002746582, 0.00030517578, -0.00039672852, 0.0005187988, -0.0008239746, 0.0017700195, 0.024841309, -0.0011901855, 0.00061035156, -0.0004272461, 0.00030517578, -0.00024414062, 0.00018310547, -0.00018310547, 0.00012207031, -0.00012207031, 6.1035156e-05, -9.1552734e-05, 3.0517578e-05, -3.0517578e-05, 0.0, 0.0, -3.0517578e-05, 3.0517578e-05, -9.1552734e-05, 9.1552734e-05, -0.00021362305, 0.00045776367, 0.01953125, -0.00018310547, 6.1035156e-05, -6.1035156e-05, 0.0, 0.0, -3.0517578e-05, 3.0517578e-05, -9.1552734e-05, 6.1035156e-05, -0.00012207031, 9.1552734e-05, -0.00015258789, 0.00012207031, -0.00018310547, 0.00015258789, -0.00021362305, 0.00021362305, -0.0002746582, 0.0002746582, -0.00033569336, 0.00033569336, -0.0004272461, 0.00045776367, -0.0005493164, 0.00061035156, -0.00079345703, 0.0009460449, -0.0013122559, 0.0018615723, -0.0032043457, 0.0093688965, 0.023834229, -0.0018920898, 0.016540527, 0.00061035156, 0.004638672, 0.0052490234, -0.0011901855, 0.00048828125, -0.0002746582, 0.00015258789, -0.00012207031, 6.1035156e-05, -9.1552734e-05, 6.1035156e-05, -0.00012207031, 0.00012207031, -0.00018310547, 0.00018310547, -0.00030517578, 0.00039672852, -0.00061035156, 0.00091552734, -0.0017089844, 0.0049438477, 0.012176514, -0.003479004, 0.002380371, -0.002166748, 0.0022277832, -0.0029296875, 0.009185791, 0.0005187988, 0.0046081543, 0.011871338, -0.0024719238, 0.0069885254, 0.00021362305, 3.0517578e-05, -6.1035156e-05, 3.0517578e-05, 0.0, -6.1035156e-05, 6.1035156e-05, -0.00012207031, 0.00012207031, -0.00018310547, 0.00021362305, -0.00030517578, 0.00033569336, -0.00045776367, 0.0005493164, -0.00076293945, 0.0010681152, -0.0018920898, 0.006652832, 0.00289917, 0.005004883, 0.002105713, -0.0013122559, 0.0009460449, -0.0008239746, 0.0007019043, -0.0006713867, 0.00061035156, -0.00064086914, 0.00064086914, -0.0007324219, 0.0008544922, -0.0012207031, 0.0022583008, 0.0049743652, 0.0023498535, 0.004272461, -0.0018615723, 0.0013427734, -0.0012207031, 0.001159668, -0.0012512207, 0.0014343262, -0.0020141602, 0.004272461, 0.0032653809, 0.0024719238, 0.0037841797, -0.0015258789, 0.00091552734, -0.0007019043, 0.0005187988, -0.00045776367, 0.00036621094, -0.00039672852, 0.0004272461, -0.0008239746, 0.0048217773, 0.0032348633, 0.008911133, 0.00091552734, 0.004058838, -0.0007019043, 0.00024414062, -9.1552734e-05, -0.00012207031, 0.00033569336, -0.00076293945, 0.002166748, 0.015014648, -0.0027160645, 0.0140686035, -0.00048828125, 0.00079345703, -0.0012512207, 0.003112793, 0.0031433105, 0.0030517578, 0.0010681152, -0.00088500977, 0.00088500977, 0.0039367676, 0.001739502, 0.0033569336, -0.00064086914, 0.00024414062, -0.00012207031, 0.0, 3.0517578e-05, -9.1552734e-05, 0.00012207031, -0.00018310547, 0.00018310547, -0.0002746582, 0.00030517578, -0.00039672852, 0.00048828125, -0.0008239746, 0.0037841797, 0.0011901855, 0.0032653809, -0.0005493164, 0.00048828125, -0.0005493164, 0.00061035156, -0.00076293945, 0.00088500977, -0.0011291504, 0.0014953613, -0.002380371, 0.0058288574, 0.00869751, -0.0020751953, 0.00091552734, -0.00039672852, -6.1035156e-05, 0.0005493164, -0.0015563965, 0.007843018, 0.004058838, -0.0016174316, 0.00076293945, 0.00024414062, 0.010345459, -0.0018920898, 0.0013122559, -0.0010986328, 0.0009460449, -0.00088500977, 0.00079345703, -0.00079345703, 0.0007324219, -0.00076293945, 0.0007324219, -0.00079345703, 0.00079345703, -0.00088500977, 0.0009460449, -0.001159668, 0.0014343262, -0.002105713, 0.004486084, 0.0128479, 0.006134033, 0.012786865, 0.0019836426, 0.000579834, -0.000579834, 0.00045776367, -0.0004272461, 0.00036621094, -0.00036621094, 0.00030517578, -0.00033569336, 0.00030517578, -0.00039672852, 0.000579834, 0.0074768066, -0.0002746582, 0.00015258789, -0.00033569336, 0.003540039, 0.0032653809, 0.0062561035, 0.0018920898, 0.0022888184, -0.0010070801, 0.0006713867, -0.000579834, 0.0005187988, -0.0005187988, 0.00048828125, -0.0005493164, 0.000579834, -0.0007019043, 0.0008544922, -0.0013122559, 0.0038146973, 0.0028076172, 0.0053710938, 0.005493164, -0.0010986328, 0.00048828125, -0.0002746582, 0.003479004, 0.003112793, 0.0009460449, -0.00030517578, 0.0036010742, 0.0029907227, 0.00091552734, -0.00036621094, 0.00012207031, 6.1035156e-05, -0.00039672852, 0.0014648438, 0.003326416, 0.0022888184, 0.00048828125, -0.00039672852, 0.00024414062, -0.00024414062, 0.0028381348, 0.0021362305, 0.001739502, 0.0058288574, 0.011962891, 0.0034484863, 0.003967285, -0.0018615723, 0.0015258789, -0.0015258789, 0.0016174316, -0.0020141602, 0.0038757324, 0.0013122559, 0.004211426, 0.0033569336, 0.004547119, 0.004486084, 0.0, -0.00024414062, 0.0032348633, 0.001373291, 0.006713867, 0.009765625, 0.006011963, 0.0049743652, -0.0020751953, 0.0012817383, -0.0009460449, 0.0007019043, -0.000579834, 0.00039672852, -0.00021362305, 0.0026550293, 0.0016174316, 0.0007019043, 0.0014038086, 0.0036315918, 0.0002746582, 0.0002746582, -0.00039672852, 0.00045776367, -0.00064086914, 0.0012817383, 0.0030517578, -0.00091552734, 0.0067749023, 0.0, 0.009307861, 0.0042419434, 0.00048828125, 0.0017700195, -0.0018615723, 0.012023926, 0.0060424805, 0.0004272461, 0.0018920898, -0.0011291504, 0.00076293945, -0.00018310547, 0.0038757324, -0.00021362305, 0.0029296875, 0.0059814453, -0.0012207031, 0.0005187988, -0.00021362305, -0.00018310547, 0.0071105957, 0.0018005371, 0.001373291, 0.0009460449, -0.0009460449, 0.0010375977, -0.0014648438, 0.0036315918, 0.0026245117, 0.011047363, 0.010314941, 0.003753662, 0.004425049, -0.0012817383, 0.0032043457, -0.0015258789, 0.002319336, 0.0006713867, 0.0007324219, 0.0014953613, 9.1552734e-05, 0.00592041, -6.1035156e-05, -0.00030517578, 0.0024108887, 0.0042419434, 0.0050354004, 0.00088500977, 0.0018005371, 0.0039367676, 0.00076293945, 0.0015563965, 0.0015563965, 0.005432129, -0.0004272461, 0.0005493164, -0.00061035156, 0.003967285, 0.0030212402, 0.00033569336, -0.00039672852, 0.00048828125, 0.0016174316, 9.1552734e-05, 0.0013122559, 0.0012512207, 0.003692627, 0.0032958984, 0.0015563965, 0.0006713867, 0.004180908, 0.0036010742, 0.0044555664, 0.0015869141, -0.00015258789, 0.00012207031, -0.00033569336, 0.0022583008, 0.0020751953, 0.0068359375, 0.0067749023, 0.0032958984, -0.00012207031, 0.0, 3.0517578e-05, 0.0017700195, 0.0016174316, 3.0517578e-05, -3.0517578e-05, 0.00012207031, 0.0032653809, 0.0012817383, 0.0005493164, 0.0069274902, 0.0014953613, 0.0008239746, -0.0006713867, 0.0025634766, 0.0016174316, 0.0002746582, -0.00039672852, 0.00048828125, -0.0008544922, 0.002960205, 0.0027770996, 0.0045166016, 0.0025939941, 0.001953125, 0.003753662, 0.0014953613, 0.0013427734, 0.004272461, 0.0016479492, 0.0013122559, 0.00064086914, -0.0010375977, 0.00390625, 0.005554199, -0.00088500977, 0.004333496, 0.00021362305, 0.00024414062, -0.0005187988, 0.00091552734, 0.0012817383, 0.002319336, 0.0018310547, 0.0008544922, 0.00039672852, -0.000579834, 0.0030212402, 0.0023498535, 0.00061035156, 0.0055236816, 0.0014343262, 0.0021972656, 0.0014343262, 0.0010986328, 0.0037231445, 0.0022277832, 0.0012207031, 3.0517578e-05, -0.00018310547, 0.00021362305, -0.00030517578, 0.00036621094, -0.00048828125, 0.000579834, -0.0008544922, 0.0016174316, 0.0017700195, 0.004272461, 0.0038757324, 0.00091552734, 6.1035156e-05, 0.00091552734, 0.002960205, 0.0030517578, 0.001739502, -0.00088500977, 0.0036621094, 0.002532959, 0.0014343262, 0.0015869141, 0.0014343262, -0.00045776367, 0.00018310547, 3.0517578e-05, 0.0014038086, 0.00024414062, 0.001159668, -0.0005493164, 0.0004272461, 0.0021362305, 0.0011291504, 0.0043029785, 0.0062561035, 0.0019226074, 0.00015258789, -0.00033569336, 0.0002746582, 6.1035156e-05, 0.005584717, 0.0034484863, 0.0022888184, 0.00036621094, 0.00079345703, 0.0030517578, 0.0006713867, 0.0020446777, 0.00012207031, -0.00018310547, 0.000579834, 0.00076293945, 0.0004272461, 0.0007019043, -0.00048828125, 0.0010681152, 0.0022583008, 0.0024108887, 0.0020446777, 0.00061035156, 0.0011291504, 3.0517578e-05, 0.0016174316, 0.0011901855, 0.0032653809, 0.0022583008, 0.0020446777, 0.0017700195, 0.0048217773, 0.0037841797, 0.0015258789, 0.0011291504, -0.0002746582, 0.0010375977, 0.0026550293, 0.0019226074, -0.00015258789, -3.0517578e-05, 6.1035156e-05, -0.00021362305, 0.0022888184, 0.0024719238, 0.002532959, 0.00012207031, 0.0011901855, 0.009918213, 0.011657715, 0.0018615723, 0.003326416, 0.0017700195, 0.0014343262, -3.0517578e-05, 0.002319336, 0.0015563965, 0.0013427734, 0.0010375977, 0.0019836426, 0.0030212402, 0.00061035156, 0.00012207031, 0.0012207031, 0.0022277832, -0.00088500977, 0.0015869141, 0.00061035156, 0.00045776367, 0.0047912598, 0.005340576, -0.00076293945, 0.0024414062, -0.00088500977, 0.0014953613, 0.0017700195, 0.0074768066, -0.00033569336, 0.0011901855, 0.00030517578, 0.0005187988, 0.0014038086, 0.0024719238, 0.0019226074, 0.0013122559, 0.00030517578, 0.008850098, 0.009246826, 0.0019226074, 0.0010681152, 6.1035156e-05, -0.00024414062, 0.0011901855, 0.00024414062, 0.0021972656, 0.004272461, -0.00064086914, 0.004211426, 0.0045166016, 0.005126953, -0.00088500977, 0.0022277832, 0.00024414062, 0.0016174316, -0.00015258789, 0.0006713867, 0.001953125, 0.0032043457, -0.0005187988, 0.0011901855, 0.00030517578, 0.0016174316, 0.003326416, 0.005065918, 0.0012817383, 0.0016174316, -0.00018310547, 0.002319336, 0.0012817383, 0.0019226074, 0.0027160645, -0.00048828125, 0.0016784668, 0.003540039, 0.0050964355, 0.0007019043, 0.00088500977}; 15 | 16 | 17 | void validate_mix(const float mix) { 18 | if (mix < 0.0f || mix > 1.0f) { 19 | throw runtime_error{"Mix argument must have value between 0 and 1."}; 20 | } 21 | } 22 | 23 | 24 | ReverbRIR::ReverbRIR(const float mix) : mix_{mix} { 25 | std::copy(DEFAULT_RIR.crbegin(), DEFAULT_RIR.crend(), back_inserter(rir_)); 26 | reset(); 27 | validate_mix(mix_); 28 | } 29 | 30 | ReverbRIR::ReverbRIR(const float * const rir, const size_t num_samples, const float mix) : mix_{mix} { 31 | for (unsigned int i = num_samples; i > 0; --i) { 32 | rir_.push_back(rir[i-1]); 33 | } 34 | reset(); 35 | validate_mix(mix_); 36 | } 37 | 38 | 39 | float inner_product_clang(const vector &buffer_, const vector &rir_, int offset) { 40 | const auto SIZE = rir_.size(); 41 | float sum = 0.0f; 42 | #pragma clang loop vectorize(assume_safety) 43 | for (unsigned int i = 0; i < SIZE; ++i) { 44 | sum += buffer_[offset + i] * rir_[i]; 45 | } 46 | return sum; 47 | } 48 | 49 | 50 | float inner_product_gcc(const vector &buffer_, const vector &rir_, int offset) { 51 | const auto SIZE = rir_.size(); 52 | float sum = 0.0f; 53 | #pragma omp simd reduction(+:sum) 54 | for (unsigned int i = 0; i < SIZE; ++i) { 55 | sum += buffer_[offset + i] * rir_[i]; 56 | } 57 | return sum; 58 | } 59 | 60 | void ReverbRIR::apply_inplace(float * const audio, const size_t num_samples) { 61 | const auto dry = 1.0f - mix_; 62 | copy(audio, audio + num_samples, back_inserter(buffer_)); 63 | for (unsigned int idx = 0; idx < num_samples; ++idx) { 64 | 65 | // Apparently std::inner_product does not get auto-vectorized, 66 | // but we can help the compiler with a few pragmas, so we write 67 | // it out explicitly. 68 | #if defined(__clang__) 69 | const auto sum = inner_product_clang(buffer_, rir_, idx); 70 | #elif defined(__GNUC__) 71 | const auto sum = inner_product_gcc(buffer_, rir_, idx); 72 | #else 73 | const auto sum = inner_product(buffer_.cbegin() + idx, buffer_.cbegin() + idx + rir_.size(), rir_.cbegin(), 0.0f); 74 | #endif 75 | 76 | audio[idx] = mix_ * sum + dry * audio[idx]; 77 | } 78 | buffer_.erase(buffer_.begin(), buffer_.begin() + num_samples); 79 | } 80 | 81 | vector ReverbRIR::apply(float const * const audio, const size_t num_samples) { 82 | vector out; 83 | copy(audio, audio + num_samples, back_inserter(out)); 84 | apply_inplace(out.data(), out.size()); 85 | return out; 86 | } 87 | 88 | void ReverbRIR::reset() { 89 | buffer_.resize(rir_.size() - 1); 90 | for (unsigned long i = 0; i < buffer_.size(); ++i) { 91 | buffer_[i] = 0.0f; 92 | } 93 | } 94 | 95 | void ReverbRIR::read_from_string(const string &data) { 96 | istringstream str{data}; 97 | unsigned int size; 98 | float sample; 99 | 100 | str >> size; 101 | rir_.clear(); 102 | for (unsigned int i = 0; i < size; ++i) { 103 | str >> sample; 104 | rir_.push_back(sample); 105 | } 106 | 107 | str >> size; 108 | buffer_.clear(); 109 | for (unsigned int i = 0; i < size; ++i) { 110 | str >> sample; 111 | buffer_.push_back(sample); 112 | } 113 | } 114 | 115 | string ReverbRIR::write_to_string() const { 116 | ostringstream str; 117 | const auto s = " "; 118 | str << rir_.size() << s; 119 | for (const auto item : rir_) { 120 | str << item << s; 121 | } 122 | str << buffer_.size() << s; 123 | for (const auto item : buffer_) { 124 | str << item << s; 125 | } 126 | return str.str(); 127 | } 128 | 129 | -------------------------------------------------------------------------------- /extensions/reverb_rir.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | class ReverbRIR { 8 | // We are maintaining a C-style API with pair of pointer + size 9 | // to be able to avoid memory copies from python/numpy/cython to C++ 10 | // and provide an inplace modification API for numpy arrays. 11 | public: 12 | ReverbRIR(const float mix = 1.0f); 13 | 14 | ReverbRIR(const float * const rir, const std::size_t num_samples, const float mix = 1.0f); 15 | 16 | void apply_inplace(float * const audio, const std::size_t num_samples); 17 | std::vector apply(float const * const audio, const std::size_t num_samples); 18 | void reset(); 19 | 20 | void read_from_string(const std::string &data); 21 | std::string write_to_string() const; 22 | 23 | // Mutable state 24 | private: 25 | std::vector buffer_; 26 | 27 | // Settings 28 | private: 29 | std::vector rir_; 30 | float mix_; 31 | }; -------------------------------------------------------------------------------- /reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eou pipefail 4 | 5 | pushd extensions; cython -3 --cplus *.pyx; popd 6 | pip uninstall -y cylimiter && pip install '.[dev]' 7 | pytest tests -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import open 3 | from os import path 4 | from setuptools import Extension 5 | from setuptools import find_packages 6 | from setuptools import setup 7 | from sys import platform 8 | 9 | try: 10 | from Cython.Build import cythonize 11 | except ImportError: 12 | cythonize = None 13 | 14 | 15 | # https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html#distributing-cython-modules 16 | def no_cythonize(extensions, **_ignore): 17 | for extension in extensions: 18 | sources = [] 19 | for sfile in extension.sources: 20 | path, ext = os.path.splitext(sfile) 21 | if ext in (".pyx", ".py"): 22 | if extension.language == "c++": 23 | ext = ".cpp" 24 | else: 25 | ext = ".c" 26 | sfile = path + ext 27 | sources.append(sfile) 28 | extension.sources[:] = sources 29 | return extensions 30 | 31 | 32 | COMPILE_ARGS = [] 33 | 34 | if os.name != "nt": 35 | COMPILE_ARGS.extend([ 36 | "-std=c++11", 37 | "-funsigned-char", 38 | "-Wno-register", 39 | "-Wno-unused-function", 40 | "-Wno-unused-local-typedefs", 41 | ]) 42 | if not platform.startswith("darwin"): 43 | COMPILE_ARGS.append("-fopenmp") 44 | COMPILE_ARGS.append("-march=native") 45 | 46 | if platform.startswith("darwin"): 47 | COMPILE_ARGS.append("-stdlib=libc++") 48 | COMPILE_ARGS.append("-mmacosx-version-min=10.7") 49 | 50 | cylimiter = Extension( 51 | name="cylimiter", 52 | language="c++", 53 | extra_compile_args=COMPILE_ARGS, 54 | include_dirs=[os.path.dirname(os.path.abspath(__file__)) + "/extensions"], 55 | sources=["extensions/cylimiter.pyx", "extensions/limiter.cpp", "extensions/reverb_rir.cpp"], 56 | ) 57 | extensions = [cylimiter] 58 | 59 | CYTHONIZE = bool(int(os.getenv("CYTHONIZE", 0))) and cythonize is not None 60 | 61 | if CYTHONIZE: 62 | compiler_directives = {"language_level": 3, "embedsignature": True} 63 | extensions = cythonize(extensions, compiler_directives=compiler_directives) 64 | else: 65 | extensions = no_cythonize(extensions) 66 | 67 | __version__ = "0.4.2" 68 | 69 | this_directory = path.abspath(path.dirname(__file__)) 70 | with open(path.join(this_directory, "README.md"), encoding="utf8") as source: 71 | long_description = source.read() 72 | 73 | 74 | def main() -> None: 75 | setup( 76 | name="cylimiter", 77 | version=__version__, 78 | author="Piotr Żelasko", 79 | description="Audio limiter in Cython.", 80 | long_description=long_description, 81 | long_description_content_type="text/markdown", 82 | keywords=[ 83 | "signal processing", 84 | "audio", 85 | ], 86 | classifiers=[ 87 | "Programming Language :: Python :: 3.6", 88 | "Programming Language :: Python :: 3.7", 89 | "Programming Language :: Python :: 3.8", 90 | "Programming Language :: Python :: 3.9", 91 | "Development Status :: 3 - Alpha", 92 | "Intended Audience :: Developers", 93 | "License :: OSI Approved :: Apache Software License", 94 | "Operating System :: OS Independent", 95 | "Topic :: Software Development :: Libraries :: Python Modules", 96 | "Topic :: Scientific/Engineering :: Mathematics", 97 | ], 98 | license="Apache 2.0", 99 | install_requires=['numpy'], 100 | extras_require={ 101 | 'dev': ['pytest', 'Cython'] 102 | }, 103 | ext_modules=extensions, 104 | packages=find_packages(exclude=["tests", "benchmark"]), 105 | zip_safe=False, 106 | ) 107 | 108 | 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pzelasko/cylimiter/3cce342466420f95e847c72f5e10603f43080443/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_limiter.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from functools import lru_cache 3 | 4 | import pytest 5 | import numpy as np 6 | from cylimiter import Limiter 7 | 8 | 9 | @lru_cache(1) 10 | def get_audio(): 11 | # 1s of 44.1kHz audio in fp32, centered on zero, with dynamic range [-5, 5] 12 | return ((np.random.rand(44100) - 0.5) * 10).astype(np.float32) 13 | 14 | 15 | def test_limiter(): 16 | # Example of applying limiter that copies the signal 17 | audio = get_audio() 18 | limiter = Limiter() 19 | audio_lim = limiter.apply(audio) 20 | assert (audio_lim != audio).any() 21 | np.testing.assert_array_less(audio_lim, 1.0) 22 | 23 | 24 | def test_limiter_nondefault_args(): 25 | # Example of applying limiter that copies the signal 26 | audio = get_audio() 27 | limiter = Limiter(attack=0.5, release=0.9, delay=100, threshold=0.9) 28 | audio_lim = limiter.apply(audio) 29 | assert (audio_lim != audio).any() 30 | 31 | 32 | def test_limiter_nondefault_args_validation(): 33 | with pytest.raises(AssertionError): 34 | limiter = Limiter(attack=1.1) 35 | 36 | with pytest.raises(AssertionError): 37 | limiter = Limiter(attack=0) 38 | 39 | with pytest.raises(AssertionError): 40 | limiter = Limiter(release=1.1) 41 | 42 | with pytest.raises(AssertionError): 43 | limiter = Limiter(threshold=-0.5) 44 | 45 | # does not raise when above 1 to support other audio effects 46 | # in effect chains that may drive the signal above 1.0 magnitude 47 | limiter = Limiter(threshold=1.1) 48 | 49 | with pytest.raises(AssertionError): 50 | limiter = Limiter(delay=0) 51 | 52 | 53 | def test_limiter_inplace_fails_with_python_list(): 54 | limiter = Limiter() 55 | 56 | # does not raise 57 | limiter.apply([1.0] * 22050) 58 | 59 | with pytest.raises(AssertionError): 60 | limiter.apply_inplace([1.0] * 22050) 61 | 62 | 63 | def test_limiter_inplace_fails_with_float64(): 64 | limiter = Limiter() 65 | 66 | audio = get_audio().astype(np.float64) 67 | 68 | with pytest.raises(AssertionError): 69 | limiter.apply_inplace(audio) 70 | 71 | 72 | def test_limiter_inplace(): 73 | limiter = Limiter() 74 | chunk_size = 1200 # for streaming processing 75 | 76 | # Example of applying limiter in-place (more efficient) 77 | audio = get_audio() 78 | print(len(audio)) 79 | for offset in range(0, len(audio), chunk_size): 80 | chunk = audio[offset : offset + chunk_size] 81 | chunk_cpy = np.copy(chunk) 82 | limiter.apply_inplace(chunk) 83 | # ... do sth with chunk 84 | assert (chunk != chunk_cpy).any() 85 | np.testing.assert_array_less(chunk, 1.0) 86 | 87 | 88 | def test_limiter_reset(): 89 | # Example of applying limiter that copies the signal 90 | audio = get_audio() 91 | limiter = Limiter() 92 | audio_lim = limiter.apply(audio) 93 | audio_lim2 = limiter.apply(audio) 94 | assert ( 95 | audio_lim != audio_lim2 96 | ).any() # some state was accumulated in limiter so the results is different 97 | 98 | limiter.reset() 99 | audio_lim_reset = limiter.apply(audio) 100 | np.testing.assert_allclose(audio_lim, audio_lim_reset) 101 | 102 | 103 | def test_limiter_pickle_works(): 104 | limiter_default = Limiter() 105 | limiter = Limiter(attack=0.555, delay=1000, threshold=0.2, release=0.01) 106 | data = pickle.dumps(limiter) 107 | limiter_unpickled = pickle.loads(data) 108 | 109 | audio = get_audio() 110 | audio_lim = limiter.apply(audio) 111 | assert (audio != audio_lim).any() 112 | audio_lim_unpickled = limiter_unpickled.apply(audio) 113 | assert (audio != audio_lim_unpickled).any() 114 | 115 | audio_lim_default = limiter_default.apply(audio) 116 | assert (audio_lim != audio_lim_default).any() 117 | assert (audio_lim_unpickled != audio_lim_default).any() 118 | 119 | np.testing.assert_allclose(audio_lim, audio_lim_unpickled) 120 | -------------------------------------------------------------------------------- /tests/test_reverb_rir.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from functools import lru_cache 3 | 4 | import pytest 5 | import numpy as np 6 | from cylimiter import ReverbRIR 7 | 8 | 9 | @lru_cache(1) 10 | def get_audio(): 11 | # 100ms of 44.1kHz audio in fp32, centered on zero, with dynamic range [-1, 1] 12 | return ((np.random.rand(4410) - 0.5) * 2).astype(np.float32) 13 | 14 | 15 | def test_reverb_rir(): 16 | # Example of applying reverb that copies the signal 17 | audio = get_audio() 18 | effect = ReverbRIR() 19 | out = effect.apply(audio) 20 | assert (out!= audio).any() 21 | np.testing.assert_array_less(out, 1.0) 22 | 23 | 24 | def test_reverb_rir_mix(): 25 | # Example of applying reverb that copies the signal 26 | audio = get_audio() 27 | effect = ReverbRIR() 28 | out = effect.apply(audio) 29 | 30 | effect2 = ReverbRIR(mix=0.9) 31 | out2 = effect.apply(audio) 32 | 33 | assert (out!= out2).any() 34 | 35 | 36 | def test_reverb_nondefault_args(): 37 | # Example of applying effect that copies the signal 38 | audio = get_audio() 39 | effect = ReverbRIR([0.0, 0.0, 0.0001, 0.02, 0.3, 0.7, 0.1, 0.1232, 0.000123, 0.0]) 40 | audio_lim = effect.apply(audio) 41 | assert (audio_lim != audio).any() 42 | 43 | 44 | def test_reverb_inplace_fails_with_python_list(): 45 | effect = ReverbRIR() 46 | 47 | # does not raise 48 | effect.apply([1.0] * 22050) 49 | 50 | with pytest.raises(AssertionError): 51 | effect.apply_inplace([1.0] * 22050) 52 | 53 | 54 | def test_reverb_inplace_fails_with_float64(): 55 | effect = ReverbRIR() 56 | 57 | audio = get_audio().astype(np.float64) 58 | 59 | with pytest.raises(AssertionError): 60 | effect.apply_inplace(audio) 61 | 62 | 63 | def test_reverb_inplace(): 64 | effect = ReverbRIR() 65 | chunk_size = 1200 # for streaming processing 66 | 67 | # Example of applying effect in-place (more efficient) 68 | audio = get_audio() 69 | print(len(audio)) 70 | for offset in range(0, len(audio), chunk_size): 71 | chunk = audio[offset : offset + chunk_size] 72 | chunk_cpy = np.copy(chunk) 73 | effect.apply_inplace(chunk) 74 | # ... do sth with chunk 75 | assert (chunk != chunk_cpy).any() 76 | np.testing.assert_array_less(chunk, 1.0) 77 | 78 | 79 | def test_reverb_reset(): 80 | # Example of applying effect that copies the signal 81 | audio = get_audio() 82 | effect = ReverbRIR() 83 | audio_lim = effect.apply(audio) 84 | audio_lim2 = effect.apply(audio) 85 | assert ( 86 | audio_lim != audio_lim2 87 | ).any() # some state was accumulated in effect so the results is different 88 | 89 | effect.reset() 90 | audio_lim_reset = effect.apply(audio) 91 | np.testing.assert_allclose(audio_lim, audio_lim_reset) 92 | 93 | 94 | def test_reverb_pickle_works(): 95 | effect_default = ReverbRIR() 96 | effect = ReverbRIR([0.0, 0.0, 0.001, 0.3, 0.7, 0.3, 0.001, 0.0, 0.0]) 97 | data = pickle.dumps(effect) 98 | effect_unpickled = pickle.loads(data) 99 | 100 | audio = get_audio() 101 | audio_lim = effect.apply(audio) 102 | assert (audio != audio_lim).any() 103 | audio_lim_unpickled = effect_unpickled.apply(audio) 104 | assert (audio != audio_lim_unpickled).any() 105 | 106 | audio_lim_default = effect_default.apply(audio) 107 | assert (audio_lim != audio_lim_default).any() 108 | assert (audio_lim_unpickled != audio_lim_default).any() 109 | 110 | np.testing.assert_allclose(audio_lim, audio_lim_unpickled) 111 | --------------------------------------------------------------------------------