daadsp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
daadsp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,3 @@
1
+ Metadata-Version: 2.4
2
+ Name: daadsp
3
+ Version: 0.1.0
@@ -0,0 +1,10 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "daadsp"
7
+ version = "0.1.0"
8
+
9
+ [tool.hatch.build.targets.wheel]
10
+ packages = ["src/daadsp"]
@@ -0,0 +1,6 @@
1
+ from .freq import *
2
+ from .amp import *
3
+ from .xenharmonic import *
4
+ from .misc import *
5
+ from .osc import *
6
+ from .series import *
@@ -0,0 +1,51 @@
1
+ import math as _math
2
+
3
+ def rpow_to_pow(rpow):
4
+ 'convert a root-power quantity to power. returns rpow²'
5
+ return rpow * rpow
6
+
7
+ def pow_to_rpow(pow):
8
+ 'convert a root-power quantity to power. returns √pow'
9
+ return _math.sqrt(pow)
10
+
11
+ def db_to_bel(db):
12
+ 'convert db to bel. returns db / 10'
13
+ return db / 10
14
+
15
+ def bel_to_db(bel):
16
+ 'convert bel to decibel. returns bel * 10'
17
+ return bel * 10
18
+
19
+ def db_to_np(db):
20
+ 'convert decibels to nepers. by default uses db * 20 * log⏨(e) which assumes decibels describe power ratios and nepers describe root-power ratios'
21
+ # db / (20 * _math.log10(_math.e))
22
+ return db * 0.11512925464970228420089957273421821038036201556142658733102210701271816669945107
23
+
24
+ def np_to_db(np):
25
+ 'convert nepers to decibels. by default uses np / (20 * log⏨(e)) which assumes decibels describe power ratios and nepers describe root-power ratios'
26
+ # np * 20 * _math.log10(_math.e)
27
+ return np * 8.6858896380650365530225783783321016458647830411094563350443397173668347208571878
28
+
29
+ def rpow_to_db(rpow, *, reference_rpow = 1, factor = 20, base = 10):
30
+ 'convert a ratio of root-power quantities like amplitude or voltage to decibels. by default uses 20 * log⏨(rpow)'
31
+ return factor * _math.log(rpow / reference_rpow, base)
32
+
33
+ def db_to_rpow(db, *, reference_rpow = 1, factor = 20, base = 10):
34
+ 'convert decibels to a ratio of root-power quantities like amplitude or voltage. by default uses 10 ^ (db / 20)'
35
+ return reference_rpow * base ** (db / factor)
36
+
37
+ def pow_to_db(pow, *, reference_pow = 1, factor = 10, base = 10):
38
+ 'convert a power ratio to decibel. by default uses 10 * log⏨(pow)'
39
+ return factor * _math.log(pow / reference_pow, base)
40
+
41
+ def db_to_pow(db, *, reference_pow = 1, factor = 10, base = 10):
42
+ 'convert decibels to a power ratio. by default uses 10 ^ (db / 10)'
43
+ return reference_pow * base ** (db / factor)
44
+
45
+ def rpow_to_np(rpow, *, reference_rpow = 1, factor = 1, base = _math.e):
46
+ 'convert a ratio of root-power quantities into nepers'
47
+ return factor * _math.log(rpow / reference_rpow, base)
48
+
49
+ def np_to_rpow(np, *, reference_rpow = 1, factor = 1, base = _math.e):
50
+ 'convert nepers to a ratio of root-power quantities'
51
+ return reference_rpow * base ** (np / factor)
@@ -0,0 +1,239 @@
1
+ # utilities related to frequency
2
+
3
+ import math as _math
4
+ from daamath import soft_log as _soft_log, soft_exp as _soft_exp
5
+ from typing import Literal as _Literal
6
+
7
+ def hz_to_midi(hz, *, reference_hz = 440, offset_midi = 69) -> float:
8
+ 'convert a midi note to Hz. by default uses 69 (nice) + 12 * log₂(hz / 440)'
9
+ return offset_midi + 12 * _math.log2(hz / reference_hz)
10
+
11
+ def midi_to_hz(midi, *, reference_hz = 440, offset_midi = 69) -> float:
12
+ 'convert a midi note to Hz. by default uses 440 * 2 ^ ((midi - 69nice) / 12)'
13
+ return reference_hz * 2 ** ((midi - offset_midi) / 12)
14
+
15
+ def hz_to_str(
16
+ hz,
17
+ char_double_flat : str = '𝄫',
18
+ char_flat : str = '♭',
19
+ char_natural : str = '♮',
20
+ char_sharp : str = '♯',
21
+ char_double_sharp: str = '𝄪',
22
+ notes : str = 'CDEFGABC',
23
+ ) -> str:
24
+ 'convert frequency in hertz to a 12-TET '
25
+ return notes[(midi + offset) % 7] + str((midi + offset) // 7)
26
+ """if isinstance(midi, int):
27
+ elif isinstance(midi, float):
28
+ temp = round(midi)
29
+ else:
30
+ raise ValueError("expected int or float")"""
31
+
32
+ def hz_to_oct(hz, base = 2) -> float:
33
+ 'convert frequency in hz to log2 frequency. by default uses log₂(hz)'
34
+ return _math.log(base, hz)
35
+
36
+ def oct_to_hz(oct, base = 2) -> int | float:
37
+ 'convert log2 frequency to frequency in hz. by default uses 2ʰᶻ'
38
+ return base ** oct
39
+
40
+ def hz_to_mel(hz, softness = 700, low_x = 0, high_x = 1000, low_y = 0, high_y = 1000):
41
+ """convert frequency in hz to mel scale. it is based on O'Shaughnessy's formula from 1987:
42
+
43
+ 2595 * log⏨(1 + hz / 700) where 2595 should actually be 1000 / log⏨(1 + 1000 / 700)
44
+ """
45
+ return _soft_log(hz, softness = softness, low_x = low_x, high_x = high_x, low_y = low_y, high_y = high_y)
46
+
47
+ def mel_to_hz(mel, softness = 700, low_x = 0, high_x = 1000, low_y = 0, high_y = 1000):
48
+ 'inverse of hz_to_mel'
49
+ return _soft_exp(mel, softness = softness, low_x = low_x, high_x = high_x, low_y = low_y, high_y = high_y)
50
+ #return break_hz * ((1 + norm_hz / break_hz) ** (mel / norm_hz) - 1)
51
+
52
+ def hz_to_mel2(hz, break_hz = 1000, norm_hz = 1000, base = _math.e):
53
+ """convert frequency in hz to mel scale. uses a two-piece fit. the derivative w.r.t. hz is continuous as well, as long as you dont change the base of the logarithm (though the actual function will still be continuous)
54
+
55
+ default formula: hz if hz ≤ 1000 else (1 + ln(hz / 1000)) * 1000
56
+
57
+ this is based on a 1000Hz normalized version of slanley's formula, except its mathematically elegant so it avoids wierd constants:
58
+
59
+ 3 * hz / 200 if hz < 1000 else 15 + 27 * log₆.₄(hz / 1000)
60
+ """
61
+ return hz if hz <= break_hz else (1 + _math.log(hz / break_hz, base)) / (1 + _math.log(norm_hz / break_hz, base)) * norm_hz
62
+
63
+ def mel_to_hz2(mel, break_hz = 1000, norm_hz = 1000, base = _math.e):
64
+ """inverse of hz_to_mel2. converts mel scale to frequency in hz. by default uses:
65
+
66
+ mel if mel ≤ 1000 else 1000 * e ** (mel / 1000 - 1)
67
+ """
68
+ return mel if mel <= break_hz else break_hz * base ** (mel / norm_hz * (1 + _math.log(norm_hz / break_hz, base)) - 1)
69
+
70
+ _beranek_1949_hz = [20, 160, 394, 670, 1000, 1420, 1900, 2450, 3120, 4000, 5100, 6600, 9000, 14000]
71
+ _beranek_1949_mel = [0, 250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3250]
72
+ _umesh_1999_hz = [40, 161, 200, 404, 693, 867, 1000, 2022, 3000, 3393, 4109, 5526, 6500, 7743, 12000]
73
+ _umesh_1999_mel = [43, 257, 300, 514, 771, 928, 1000, 1542, 2000, 2142, 2314, 2600, 2771, 2914, 3228]
74
+ """
75
+ def hz_to_mel3(hz, = _beranek_1949_hz, = _beranek_1949_mel):
76
+ 'convert frequency in hz to mel scale. uses a linear interpolation of the data from beranek 1949'
77
+ def hz_to_mel4(hz, = _umesh_1999_hz, = _umesh_1999_mel):
78
+ 'convert frequency in hz to mel scale. uses a linear interpolation of the data from umesh 1999'
79
+ """
80
+
81
+ def hz_to_bark(hz: float, norm_hz: float = 0, formula: _Literal['zt1', 'zt2', 'zt3', 'tjomov', 'schroeder', 'ht1', 'ht2', 'wsg'] = 'ht2') -> float:
82
+ 'convert frequency in Hertz to the bark scale'
83
+ match formula:
84
+ case 'zt2':
85
+ result = 14.2 * _math.log10(hz / 1000) + 8.7
86
+ case 'zt3':
87
+ result = 6.578 * _math.log(hz) - 36.99
88
+ case 'zt1':
89
+ result = 13 * arctan(0.00076 * hz) + 3.5 * arctan((hz / 7500) ** 2)
90
+ case 'ht1':
91
+ result = 26.81 * hz / (1960 + hz) - 0.53
92
+ case 'ht2':
93
+ bark = 26.81 * hz / (1960 + hz) - 0.53
94
+ if bark < 2:
95
+ result = bark + 0.15 * (2 - bark)
96
+ elif bark > 20.1:
97
+ result = bark + 0.22 * (bark - 20.1)
98
+ else:
99
+ result = bark
100
+ case 'tjomov':
101
+ result = 600 * _math.sinh(hz / 6.7) + 20
102
+ case 'schroeder':
103
+ result = 650 * _math.sinh(hz / 7)
104
+ case 'wsg':
105
+ result = 6 * _math.asinh(hz / 600)
106
+ case _:
107
+ raise ValueError('unrecognized formula')
108
+
109
+ if norm_hz > 0:
110
+ return result / hz_to_bark(norm_hz, formula = formula) * norm_hz
111
+
112
+ return result
113
+
114
+ def bark_to_hz(bark: float, norm_hz: float = 0, formula: str = 'ht2') -> float:
115
+ 'inverse of hz_to_bark. converts bark scale values to frequency in Hertz'
116
+ if norm_hz > 0:
117
+ bark = bark * hz_to_bark(norm_hz, formula = formula) / norm_hz
118
+
119
+ match formula:
120
+ case 'zt2':
121
+ result = 10 ** ((bark - 33.9) / 14.2)
122
+ case 'zt3':
123
+ result = _math.exp((bark + 36.99) / 6.578)
124
+ case 'ht1':
125
+ if hasattr(_math, 'fma'):
126
+ result = _math.fma(bark, 1960, 0.53) / (26.28 - bark)
127
+ else:
128
+ result = 1960 * (bark + 0.53) / (26.28 - bark)
129
+ case 'ht2':
130
+ if bark < 2:
131
+ bark = (bark - 0.3) / 0.85
132
+ elif bark > 20.1:
133
+ bark = (bark + 4.422) / 1.22
134
+ result = 1960 * (bark + 0.53) / (26.28 - bark)
135
+ case 'tjomov':
136
+ result = 6.7 * _math.asinh((bark - 20) / 600)
137
+ case 'schroeder':
138
+ result = 7 * _math.asinh(bark / 650)
139
+ case 'wsg':
140
+ result = 600 * _math.sinh(bark / 6)
141
+ case _:
142
+ raise ValueError('unrecognized formula')
143
+
144
+ return result
145
+ """
146
+
147
+ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
148
+ 60 150 250 350 450 570 700 840 1000 1170 1370 1600 1850 2150 2500 2900 3400 4000 4800 5800 7000 8500 10500 13500
149
+ 20 100 200 300 400 510 630 770 920 1080 1270 1480 1720 2000 2320 2700 3150 3700 4400 5300 6400 7700 9500 12000 15500
150
+ 80 100 100 100 110 120 140 150 160 190 210 240 280 320 380 450 550 700 900 1100 1300 1800 2500 3500
151
+ no. center cutoff bandwidth
152
+ 20
153
+ 1 60 100 80
154
+ 2 150 200 100
155
+ 3 250 300 100
156
+ 4 350 400 100
157
+ 5 450 510 110
158
+ 6 570 630 120
159
+ 7 700 770 140
160
+ 8 840 920 150
161
+ 9 1000 1080 160
162
+ 10 1170 1270 190
163
+ 11 1370 1480 210
164
+ 12 1600 1720 240
165
+ 13 1850 2000 280
166
+ 14 2150 2320 320
167
+ 15 2500 2700 380
168
+ 16 2900 3150 450
169
+ 17 3400 3700 550
170
+ 18 4000 4400 700
171
+ 19 4800 5300 900
172
+ 20 5800 6400 1100
173
+ 21 7000 7700 1300
174
+ 22 8500 9500 1800
175
+ 23 10500 12000 2500
176
+ 24 13500 15500 3500
177
+ """
178
+
179
+ def hz_to_erbs(hz, norm_hz = 0):
180
+ """convert hz to erbs (also known as caws)
181
+
182
+ formula: A * log1p(B * (hz / (hz + C))) which is mathematically equivalent to A * ln(1 + (hz * B) / (hz + C))
183
+ where: A = 100000 / √80109737
184
+ ≈ 11.1726796613307508659409591598553369046875791603224092625910488…
185
+ B = (9339 + √80109737) / (9339 - √80109737) - 1
186
+ ≈ 46.0653791116351008187372868311781991418099625357253121238190101…
187
+ C = (9339 + √80109737) / 1.246
188
+ ≈ 14678.4946168094343650325240435293097711148959365979858846912749…
189
+ the constants are precomputed using an arbitrary precision calculator so they are as precise as a double can be
190
+ this is a numerically stable version of the integral of reciprocal of 6.23khz² + 93.39khz + 28.52. with the integration constant set so that hz_to_erbs(0) = 0, the formula is ln(abs(1.246 * hz + 9339 - √80109737) / abs(1.246 * hz + 9339 + √80109737)) * 100000 / √80109737 + ln(abs(9339 - √80109737) / abs(9339 + √80109737)) * 10000/ √80109737. derive the rest yourself
191
+
192
+ an alternate formula is: log((hz + A) / (hz + B), base = C) + D
193
+ where: A = (9339 - √80109737) / 1.246
194
+ ≈ 311.87456457098324…
195
+ B = (9339 + √80109737) / 1.246
196
+ ≈ 14678.494616809434…
197
+ C = e ^ (100000 / √80109737)
198
+ ≈ 1.0936317547750625…
199
+ D = log(B / A, base = C) = ln(B / A) * 10000 / √80109737
200
+ ≈ 43.031996702539864…
201
+ """
202
+ if norm_hz > 0:
203
+ return _math.log1p(46.0653791116351008187372868311781991418099625357253121238190101 * (hz / (hz + 14678.4946168094343650325240435293097711148959365979858846912749))) / _math.log1p(46.0653791116351008187372868311781991418099625357253121238190101 * (norm_hz / (norm_hz + 14678.4946168094343650325240435293097711148959365979858846912749)))
204
+ else:
205
+ return 11.1726796613307508659409591598553369046875791603224092625910488 * _math.log1p(46.0653791116351008187372868311781991418099625357253121238190101 * (hz / (hz + 14678.4946168094343650325240435293097711148959365979858846912749)))
206
+
207
+ def erbs_to_hz(erbs):
208
+ """inverse of hz_to_erbs. there are many ways to perform this but they are less numerically stable than hz_to_erbs
209
+
210
+ default formula: A * (T / (B - T))
211
+ where A = (9339 + √80109737) / 1.246
212
+ ≈ 14678.4946168094343650325240435293097711148959365979858846912…
213
+ B = (9339 + √80109737) / (9339 - √80109737) - 1
214
+ ≈ 46.0653791116351008187372868311781991418099625357253121238190…
215
+ T = expm1 (erbs * C) which is mathematically equivalent to e ^ (erbs * C) - 1
216
+ where C = √80109737 / 100000
217
+ ≈ 0.089504042925445552188305249582375199748091603370010904123253286141232001286708029
218
+
219
+ the constants are precomputed using an arbitrary precision calculator so they are as precise as a double can be
220
+
221
+ there may be other forms. i havent explored them yet
222
+ """
223
+ temp = _math.expm1(erbs / 11.1726796613307508659409591598553369046875791603224092625910488)
224
+
225
+ return 14678.4946168094343650325240435293097711148959365979858846912749 * (temp / (46.0653791116351008187372868311781991418099625357253121238190101 - temp))
226
+
227
+ def hz_to_gw(hz, softness = 165.4, low_x = 20, low_y = 0, high_x = 20000, high_y = 1):
228
+ """convert frequency in hertz to cochlear frequency–place map according to the greenwood function
229
+
230
+ this is the "actual" form of greenwood's original function which is hz = 165.4 * (10 ^ (2.1 * gw) - 0.88). the inverse is gw = log10(hz / 165.4 + 0.88) / 2.1. in my formula, 165.4 stays as it is, but 0.88 is supposed to be 1 - 20 / 165.4 and 2.1 is supposed to be log10(1 + (20000 - 20) / 165.4). derive the rest yourself
231
+ """
232
+ return _soft_log(hz, softness = softness, low_x = low_x, high_x = high_x, low_y = low_y, high_y = high_y)
233
+
234
+ def gw_to_hz(gw, softness = 165.4, low_x = 0, low_y = 20, high_x = 1, high_y = 20000):
235
+ """inverse of hz_to_gw. derived from greenwood's formula hz = 165.4 * (10 ^ (2.1 * gw) - 0.88) where 0.88 is supposed to be 1 - 20 / 165.4 and 2.1 is supposed to be log10(1 + (20000 - 20) / 165.4). derive the rest yourself
236
+ """
237
+ return _soft_exp(gw, softness = softness, low_x = low_x, high_x = high_x, low_y = low_y, high_y = high_y)
238
+
239
+
@@ -0,0 +1,120 @@
1
+ from daamath import lerp as _plerp, unlerp as _unplerp # for frequency lerping
2
+ from collections.abc import Sequence as _Sequence
3
+
4
+ _DEFAULT_LOW_HZ = 20
5
+ _DEFAULT_HIGH_HZ = 20000
6
+
7
+ def lerp_freq(
8
+ value: float ,
9
+ low : float = _DEFAULT_LOW_HZ ,
10
+ high : float = _DEFAULT_HIGH_HZ,
11
+ power: float = 0 ,
12
+ ) -> float:
13
+ 'lerp a fractional value to a frequency in an interval. by default, maps [0, 1] → [20, 20000] in oct space'
14
+ return _plerp(value, low, high, power = power)
15
+
16
+ def unlerp_freq(
17
+ value: float ,
18
+ low : float = _DEFAULT_LOW_HZ ,
19
+ high : float = _DEFAULT_HIGH_HZ,
20
+ power: float = 0 ,
21
+ ) -> float:
22
+ 'unlerp a frequency in an interval to a fractional value. by default, maps [20, 20000] in hz space to [0, 1]'
23
+ return _unplerp(value, low, high, power = power)
24
+
25
+ # finished up to this ----------------------------------------------------------
26
+
27
+ # string helpers
28
+
29
+ # factories --------------------------------------------------------------------
30
+ """
31
+ def multilerp(value: float, x_values: _Iterable[float], y_values: _Iterable[float], is_sorted: bool = False) -> float:
32
+ 'a slow inefficient ass function that linearly interpolates a bunch of points and lets you sample on them'
33
+
34
+ if x_value
35
+
36
+ #x_values, y_values = (x_values, y_values) if is_sorted else zip(*sorted(zip(x_values, y_values)))
37
+
38
+ index_low: int = 0
39
+ index_high: int = 0
40
+
41
+ if is_sorted:
42
+ for index, x_value in enumerate(x_values):
43
+ if x_value > value:
44
+ break
45
+ index_low =
46
+ else:
47
+ for index, x_value in enumerate(x_values):
48
+ if x_value == value:
49
+ return y_values[index]
50
+ index_low = index if x_value < value and index_low < index else index_low
51
+ index_high = index if x_value > value and index_high > index else index_high
52
+
53
+ value_intermediate = (value - x_values[index_low]) / (x_values[index_high] - x_values[index_low])
54
+ return y_values[index_low] * (1 - value_intermediate) + y_values[index_high] * value_intermediate
55
+ """
56
+ """
57
+ def factory_ascii_converter(from_str: str, to_str: str) -> _Callable[[str], str]:
58
+ # create a dictionary mapping for all non-space characters
59
+ mapping = {a: b for a, b in zip(from_str, to_str) if b.strip()}
60
+ def converter(s: str) -> str:
61
+ return ''.join(mapping.get(ch, ch) for ch in s)
62
+ return converter
63
+
64
+ def factory_interpolation(x_values: _Sequence[int|float], y_values: _Sequence[int|float]) -> _Callable[[int|float], int|float]:
65
+ 'a factory that turns a bunch of points into a sample-able ℝ ⟼ ℝ function by a polynomial interpolation'
66
+ raise NotImplementedError
67
+ """
68
+
69
+
70
+ # specific converters
71
+ #to_superscript = factory_ascii_converter(ASCII, ASCII_SUPERSCRIPT)
72
+ #to_subscript = factory_ascii_converter(ASCII, ASCII_SUBSCRIPT)
73
+
74
+ # add slerp and unslerp
75
+
76
+ # DSP --------------------------------------------------------------------------
77
+
78
+ def wave_square(length: int, amplitude: float = 1, period_samples: int = 2, offset: float = 0) -> _Sequence[float]:
79
+ if not isinstance(period_samples, int) or period_samples % 2 != 0:
80
+ raise ValueError("only even integer period samples supported for now")
81
+
82
+ if length % period_samples != 0:
83
+ raise ValueError("length should be a multiple of period_samples")
84
+
85
+ one_shot = [offset + amplitude] * (period_samples // 2) + [offset - amplitude] * (period_samples // 2)
86
+ return one_shot * (length // period_samples)
87
+
88
+ '''
89
+ def coeffs_from_roots(roots: _Sequence[float]) -> _Sequence[float]:
90
+ """
91
+ given an iterable of roots r1, r2, ..., rn
92
+ returns coefficients [c0, c1, ..., cn] such that
93
+
94
+ (x - r1)(x - r2)...(x - rn)
95
+ = c0*x^n + c1*x^(n-1) + ... + cn
96
+ """
97
+ coeffs = [1]
98
+
99
+ for r in roots:
100
+ new = [0] * (len(coeffs) + 1)
101
+ for i, c in enumerate(coeffs):
102
+ new[i] += c # multiply by x
103
+ new[i + 1] -= r * c # multiply by -r
104
+ coeffs = new
105
+
106
+ return coeffs
107
+ '''
108
+ """
109
+ Fs = 48000
110
+ f0 = 200
111
+ dBgain = 10
112
+ Q = 2
113
+
114
+ A = 10 ** (dBgain / 40)
115
+ w0 = 2 * _math.pi * f0 * Fs
116
+ cos_w0 = cos(w0)
117
+ sin_w0 = sin(w0)
118
+
119
+ alpha = sin_w0 / (2 * Q)
120
+ """
@@ -0,0 +1,14 @@
1
+ # oscillators
2
+
3
+ # these should ideally be generators instead of returning the whole array as is
4
+
5
+ def wave_square(length: int, amplitude: float = 1, period_samples: int = 2, offset: float = 0) -> _Sequence[float]:
6
+ if not isinstance(period_samples, int) or period_samples % 2 != 0:
7
+ raise ValueError("only even integer period samples supported for now")
8
+
9
+ if length % period_samples != 0:
10
+ raise ValueError("length should be a multiple of period_samples")
11
+
12
+ one_shot = [offset + amplitude] * (period_samples // 2) + [offset - amplitude] * (period_samples // 2)
13
+ return one_shot * (length // period_samples)
14
+
@@ -0,0 +1,88 @@
1
+ import daamath as _dm
2
+
3
+ _R80=[
4
+ 1.00,1.03,1.06,1.09, 1.12,1.15,1.18,1.22,
5
+ 1.25,1.28,1.32,1.36, 1.40,1.45,1.50,1.55,
6
+ 1.60,1.65,1.70,1.75, 1.80,1.85,1.90,1.95,
7
+ 2.00,2.06,2.12,2.18, 2.24,2.30,2.36,2.43,
8
+ 2.50,2.58,2.65,2.72, 2.80,2.90,3.00,3.07,
9
+ 3.15,3.25,3.35,3.45, 3.55,3.65,3.75,3.87,
10
+ 4.00,4.12,4.25,4.37, 4.50,4.62,4.75,4.87,
11
+ 5.00,5.15,5.30,5.45, 5.60,5.80,6.00,6.15,
12
+ 6.30,6.50,6.70,6.90, 7.10,7.30,7.50,7.75,
13
+ 8.00,8.25,8.50,8.75, 9.00,9.25,9.50,9.75]
14
+
15
+ _E24 = [
16
+ 1.0,1.1,1.2,1.3, 1.5,1.6,1.8,2.0,
17
+ 2.2,2.4,2.7,3.0, 3.3,3.6,3.9,4.3,
18
+ 4.7,5.1,5.6,6.2, 6.8,7.5,8.2,9.1]
19
+
20
+ _E192 = [
21
+ 1.00,1.01,1.02,1.04, 1.05,1.06,1.07,1.09,
22
+ 1.10,1.11,1.13,1.14, 1.15,1.17,1.18,1.20,
23
+ 1.21,1.23,1.24,1.26, 1.27,1.29,1.30,1.32,
24
+ 1.33,1.35,1.37,1.38, 1.40,1.42,1.43,1.45,
25
+
26
+ 1.47,1.49,1.50,1.52, 1.54,1.56,1.58,1.60,
27
+ 1.62,1.64,1.65,1.67, 1.69,1.72,1.74,1.76,
28
+ 1.78,1.80,1.82,1.84, 1.87,1.89,1.91,1.93,
29
+ 1.96,1.98,2.00,2.03, 2.05,2.08,2.10,2.13,
30
+
31
+ 2.15,2.18,2.21,2.23, 2.26,2.29,2.32,2.34,
32
+ 2.37,2.40,2.43,2.46, 2.49,2.52,2.55,2.58,
33
+ 2.61,2.64,2.67,2.71, 2.74,2.77,2.80,2.84,
34
+ 2.87,2.91,2.94,2.98, 3.01,3.05,3.09,3.12,
35
+
36
+ 3.16,3.20,3.24,3.28, 3.32,3.36,3.40,3.44,
37
+ 3.48,3.52,3.57,3.61, 3.65,3.70,3.74,3.79,
38
+ 3.83,3.88,3.92,3.97, 4.02,4.07,4.12,4.17,
39
+ 4.22,4.27,4.32,4.37, 4.42,4.48,4.53,4.59,
40
+
41
+ 4.64,4.70,4.75,4.81, 4.87,4.93,4.99,5.05,
42
+ 5.11,5.17,5.23,5.30, 5.36,5.42,5.49,5.56,
43
+ 5.62,5.69,5.76,5.83, 5.90,5.97,6.04,6.12,
44
+ 6.19,6.26,6.34,6.42, 6.49,6.57,6.65,6.73,
45
+
46
+ 6.81,6.90,6.98,7.06, 7.15,7.23,7.32,7.41,
47
+ 7.50,7.59,7.68,7.77, 7.87,7.96,8.06,8.16,
48
+ 8.25,8.35,8.45,8.56, 8.66,8.76,8.87,8.98,
49
+ 9.09,9.20,9.31,9.42, 9.53,9.65,9.76,9.88]
50
+
51
+ R = {
52
+ 5: _R80[::16],
53
+ 10: _R80[::8],
54
+ 20: _R80[::4],
55
+ 40: _R80[::2],
56
+ 80: _R80}
57
+
58
+ E_prec = {
59
+ 48: _E192[::4],
60
+ 96: _E192[::2],
61
+ 192: _E192}
62
+
63
+ E_pref = {
64
+ 3: _E24[::8],
65
+ 6: _E24[::4],
66
+ 12: _E24[::2],
67
+ 24: _E24}
68
+
69
+ E = E_pref | E_prec
70
+
71
+ def custom_series(count, decimal_digits, base = 10, floored: bool = False):
72
+ for i in range(count):
73
+ actual = base ** ((i + count * decimal_digits) / count)
74
+
75
+ if floored:
76
+ return _dm.floor(actual)
77
+
78
+ low = _dm.floor(actual)
79
+ high = _dm.ceil(actual)
80
+ low_error = _dm.ln_div(low, actual)
81
+ high_error = _dm.ln_div(high, actual)
82
+
83
+ #print(actual, low, high, low_error, high_error)
84
+
85
+ if abs(low_error) < abs(high_error):
86
+ yield low
87
+ else:
88
+ yield high
@@ -0,0 +1,60 @@
1
+ # utilities related to xenharmonic theory
2
+ from numbers import Rational as _Rational, Integral as _Integral
3
+ from collections.abc import Collection as _Collection
4
+ from collections import namedtuple as _namedtuple
5
+ from fractions import Fraction as _Fraction
6
+ import math as _math
7
+
8
+ def tenney_height(ratio: _Rational) -> float:
9
+ 'log2(numerator * denominator). imposes an order as a scaled norm on the rational numbers'
10
+ return _math.log2(ratio.numerator * ratio.denominator)
11
+
12
+ def complexity(notes: _Collection[_Integral]) -> _Integral:
13
+ 'how complex is a chord?'
14
+ return _math.prod(notes) // (_math.gcd(*notes) ** len(notes))
15
+
16
+ _idfk = _namedtuple('Fraction', ['numerator', 'denominator'])
17
+
18
+ def stern_brocot_dfs(depth: int, start: _Rational = _Fraction(0, 1), stop: _Rational = _idfk(0, 1)) -> _Sequence[_Rational]:
19
+ 'generate brocot tree as a sequence down to a certain depth, in order of absolute value'
20
+
21
+ # initialize array
22
+ size: int = 2 ** depth + 1
23
+ result: list[_Rational] = [None] * size
24
+ result[0] = start
25
+ result[-1] = stop
26
+
27
+ for depth in range(depth + 1):
28
+ step = size // 2 ** depth
29
+
30
+ for index in range(step, size, step * 2):
31
+ numerator = result[index - step].numerator + result[index + step].numerator
32
+ denominator = result[index - step].denominator + result[index + step].denominator
33
+ result[index] = _Fraction(numerator, denominator)
34
+
35
+ return result
36
+
37
+ def stern_brocot_bfs(depth: int, start: _Rational = _Fraction(0, 1), stop: _Rational = _idfk(0, 1)) -> _Sequence[_Rational]:
38
+ 'generate brocot tree as a sequence down to a certain depth, in order of appearance'
39
+ seq = stern_brocot_dfs(depth, start, stop)
40
+ size = len(seq)
41
+
42
+ def index_translate(index: int, size = size) -> int:
43
+ match index:
44
+ case 0: return 0
45
+ case 1: return size - 1
46
+ case _: return int(size * ((index - 0.5) * 2 ** -_math.floor(_math.log2(index - 1)) - 1))
47
+
48
+ return [seq[index_translate(index)] for index in range(size)]
49
+
50
+ def prime_limit(ratio: _Rational) -> int:
51
+ 'return the highest prime that appears in a ratio'
52
+ import sympy
53
+ #return daamath.Monzo(ratio.numerator, ratio.denominator).keys()[-1]
54
+ factors = set(sympy.factorint(ratio.numerator).keys()) | set(sympy.factorint(ratio.denominator).keys())
55
+ return max(factors) if factors else 2
56
+
57
+ def ed_note(interval: float, ed_step: float) -> float:
58
+ 'find the note of the given interval in the given ED tuning. returns log(interval, base=ed_step)'
59
+ return _math.log(interval, ed_step)
60
+