beqforge 0.1.0__py3-none-any.whl
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.
- beq_common/AGENTS.md +34 -0
- beq_common/__init__.py +6 -0
- beq_common/biquad.py +427 -0
- beq_common/provenance.py +44 -0
- beq_common/publication.py +20 -0
- beq_common/types.py +30 -0
- beqforge/AGENTS.md +530 -0
- beqforge/__init__.py +79 -0
- beqforge/accept.py +739 -0
- beqforge/beqd.py +202 -0
- beqforge/biquad.py +27 -0
- beqforge/cache.py +616 -0
- beqforge/charts.py +375 -0
- beqforge/cli.py +61 -0
- beqforge/design.py +394 -0
- beqforge/designer.py +624 -0
- beqforge/diagnose.py +763 -0
- beqforge/explain.py +248 -0
- beqforge/extraction.py +469 -0
- beqforge/filters.py +1161 -0
- beqforge/harness.py +457 -0
- beqforge/identify.py +295 -0
- beqforge/material.py +187 -0
- beqforge/pipeline.py +1995 -0
- beqforge/record.py +525 -0
- beqforge/reference.py +153 -0
- beqforge/rolloff.py +138 -0
- beqforge/verify.py +486 -0
- beqforge-0.1.0.dist-info/METADATA +399 -0
- beqforge-0.1.0.dist-info/RECORD +72 -0
- beqforge-0.1.0.dist-info/WHEEL +4 -0
- beqforge-0.1.0.dist-info/entry_points.txt +4 -0
- beqforge-0.1.0.dist-info/licenses/LICENCE.md +21 -0
- beqforge_device_check/AGENTS.md +68 -0
- beqforge_device_check/__init__.py +3 -0
- beqforge_device_check/analyse.py +788 -0
- beqforge_device_check/audio.py +205 -0
- beqforge_device_check/catalogue.py +359 -0
- beqforge_device_check/cli.py +956 -0
- beqforge_device_check/coefficients.py +100 -0
- beqforge_device_check/engines.py +531 -0
- beqforge_device_check/evidence.py +245 -0
- beqforge_device_check/manifest.py +337 -0
- beqforge_device_check/measurement.py +353 -0
- beqforge_device_check/profiles.py +117 -0
- beqforge_device_check/qualification.py +548 -0
- beqforge_device_check/report.py +727 -0
- beqforge_device_check/transactions.py +708 -0
- beqforge_device_check/verify.py +250 -0
- beqoptimiser/AGENTS.md +74 -0
- beqoptimiser/__init__.py +34 -0
- beqoptimiser/cache.py +358 -0
- beqoptimiser/cli.py +184 -0
- beqoptimiser/core.py +356 -0
- beqoptimiser/data/seed-manifest.json +40 -0
- beqoptimiser/data/seed.json.gz +0 -0
- tools/__init__.py +0 -0
- tools/design_beq.py +499 -0
- tools/designer_server.py +413 -0
- tools/extract.py +223 -0
- tools/ledger_template.html +416 -0
- tools/negative_corpus.py +192 -0
- tools/optimiser_catalogue_report.py +550 -0
- tools/prepare_device_check_helper.py +140 -0
- tools/render_ledger.py +297 -0
- tools/replay.py +109 -0
- tools/seed_optimiser_cache.py +190 -0
- tools/smoke_test_device_check.py +97 -0
- tools/smoke_test_exe.py +281 -0
- tools/smoke_test_install.py +163 -0
- tools/summarise.py +124 -0
- tools/validate_evidence.py +40 -0
beq_common/AGENTS.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Shared primitives
|
|
2
|
+
|
|
3
|
+
Read the repository [AGENTS.md](../AGENTS.md) first. Commands and paths below are relative to
|
|
4
|
+
the repository root. This package imports no workflow package and contains only neutral
|
|
5
|
+
primitives needed across workflows.
|
|
6
|
+
|
|
7
|
+
| File | Responsibility |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `types.py` | Shared filter specifications and design types |
|
|
10
|
+
| `biquad.py` | Canonical RBJ biquad arithmetic |
|
|
11
|
+
| `publication.py` | Canonical publication rounding |
|
|
12
|
+
| `provenance.py` | Shared source/build revision handling |
|
|
13
|
+
| `__init__.py` | The single distribution version and public shared types |
|
|
14
|
+
|
|
15
|
+
Changes can reach multiple workflows. Read the affected packages' instructions before editing;
|
|
16
|
+
designer-reachable changes require the regression procedure in
|
|
17
|
+
[beqforge/AGENTS.md](../beqforge/AGENTS.md). Shared modules reached by the designer must remain
|
|
18
|
+
covered by its record and stage source fingerprints. Optimiser cache implementation identity
|
|
19
|
+
and frozen device-check provenance must continue to reflect their numerical dependencies.
|
|
20
|
+
|
|
21
|
+
RBJ arithmetic also exists in the designer's vectorised `beqforge.filters._sos_from_parameters`.
|
|
22
|
+
When changing the equations, update both implementations together and retain their equivalence
|
|
23
|
+
test. `beqforge/biquad.py` and legacy designer type exports are compatibility paths to shared
|
|
24
|
+
primitives; do not turn them into independent implementations.
|
|
25
|
+
|
|
26
|
+
The version in `beq_common.__version__` is consumed by Hatch, every workflow and provenance.
|
|
27
|
+
A version change can invalidate recorded/cache identities, including the bundled optimiser
|
|
28
|
+
seed; account for that when preparing a release. Frozen builds have no checkout source to
|
|
29
|
+
hash, so preserve their baked revision stamps.
|
|
30
|
+
|
|
31
|
+
Check affected numerical tests, fingerprint/invalidation tests and
|
|
32
|
+
`tests/test_package_boundaries.py`. Shared-code changes must retain isolated dependency
|
|
33
|
+
profiles and avoid importing SciPy, plotting or device libraries merely to expose types or
|
|
34
|
+
RBJ arithmetic.
|
beq_common/__init__.py
ADDED
beq_common/biquad.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SOS(ABC):
|
|
8
|
+
def __init__(self, f_id=-1, fs=48000):
|
|
9
|
+
self.__id = f_id
|
|
10
|
+
self.fs = fs
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def id(self):
|
|
14
|
+
return self.__id
|
|
15
|
+
|
|
16
|
+
@id.setter
|
|
17
|
+
def id(self, id):
|
|
18
|
+
self.__id = id
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def get_sos(self) -> list[list[float]] | None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Biquad(SOS):
|
|
26
|
+
def __init__(self, fs, f_id=-1):
|
|
27
|
+
super().__init__(f_id=f_id, fs=fs)
|
|
28
|
+
self.a, self.b = self._compute_coeffs()
|
|
29
|
+
self.__transfer_function = None
|
|
30
|
+
|
|
31
|
+
def __eq__(self, o: object) -> bool:
|
|
32
|
+
equal = self.__class__.__name__ == o.__class__.__name__
|
|
33
|
+
equal &= self.fs == o.fs
|
|
34
|
+
return equal
|
|
35
|
+
|
|
36
|
+
def __repr__(self):
|
|
37
|
+
return self.description
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def description(self):
|
|
41
|
+
description = ""
|
|
42
|
+
if hasattr(self, "display_name"):
|
|
43
|
+
description += self.display_name
|
|
44
|
+
return description
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def filter_type(self):
|
|
48
|
+
return self.__class__.__name__
|
|
49
|
+
|
|
50
|
+
def __len__(self):
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def _compute_coeffs(self):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def sort_key(self):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def __format_index(prefix, idx, show_index):
|
|
63
|
+
if show_index:
|
|
64
|
+
return f"{prefix}{idx}="
|
|
65
|
+
else:
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
def get_sos(self) -> list[list[float]] | None:
|
|
69
|
+
return [np.concatenate((self.b, self.a)).tolist()]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Gain(Biquad):
|
|
73
|
+
def __init__(self, fs, gain, f_id=-1):
|
|
74
|
+
self.gain = gain
|
|
75
|
+
super().__init__(fs, f_id=f_id)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def filter_type(self):
|
|
79
|
+
return "Gain"
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def display_name(self):
|
|
83
|
+
return "Gain"
|
|
84
|
+
|
|
85
|
+
def _compute_coeffs(self):
|
|
86
|
+
return np.array([1.0, 0.0, 0.0]), np.array(
|
|
87
|
+
[10.0 ** (self.gain / 20.0), 0.0, 0.0]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def resample(self, new_fs):
|
|
91
|
+
"""
|
|
92
|
+
Creates a filter at the specified fs.
|
|
93
|
+
:param new_fs: the new fs.
|
|
94
|
+
:return: the new filter.
|
|
95
|
+
"""
|
|
96
|
+
return Gain(new_fs, self.gain, f_id=self.id)
|
|
97
|
+
|
|
98
|
+
def sort_key(self):
|
|
99
|
+
return f"00000{self.gain:05}{self.filter_type}"
|
|
100
|
+
|
|
101
|
+
def to_json(self):
|
|
102
|
+
return {"_type": self.__class__.__name__, "fs": self.fs, "gain": self.gain}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BiquadWithQ(Biquad):
|
|
106
|
+
def __init__(self, fs, freq, q, f_id=-1):
|
|
107
|
+
self.freq = round(float(freq), 2)
|
|
108
|
+
self.q = float(q)
|
|
109
|
+
self.w0 = 2.0 * math.pi * freq / fs
|
|
110
|
+
self.cos_w0 = math.cos(self.w0)
|
|
111
|
+
self.sin_w0 = math.sin(self.w0)
|
|
112
|
+
self.alpha = self.sin_w0 / (2.0 * self.q)
|
|
113
|
+
super().__init__(fs, f_id=f_id)
|
|
114
|
+
|
|
115
|
+
def __eq__(self, o: object) -> bool:
|
|
116
|
+
return super().__eq__(o) and self.freq == o.freq
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def description(self):
|
|
120
|
+
return super().description + f" {self.freq}/{self.q:.4g}"
|
|
121
|
+
|
|
122
|
+
def sort_key(self):
|
|
123
|
+
return f"{self.freq:05}00000{self.filter_type}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Passthrough(Gain):
|
|
127
|
+
def __init__(self, fs=1000, f_id=-1):
|
|
128
|
+
super().__init__(fs, 0, f_id=f_id)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def display_name(self):
|
|
132
|
+
return "Passthrough"
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def description(self):
|
|
136
|
+
return "Passthrough"
|
|
137
|
+
|
|
138
|
+
def sort_key(self):
|
|
139
|
+
return "ZZZZZZZZZZZZZZ"
|
|
140
|
+
|
|
141
|
+
def resample(self, new_fs):
|
|
142
|
+
return Passthrough(fs=new_fs, f_id=self.id)
|
|
143
|
+
|
|
144
|
+
def to_json(self):
|
|
145
|
+
return {"_type": self.__class__.__name__, "fs": self.fs}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class BiquadWithQGain(BiquadWithQ):
|
|
149
|
+
def __init__(self, fs, freq, q, gain, f_id=-1):
|
|
150
|
+
self.gain = round(float(gain), 3)
|
|
151
|
+
super().__init__(fs, freq, q, f_id=f_id)
|
|
152
|
+
|
|
153
|
+
def __eq__(self, o: object) -> bool:
|
|
154
|
+
return super().__eq__(o) and self.gain == o.gain
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def description(self):
|
|
158
|
+
return super().description + f"/{self.gain}dB"
|
|
159
|
+
|
|
160
|
+
def sort_key(self):
|
|
161
|
+
return f"{self.freq:05}{self.gain:05}{self.filter_type}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class PeakingEQ(BiquadWithQGain):
|
|
165
|
+
"""
|
|
166
|
+
H(s) = (s^2 + s*(A/Q) + 1) / (s^2 + s/(A*Q) + 1)
|
|
167
|
+
|
|
168
|
+
b0 = 1 + alpha*A
|
|
169
|
+
b1 = -2*cos(w0)
|
|
170
|
+
b2 = 1 - alpha*A
|
|
171
|
+
a0 = 1 + alpha/A
|
|
172
|
+
a1 = -2*cos(w0)
|
|
173
|
+
a2 = 1 - alpha/A
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, fs, freq, q, gain, f_id=-1):
|
|
177
|
+
super().__init__(fs, freq, q, gain, f_id=f_id)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def filter_type(self):
|
|
181
|
+
return "PEQ"
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def display_name(self):
|
|
185
|
+
return "PEQ"
|
|
186
|
+
|
|
187
|
+
def _compute_coeffs(self):
|
|
188
|
+
A = 10.0 ** (self.gain / 40.0)
|
|
189
|
+
a = np.array(
|
|
190
|
+
[1.0 + self.alpha / A, -2.0 * self.cos_w0, 1.0 - self.alpha / A],
|
|
191
|
+
dtype=np.float64,
|
|
192
|
+
)
|
|
193
|
+
b = np.array(
|
|
194
|
+
[1.0 + self.alpha * A, -2.0 * self.cos_w0, 1.0 - self.alpha * A],
|
|
195
|
+
dtype=np.float64,
|
|
196
|
+
)
|
|
197
|
+
return a / a[0], b / a[0]
|
|
198
|
+
|
|
199
|
+
def resample(self, new_fs):
|
|
200
|
+
"""
|
|
201
|
+
Creates a filter at the specified fs.
|
|
202
|
+
:param new_fs: the new fs.
|
|
203
|
+
:return: the new filter.
|
|
204
|
+
"""
|
|
205
|
+
return PeakingEQ(new_fs, self.freq, self.q, self.gain, f_id=self.id)
|
|
206
|
+
|
|
207
|
+
def to_json(self):
|
|
208
|
+
return {
|
|
209
|
+
"_type": self.__class__.__name__,
|
|
210
|
+
"fs": self.fs,
|
|
211
|
+
"fc": self.freq,
|
|
212
|
+
"q": self.q,
|
|
213
|
+
"gain": self.gain,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def q_to_s(q, gain):
|
|
218
|
+
"""
|
|
219
|
+
translates Q to S for a shelf filter.
|
|
220
|
+
:param q: the Q.
|
|
221
|
+
:param gain: the gain.
|
|
222
|
+
:return: the S.
|
|
223
|
+
"""
|
|
224
|
+
return 1.0 / (
|
|
225
|
+
(
|
|
226
|
+
((1.0 / q) ** 2.0 - 2.0)
|
|
227
|
+
/ ((10.0 ** (gain / 40.0)) + 1.0 / (10.0 ** (gain / 40.0)))
|
|
228
|
+
)
|
|
229
|
+
+ 1.0
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def s_to_q(s, gain):
|
|
234
|
+
"""
|
|
235
|
+
translates S to Q for a shelf filter.
|
|
236
|
+
:param s: the S.
|
|
237
|
+
:param gain: the gain.
|
|
238
|
+
:return: the Q.
|
|
239
|
+
"""
|
|
240
|
+
A = 10.0 ** (gain / 40.0)
|
|
241
|
+
return 1.0 / math.sqrt(((A + 1.0 / A) * (1.0 / s - 1.0)) + 2.0)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class Shelf(BiquadWithQGain):
|
|
245
|
+
def __init__(self, fs, freq, q, gain, count, f_id=-1):
|
|
246
|
+
self.A = 10.0 ** (gain / 40.0)
|
|
247
|
+
super().__init__(fs, freq, q, gain, f_id=f_id)
|
|
248
|
+
self.count = count
|
|
249
|
+
self.__cached_cascade = None
|
|
250
|
+
|
|
251
|
+
def q_to_s(self):
|
|
252
|
+
"""
|
|
253
|
+
:return: the filter Q as S
|
|
254
|
+
"""
|
|
255
|
+
return q_to_s(self.q, self.gain)
|
|
256
|
+
|
|
257
|
+
def __len__(self):
|
|
258
|
+
return self.count
|
|
259
|
+
|
|
260
|
+
def flatten(self):
|
|
261
|
+
"""
|
|
262
|
+
:return: an iterable of length count of this shelf where each shelf has count=1
|
|
263
|
+
"""
|
|
264
|
+
if self.count == 1:
|
|
265
|
+
return [self]
|
|
266
|
+
else:
|
|
267
|
+
return [
|
|
268
|
+
self.__class__(self.fs, self.freq, self.q, self.gain, 1)
|
|
269
|
+
] * self.count
|
|
270
|
+
|
|
271
|
+
def format_biquads(
|
|
272
|
+
self,
|
|
273
|
+
minidsp_style,
|
|
274
|
+
separator=",\n",
|
|
275
|
+
show_index=True,
|
|
276
|
+
to_hex=False,
|
|
277
|
+
fixed_point=False,
|
|
278
|
+
):
|
|
279
|
+
single = super().format_biquads(
|
|
280
|
+
minidsp_style,
|
|
281
|
+
separator=separator,
|
|
282
|
+
show_index=show_index,
|
|
283
|
+
to_hex=to_hex,
|
|
284
|
+
fixed_point=fixed_point,
|
|
285
|
+
)
|
|
286
|
+
if self.count == 1:
|
|
287
|
+
return single
|
|
288
|
+
elif self.count > 1:
|
|
289
|
+
return single * self.count
|
|
290
|
+
else:
|
|
291
|
+
raise ValueError("Shelf must have non zero count")
|
|
292
|
+
|
|
293
|
+
def get_sos(self):
|
|
294
|
+
return super().get_sos() * self.count
|
|
295
|
+
|
|
296
|
+
def to_json(self):
|
|
297
|
+
return {
|
|
298
|
+
"_type": self.__class__.__name__,
|
|
299
|
+
"fs": self.fs,
|
|
300
|
+
"fc": self.freq,
|
|
301
|
+
"q": self.q,
|
|
302
|
+
"gain": self.gain,
|
|
303
|
+
"count": self.count,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def description(self):
|
|
308
|
+
if self.count > 1:
|
|
309
|
+
return super().description + f" x{self.count}"
|
|
310
|
+
else:
|
|
311
|
+
return super().description
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class LowShelf(Shelf):
|
|
315
|
+
"""
|
|
316
|
+
lowShelf: H(s) = A * (s^2 + (sqrt(A)/Q)*s + A)/(A*s^2 + (sqrt(A)/Q)*s + 1)
|
|
317
|
+
|
|
318
|
+
b0 = A*( (A+1) - (A-1)*cos(w0) + 2*sqrt(A)*alpha )
|
|
319
|
+
b1 = 2*A*( (A-1) - (A+1)*cos(w0) )
|
|
320
|
+
b2 = A*( (A+1) - (A-1)*cos(w0) - 2*sqrt(A)*alpha )
|
|
321
|
+
a0 = (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha
|
|
322
|
+
a1 = -2*( (A-1) + (A+1)*cos(w0) )
|
|
323
|
+
a2 = (A+1) + (A-1)*cos(w0) - 2*sqrt(A)*alpha
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def __init__(self, fs, freq, q, gain, count=1, f_id=-1):
|
|
327
|
+
super().__init__(fs, freq, q, gain, count, f_id=f_id)
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def filter_type(self):
|
|
331
|
+
return "LS"
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def display_name(self):
|
|
335
|
+
return "Low Shelf"
|
|
336
|
+
|
|
337
|
+
def _compute_coeffs(self):
|
|
338
|
+
A = 10.0 ** (self.gain / 40.0)
|
|
339
|
+
a = np.array(
|
|
340
|
+
[
|
|
341
|
+
(A + 1) + ((A - 1) * self.cos_w0) + (2.0 * math.sqrt(A) * self.alpha),
|
|
342
|
+
-2.0 * ((A - 1) + ((A + 1) * self.cos_w0)),
|
|
343
|
+
(A + 1) + ((A - 1) * self.cos_w0) - (2.0 * math.sqrt(A) * self.alpha),
|
|
344
|
+
],
|
|
345
|
+
dtype=np.float64,
|
|
346
|
+
)
|
|
347
|
+
b = np.array(
|
|
348
|
+
[
|
|
349
|
+
A
|
|
350
|
+
* (
|
|
351
|
+
(A + 1)
|
|
352
|
+
- ((A - 1) * self.cos_w0)
|
|
353
|
+
+ (2.0 * math.sqrt(A) * self.alpha)
|
|
354
|
+
),
|
|
355
|
+
2.0 * A * ((A - 1) - ((A + 1) * self.cos_w0)),
|
|
356
|
+
A
|
|
357
|
+
* ((A + 1) - ((A - 1) * self.cos_w0) - (2 * math.sqrt(A) * self.alpha)),
|
|
358
|
+
],
|
|
359
|
+
dtype=np.float64,
|
|
360
|
+
)
|
|
361
|
+
return a / a[0], b / a[0]
|
|
362
|
+
|
|
363
|
+
def resample(self, new_fs):
|
|
364
|
+
"""
|
|
365
|
+
Creates a filter at the specified fs.
|
|
366
|
+
:param new_fs: the new fs.
|
|
367
|
+
:return: the new filter.
|
|
368
|
+
"""
|
|
369
|
+
return LowShelf(new_fs, self.freq, self.q, self.gain, self.count, f_id=self.id)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class HighShelf(Shelf):
|
|
373
|
+
"""
|
|
374
|
+
highShelf: H(s) = A * (A*s^2 + (sqrt(A)/Q)*s + 1)/(s^2 + (sqrt(A)/Q)*s + A)
|
|
375
|
+
|
|
376
|
+
b0 = A*( (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha )
|
|
377
|
+
b1 = -2*A*( (A-1) + (A+1)*cos(w0) )
|
|
378
|
+
b2 = A*( (A+1) + (A-1)*cos(w0) - 2*sqrt(A)*alpha )
|
|
379
|
+
a0 = (A+1) - (A-1)*cos(w0) + 2*sqrt(A)*alpha
|
|
380
|
+
a1 = 2*( (A-1) - (A+1)*cos(w0) )
|
|
381
|
+
a2 = (A+1) - (A-1)*cos(w0) - 2*sqrt(A)*alpha
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
def __init__(self, fs, freq, q, gain, count=1, f_id=-1):
|
|
386
|
+
super().__init__(fs, freq, q, gain, count, f_id=f_id)
|
|
387
|
+
|
|
388
|
+
def __eq__(self, o: object) -> bool:
|
|
389
|
+
return super().__eq__(o) and self.count == o.count
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def filter_type(self):
|
|
393
|
+
return "HS"
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def display_name(self):
|
|
397
|
+
return "High Shelf"
|
|
398
|
+
|
|
399
|
+
def _compute_coeffs(self):
|
|
400
|
+
A = self.A
|
|
401
|
+
cos_w0 = self.cos_w0
|
|
402
|
+
alpha = self.alpha
|
|
403
|
+
a = np.array(
|
|
404
|
+
[
|
|
405
|
+
(A + 1) - ((A - 1) * cos_w0) + (2.0 * math.sqrt(A) * alpha),
|
|
406
|
+
2.0 * ((A - 1) - ((A + 1) * cos_w0)),
|
|
407
|
+
(A + 1) - ((A - 1) * cos_w0) - (2.0 * math.sqrt(A) * alpha),
|
|
408
|
+
],
|
|
409
|
+
dtype=np.float64,
|
|
410
|
+
)
|
|
411
|
+
b = np.array(
|
|
412
|
+
[
|
|
413
|
+
A * ((A + 1) + ((A - 1) * cos_w0) + (2.0 * math.sqrt(A) * alpha)),
|
|
414
|
+
-2.0 * A * ((A - 1) + ((A + 1) * cos_w0)),
|
|
415
|
+
A * ((A + 1) + ((A - 1) * cos_w0) - (2.0 * math.sqrt(A) * alpha)),
|
|
416
|
+
],
|
|
417
|
+
dtype=np.float64,
|
|
418
|
+
)
|
|
419
|
+
return a / a[0], b / a[0]
|
|
420
|
+
|
|
421
|
+
def resample(self, new_fs):
|
|
422
|
+
"""
|
|
423
|
+
Creates a filter at the specified fs.
|
|
424
|
+
:param new_fs: the new fs.
|
|
425
|
+
:return: the new filter.
|
|
426
|
+
"""
|
|
427
|
+
return HighShelf(new_fs, self.freq, self.q, self.gain, self.count, f_id=self.id)
|
beq_common/provenance.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Distribution build identity, without importing any workflow package."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
|
|
10
|
+
BUILD_REVISION_FILE = "BUILD_REVISION"
|
|
11
|
+
PACKAGES = ("beq_common", "beqforge", "beqforge_device_check", "beqoptimiser")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def revision() -> str:
|
|
15
|
+
root = Path(__file__).resolve().parent.parent
|
|
16
|
+
if getattr(sys, "frozen", False):
|
|
17
|
+
try:
|
|
18
|
+
return (
|
|
19
|
+
Path(__file__).parent / BUILD_REVISION_FILE
|
|
20
|
+
).read_text().strip() + " (frozen build)"
|
|
21
|
+
except OSError:
|
|
22
|
+
return "unknown (frozen build without BUILD_REVISION)"
|
|
23
|
+
digest = hashlib.sha256()
|
|
24
|
+
try:
|
|
25
|
+
for package in PACKAGES:
|
|
26
|
+
for path in sorted((root / package).rglob("*.py")):
|
|
27
|
+
digest.update(path.relative_to(root).as_posix().encode())
|
|
28
|
+
digest.update(b"\0")
|
|
29
|
+
digest.update(hashlib.sha256(path.read_bytes()).digest())
|
|
30
|
+
except OSError:
|
|
31
|
+
return "unknown (sources unreadable)"
|
|
32
|
+
try:
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
["git", "describe", "--always", "--dirty", "--abbrev=12"],
|
|
35
|
+
cwd=root,
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=5,
|
|
39
|
+
check=False,
|
|
40
|
+
)
|
|
41
|
+
label = result.stdout.strip() or __version__
|
|
42
|
+
except (OSError, subprocess.SubprocessError):
|
|
43
|
+
label = __version__
|
|
44
|
+
return f"{label}+src:{digest.hexdigest()[:12]}"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Canonical published parameter precision, shared across workflows."""
|
|
2
|
+
|
|
3
|
+
from .types import BiquadSpec
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def publication_filters(filters: list[BiquadSpec]) -> list[BiquadSpec]:
|
|
7
|
+
"""Canonical text parameters: Hz to 2 decimals, dB to 3, Q to 4.
|
|
8
|
+
|
|
9
|
+
Independent of the device coefficient format and the hypothetical jitter diagnostic.
|
|
10
|
+
Both judging and external export must use these same parameters.
|
|
11
|
+
"""
|
|
12
|
+
return [
|
|
13
|
+
BiquadSpec(
|
|
14
|
+
s.type,
|
|
15
|
+
round(float(s.freq_hz), 2),
|
|
16
|
+
round(float(s.gain_db), 3),
|
|
17
|
+
round(float(s.q), 4),
|
|
18
|
+
)
|
|
19
|
+
for s in filters
|
|
20
|
+
]
|
beq_common/types.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Rate-independent published filter descriptions."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
BiquadType = Literal["peaking_eq", "low_shelf", "high_shelf"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class BiquadSpec:
|
|
12
|
+
"""One publishable biquad section — designer-interface.md v1.0 §3.
|
|
13
|
+
|
|
14
|
+
Deliberately carries no sample rate: `freq_hz`/`gain_db`/`q` fully determine an RBJ
|
|
15
|
+
section independent of the rate it is eventually realised at. `gain_db` follows the RBJ
|
|
16
|
+
convention `A = 10 ** (gain_db / 40)`, not `/20`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
type: BiquadType
|
|
20
|
+
freq_hz: float
|
|
21
|
+
gain_db: float
|
|
22
|
+
q: float
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
if self.freq_hz <= 0:
|
|
26
|
+
raise ValueError(f"freq_hz must be > 0, got {self.freq_hz}")
|
|
27
|
+
if self.q <= 0:
|
|
28
|
+
raise ValueError(f"q must be > 0, got {self.q}")
|
|
29
|
+
if not math.isfinite(self.gain_db):
|
|
30
|
+
raise ValueError(f"gain_db must be finite, got {self.gain_db}")
|