PyOctaveBand 1.0.1__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.
@@ -0,0 +1,96 @@
1
+ # Copyright (c) 2020. Jose M. Requena-Plens
2
+ """
3
+ Octave-Band and Fractional Octave-Band filter for signals in the time domain.
4
+ Implementation according to ANSI s1.11-2004 and IEC 61260-1-2014.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import List, Tuple, cast
10
+
11
+ import matplotlib
12
+ import numpy as np
13
+
14
+ from .calibration import calculate_sensitivity
15
+ from .core import OctaveFilterBank
16
+ from .frequencies import getansifrequencies, normalizedfreq
17
+ from .parametric_filters import linkwitz_riley, time_weighting, weighting_filter
18
+
19
+ # Use non-interactive backend for plots
20
+ matplotlib.use("Agg")
21
+
22
+ # Public methods
23
+ __all__ = [
24
+ "octavefilter",
25
+ "getansifrequencies",
26
+ "normalizedfreq",
27
+ "OctaveFilterBank",
28
+ "weighting_filter",
29
+ "time_weighting",
30
+ "linkwitz_riley",
31
+ "calculate_sensitivity",
32
+ ]
33
+
34
+
35
+ def octavefilter(
36
+ x: List[float] | np.ndarray,
37
+ fs: int,
38
+ fraction: float = 1,
39
+ order: int = 6,
40
+ limits: List[float] | None = None,
41
+ show: bool = False,
42
+ sigbands: bool = False,
43
+ plot_file: str | None = None,
44
+ **kwargs: str | float | bool
45
+ ) -> Tuple[np.ndarray, List[float]] | Tuple[np.ndarray, List[float], List[np.ndarray]]:
46
+ """
47
+ Filter a signal with octave or fractional octave filter bank.
48
+
49
+ This method uses a filter bank with Second-Order Sections (SOS) coefficients.
50
+ To obtain the correct coefficients, automatic subsampling is applied to the
51
+ signal in each filtered band.
52
+
53
+ Multichannel support: If x is 2D (channels, samples), each channel is filtered.
54
+
55
+ :param x: Input signal (1D array or 2D array [channels, samples]).
56
+ :type x: Union[List[float], np.ndarray]
57
+ :param fs: Sample rate in Hz.
58
+ :type fs: int
59
+ :param fraction: Bandwidth 'b'. Examples: 1/3-octave b=3, 1-octave b=1, 2/3-octave b=1.5. Default: 1.
60
+ :type fraction: float
61
+ :param order: Order of the filter. Default: 6.
62
+ :type order: int
63
+ :param limits: Minimum and maximum limit frequencies [f_min, f_max]. Default [12, 20000].
64
+ :type limits: Optional[List[float]]
65
+ :param show: If True, plot and show the filter response.
66
+ :type show: bool
67
+ :param sigbands: If True, also return the signal in the time domain divided into bands.
68
+ :type sigbands: bool
69
+ :param plot_file: Path to save the filter response plot.
70
+ :type plot_file: Optional[str]
71
+ :param filter_type: (Optional) Type of filter ('butter', 'cheby1', 'cheby2', 'ellip', 'bessel'). Default: 'butter'.
72
+ :param ripple: (Optional) Passband ripple in dB (for cheby1, ellip). Default: 0.1.
73
+ :param attenuation: (Optional) Stopband attenuation in dB (for cheby2, ellip). Default: 60.0.
74
+ :param calibration_factor: (Optional) Sensitivity multiplier. Default: 1.0.
75
+ :param dbfs: (Optional) If True, return results in dBFS. Default: False.
76
+ :param mode: (Optional) 'rms' or 'peak'. Default: 'rms'.
77
+ :return: A tuple containing (SPL_array, Frequencies_list) or (SPL_array, Frequencies_list, signals).
78
+ :rtype: Union[Tuple[np.ndarray, List[float]], Tuple[np.ndarray, List[float], List[np.ndarray]]]
79
+ """
80
+
81
+ # Use the class-based implementation
82
+ filter_bank = OctaveFilterBank(
83
+ fs=fs,
84
+ fraction=fraction,
85
+ order=order,
86
+ limits=limits,
87
+ filter_type=cast(str, kwargs.get("filter_type", "butter")),
88
+ ripple=cast(float, kwargs.get("ripple", 0.1)),
89
+ attenuation=cast(float, kwargs.get("attenuation", 60.0)),
90
+ show=show,
91
+ plot_file=plot_file,
92
+ calibration_factor=cast(float, kwargs.get("calibration_factor", 1.0)),
93
+ dbfs=cast(bool, kwargs.get("dbfs", False))
94
+ )
95
+
96
+ return filter_bank.filter(x, sigbands=sigbands, mode=cast(str, kwargs.get("mode", "rms")))
@@ -0,0 +1,32 @@
1
+ # Copyright (c) 2026. Jose M. Requena-Plens
2
+ """
3
+ Calibration utilities for mapping digital signals to physical SPL levels.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import List
9
+
10
+ import numpy as np
11
+
12
+
13
+ def calculate_sensitivity(
14
+ ref_signal: List[float] | np.ndarray,
15
+ target_spl: float = 94.0,
16
+ ref_pressure: float = 2e-5
17
+ ) -> float:
18
+ """
19
+ Calculate the calibration factor (multiplier) to convert digital units
20
+ to Pascals based on a reference recording (e.g., 1kHz @ 94dB).
21
+
22
+ :param ref_signal: Recording of the calibration tone.
23
+ :param target_spl: The known SPL level of the calibrator (default 94 dB).
24
+ :param ref_pressure: Reference pressure (default 20 microPascals).
25
+ :return: Calibration factor (sensitivity multiplier).
26
+ """
27
+ rms_ref = np.std(ref_signal)
28
+ if rms_ref == 0:
29
+ raise ValueError("Reference signal is silent, cannot calibrate.")
30
+
31
+ factor = (ref_pressure * 10**(target_spl / 20)) / rms_ref
32
+ return float(factor)
pyoctaveband/core.py ADDED
@@ -0,0 +1,191 @@
1
+ # Copyright (c) 2026. Jose M. Requena-Plens
2
+ """
3
+ Core processing logic and FilterBank class for pyoctaveband.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import List, Tuple, cast
9
+
10
+ import numpy as np
11
+ from scipy import signal
12
+
13
+ from .filter_design import _design_sos_filter
14
+ from .frequencies import _genfreqs
15
+ from .utils import _downsamplingfactor, _resample_to_length, _typesignal
16
+
17
+
18
+ class OctaveFilterBank:
19
+ """
20
+ A class-based representation of an Octave Filter Bank.
21
+ Allows for pre-calculating and reusing filter coefficients.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ fs: int,
27
+ fraction: float = 1,
28
+ order: int = 6,
29
+ limits: List[float] | None = None,
30
+ filter_type: str = "butter",
31
+ ripple: float = 0.1,
32
+ attenuation: float = 60.0,
33
+ show: bool = False,
34
+ plot_file: str | None = None,
35
+ calibration_factor: float = 1.0,
36
+ dbfs: bool = False,
37
+ ) -> None:
38
+ """
39
+ Initialize the Octave Filter Bank.
40
+
41
+ :param fs: Sample rate in Hz.
42
+ :param fraction: Bandwidth fraction (e.g., 1 for octave, 3 for 1/3 octave).
43
+ :param order: Filter order.
44
+ :param limits: Frequency limits [f_min, f_max].
45
+ :param filter_type: Type of filter ('butter', 'cheby1', 'cheby2', 'ellip', 'bessel').
46
+ :param ripple: Passband ripple in dB.
47
+ :param attenuation: Stopband attenuation in dB.
48
+ :param show: If True, show the filter response plot.
49
+ :param plot_file: Path to save the filter response plot.
50
+ :param calibration_factor: Calibration factor for SPL calculation.
51
+ :param dbfs: If True, calculate SPL in dBFS.
52
+ """
53
+ if fs <= 0:
54
+ raise ValueError("Sample rate 'fs' must be positive.")
55
+ if fraction <= 0:
56
+ raise ValueError("Bandwidth 'fraction' must be positive.")
57
+ if order <= 0:
58
+ raise ValueError("Filter 'order' must be positive.")
59
+ if limits is None:
60
+ limits = [12, 20000]
61
+ if len(limits) != 2:
62
+ raise ValueError("Limits must be a list of two frequencies [f_min, f_max].")
63
+ if limits[0] <= 0 or limits[1] <= 0:
64
+ raise ValueError("Limit frequencies must be positive.")
65
+ if limits[0] >= limits[1]:
66
+ raise ValueError("The lower limit must be less than the upper limit.")
67
+
68
+ valid_filters = ["butter", "cheby1", "cheby2", "ellip", "bessel"]
69
+ if filter_type not in valid_filters:
70
+ raise ValueError(f"Invalid filter_type. Must be one of {valid_filters}")
71
+
72
+ self.fs = fs
73
+ self.fraction = fraction
74
+ self.order = order
75
+ self.limits = limits
76
+ self.filter_type = filter_type
77
+ self.ripple = ripple
78
+ self.attenuation = attenuation
79
+ self.calibration_factor = calibration_factor
80
+ self.dbfs = dbfs
81
+
82
+ # Generate frequencies
83
+ self.freq, self.freq_d, self.freq_u = _genfreqs(limits, fraction, fs)
84
+ self.num_bands = len(self.freq)
85
+
86
+ # Calculate factors and design SOS
87
+ self.factor = _downsamplingfactor(self.freq_u, fs)
88
+ self.sos = _design_sos_filter(
89
+ self.freq, self.freq_d, self.freq_u, fs, order, self.factor,
90
+ filter_type, ripple, attenuation, show, plot_file
91
+ )
92
+
93
+ def filter(
94
+ self,
95
+ x: List[float] | np.ndarray,
96
+ sigbands: bool = False,
97
+ mode: str = "rms"
98
+ ) -> Tuple[np.ndarray, List[float]] | Tuple[np.ndarray, List[float], List[np.ndarray]]:
99
+ """
100
+ Apply the pre-designed filter bank to a signal.
101
+
102
+ :param x: Input signal (1D array or 2D array [channels, samples]).
103
+ :param sigbands: If True, also return the signal in the time domain divided into bands.
104
+ :param mode: 'rms' for energy-based level, 'peak' for peak-holding level.
105
+ :return: A tuple containing (SPL_array, Frequencies_list) or (SPL_array, Frequencies_list, signals).
106
+ """
107
+
108
+ # Convert input to numpy array
109
+ x_proc = _typesignal(x)
110
+
111
+ # Handle multichannel detection
112
+ is_multichannel = x_proc.ndim > 1
113
+ if not is_multichannel:
114
+ x_proc = x_proc[np.newaxis, :] # Standardize to 2D
115
+
116
+ num_channels = x_proc.shape[0]
117
+
118
+ # Process signal across all bands and channels
119
+ spl, xb = self._process_bands(x_proc, num_channels, sigbands, mode=mode)
120
+
121
+ # Format output based on input dimensionality
122
+ if not is_multichannel:
123
+ spl = spl[0]
124
+ if sigbands and xb is not None:
125
+ xb = [band[0] for band in xb]
126
+
127
+ if sigbands and xb is not None:
128
+ return spl, self.freq, xb
129
+ else:
130
+ return spl, self.freq
131
+
132
+ def _process_bands(
133
+ self,
134
+ x_proc: np.ndarray,
135
+ num_channels: int,
136
+ sigbands: bool,
137
+ mode: str = "rms"
138
+ ) -> Tuple[np.ndarray, List[np.ndarray] | None]:
139
+ """
140
+ Process signal through each frequency band.
141
+
142
+ :param x_proc: Standardized 2D input signal [channels, samples].
143
+ :param num_channels: Number of channels.
144
+ :param sigbands: If True, return filtered bands.
145
+ :param mode: 'rms' or 'peak'.
146
+ :return: A tuple containing (SPL_array, Optional_List_of_filtered_signals).
147
+ """
148
+ spl = np.zeros([num_channels, self.num_bands])
149
+ xb: List[np.ndarray] | None = [np.array([]) for _ in range(self.num_bands)] if sigbands else None
150
+
151
+ for idx in range(self.num_bands):
152
+ for ch in range(num_channels):
153
+ # Core DSP logic extracted to reduce complexity
154
+ filtered_signal = self._filter_and_resample(x_proc[ch], idx)
155
+
156
+ # Sound Level Calculation
157
+ spl[ch, idx] = self._calculate_level(filtered_signal, mode)
158
+
159
+ if sigbands and xb is not None:
160
+ # Restore original length
161
+ y_resampled = _resample_to_length(filtered_signal, int(self.factor[idx]), x_proc.shape[1])
162
+ if ch == 0:
163
+ xb[idx] = np.zeros([num_channels, x_proc.shape[1]])
164
+ xb[idx][ch] = y_resampled
165
+ return spl, xb
166
+
167
+ def _filter_and_resample(self, x_ch: np.ndarray, idx: int) -> np.ndarray:
168
+ """Resample and filter a single channel for a specific band."""
169
+ if self.factor[idx] > 1:
170
+ sd = signal.resample_poly(x_ch, 1, self.factor[idx])
171
+ else:
172
+ sd = x_ch
173
+
174
+ return cast(np.ndarray, signal.sosfilt(self.sos[idx], sd))
175
+
176
+ def _calculate_level(self, y: np.ndarray, mode: str) -> float:
177
+ """Calculate the level (RMS or Peak) in dB."""
178
+ if mode.lower() == "rms":
179
+ val_linear = np.std(y)
180
+ elif mode.lower() == "peak":
181
+ val_linear = np.max(np.abs(y))
182
+ else:
183
+ raise ValueError("Invalid mode. Use 'rms' or 'peak'.")
184
+
185
+ if self.dbfs:
186
+ # dBFS: 0 dB is RMS = 1.0 or Peak = 1.0
187
+ return float(20 * np.log10(np.max([val_linear, np.finfo(float).eps])))
188
+
189
+ # Physical SPL: apply sensitivity and use 20uPa reference
190
+ pressure_pa = val_linear * self.calibration_factor
191
+ return float(20 * np.log10(np.max([pressure_pa, np.finfo(float).eps]) / 2e-5))
@@ -0,0 +1,118 @@
1
+ # Copyright (c) 2026. Jose M. Requena-Plens
2
+ """
3
+ Filter design and visualization for pyoctaveband.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import List
9
+
10
+ import matplotlib.pyplot as plt
11
+ import numpy as np
12
+ from scipy import signal
13
+
14
+
15
+ def _design_sos_filter(
16
+ freq: List[float],
17
+ freq_d: List[float],
18
+ freq_u: List[float],
19
+ fs: int,
20
+ order: int,
21
+ factor: np.ndarray,
22
+ filter_type: str,
23
+ ripple: float,
24
+ attenuation: float,
25
+ show: bool = False,
26
+ plot_file: str | None = None,
27
+ ) -> List[np.ndarray]:
28
+ """
29
+ Generate SOS coefficients for the filter bank.
30
+
31
+ :param freq: Center frequencies.
32
+ :param freq_d: Lower edge frequencies.
33
+ :param freq_u: Upper edge frequencies.
34
+ :param fs: Original sample rate.
35
+ :param order: Filter order.
36
+ :param factor: Downsampling factors per band.
37
+ :param filter_type: Type of filter.
38
+ :param ripple: Passband ripple (dB).
39
+ :param attenuation: Stopband attenuation (dB).
40
+ :param show: If True, plot response.
41
+ :param plot_file: Path to save plot.
42
+ :return: List of SOS coefficient arrays.
43
+ """
44
+ sos = [np.array([]) for _ in range(len(freq))]
45
+
46
+ for idx, (lower, upper) in enumerate(zip(freq_d, freq_u)):
47
+ fsd = fs / factor[idx]
48
+ wn = np.array([lower, upper]) / (fsd / 2)
49
+
50
+ if filter_type == "butter":
51
+ sos[idx] = signal.butter(N=order, Wn=wn, btype="bandpass", output="sos")
52
+ elif filter_type == "cheby1":
53
+ sos[idx] = signal.cheby1(N=order, rp=ripple, Wn=wn, btype="bandpass", output="sos")
54
+ elif filter_type == "cheby2":
55
+ sos[idx] = signal.cheby2(N=order, rs=attenuation, Wn=wn, btype="bandpass", output="sos")
56
+ elif filter_type == "ellip":
57
+ sos[idx] = signal.ellip(N=order, rp=ripple, rs=attenuation, Wn=wn, btype="bandpass", output="sos")
58
+ elif filter_type == "bessel":
59
+ sos[idx] = signal.bessel(N=order, Wn=wn, btype="bandpass", norm="phase", output="sos")
60
+
61
+ if show or plot_file:
62
+ _showfilter(sos, freq, freq_u, freq_d, fs, factor, show, plot_file)
63
+
64
+ return sos
65
+
66
+ def _showfilter(
67
+ sos: List[np.ndarray],
68
+ freq: List[float],
69
+ freq_u: List[float],
70
+ freq_d: List[float],
71
+ fs: int,
72
+ factor: np.ndarray,
73
+ show: bool = False,
74
+ plot_file: str | None = None,
75
+ ) -> None:
76
+ """
77
+ Visualize filter bank frequency response.
78
+
79
+ :param sos: List of SOS coefficients.
80
+ :param freq: Center frequencies.
81
+ :param freq_u: Upper edges.
82
+ :param freq_d: Lower edges.
83
+ :param fs: Original sample rate.
84
+ :param factor: Downsampling factors.
85
+ :param show: If True, show the plot.
86
+ :param plot_file: Path to save the plot.
87
+ """
88
+ wn = 8192
89
+ w = np.zeros([wn, len(freq)])
90
+ h: np.ndarray = np.zeros([wn, len(freq)], dtype=np.complex128)
91
+
92
+ for idx in range(len(freq)):
93
+ fsd = fs / factor[idx]
94
+ w[:, idx], h[:, idx] = signal.sosfreqz(sos[idx], worN=wn, whole=False, fs=fsd)
95
+
96
+ fig, ax = plt.subplots(figsize=(10, 6))
97
+ ax.semilogx(w, 20 * np.log10(abs(h) + np.finfo(float).eps), color="#1f77b4", linewidth=1.2)
98
+ ax.axhline(-3, color="#d62728", linestyle="--", alpha=0.5, linewidth=1, label="-3 dB")
99
+
100
+ ax.set_title("Filter Bank Frequency Response", fontweight="bold", pad=15)
101
+ ax.set_xlabel("Frequency [Hz]")
102
+ ax.set_ylabel("Amplitude [dB]")
103
+ ax.grid(which="major", color="#e0e0e0", linestyle="-")
104
+ ax.grid(which="minor", color="#e0e0e0", linestyle=":", alpha=0.4)
105
+
106
+ plt.xlim(freq_d[0] * 0.8, freq_u[-1] * 1.2)
107
+ plt.ylim(-4, 1)
108
+
109
+ xticks = [16, 31.5, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
110
+ xticklabels = ["16", "31.5", "63", "125", "250", "500", "1k", "2k", "4k", "8k", "16k"]
111
+ ax.set_xticks(xticks)
112
+ ax.set_xticklabels(xticklabels)
113
+
114
+ if plot_file:
115
+ plt.savefig(plot_file, dpi=150, bbox_inches="tight")
116
+ if show:
117
+ plt.show()
118
+ plt.close(fig)
@@ -0,0 +1,142 @@
1
+ # Copyright (c) 2026. Jose M. Requena-Plens
2
+ """
3
+ Frequency calculation logic according to ANSI/IEC standards.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import warnings
9
+ from typing import List, Tuple
10
+
11
+ import numpy as np
12
+
13
+
14
+ def getansifrequencies(
15
+ fraction: float,
16
+ limits: List[float] | None = None,
17
+ ) -> Tuple[List[float], List[float], List[float]]:
18
+ """
19
+ Calculate frequencies according to ANSI/IEC standards.
20
+
21
+ :param fraction: Bandwidth fraction (e.g., 1, 3).
22
+ :param limits: [f_min, f_max] limits.
23
+ :return: Tuple of (center_freqs, lower_edges, upper_edges).
24
+ """
25
+ if limits is None:
26
+ limits = [12, 20000]
27
+
28
+ g = 10 ** (3 / 10)
29
+ fr = 1000
30
+
31
+ x = _initindex(limits[0], fr, g, fraction)
32
+ freq = np.array([_ratio(g, x, fraction) * fr])
33
+
34
+ freq_x = freq[0]
35
+ while freq_x * _bandedge(g, fraction) < limits[1]:
36
+ x += 1
37
+ freq_x = _ratio(g, x, fraction) * fr
38
+ freq = np.append(freq, freq_x)
39
+
40
+ freq_d = freq / _bandedge(g, fraction)
41
+ freq_u = freq * _bandedge(g, fraction)
42
+
43
+ return freq.tolist(), freq_d.tolist(), freq_u.tolist()
44
+
45
+
46
+ def _initindex(f: float, fr: float, g: float, b: float) -> int:
47
+ """
48
+ Calculate starting index for band generation.
49
+
50
+ :param f: Frequency.
51
+ :param fr: Reference frequency.
52
+ :param g: Base ratio.
53
+ :param b: Bandwidth fraction.
54
+ :return: Index integer.
55
+ """
56
+ if round(b) % 2:
57
+ return int(np.round((b * np.log(f / fr) + 30 * np.log(g)) / np.log(g)))
58
+ return int(np.round((2 * b * np.log(f / fr) + 59 * np.log(g)) / (2 * np.log(g))))
59
+
60
+
61
+ def _ratio(g: float, x: int, b: float) -> float:
62
+ """
63
+ Calculate ratio for center frequency.
64
+
65
+ :param g: Base ratio.
66
+ :param x: Index.
67
+ :param b: Bandwidth fraction.
68
+ :return: Frequency ratio.
69
+ """
70
+ if round(b) % 2:
71
+ return float(g ** ((x - 30) / b))
72
+ return float(g ** ((2 * x - 59) / (2 * b)))
73
+
74
+
75
+ def _bandedge(g: float, b: float) -> float:
76
+ """
77
+ Calculate band-edge ratio.
78
+
79
+ :param g: Base ratio.
80
+ :param b: Bandwidth fraction.
81
+ :return: Edge ratio.
82
+ """
83
+ return float(g ** (1 / (2 * b)))
84
+
85
+
86
+ def _deleteouters(
87
+ freq: List[float], freq_d: List[float], freq_u: List[float], fs: int
88
+ ) -> Tuple[List[float], List[float], List[float]]:
89
+ """
90
+ Remove bands exceeding the Nyquist frequency.
91
+
92
+ :param freq: Center frequencies.
93
+ :param freq_d: Lower edges.
94
+ :param freq_u: Upper edges.
95
+ :param fs: Sample rate.
96
+ :return: Filtered (center, lower, upper) frequencies.
97
+ """
98
+ freq_arr = np.array(freq)
99
+ freq_d_arr = np.array(freq_d)
100
+ freq_u_arr = np.array(freq_u)
101
+
102
+ idx = np.nonzero(freq_u_arr > fs / 2)[0]
103
+ if len(idx) > 0:
104
+ warnings.warn("Low sampling rate: frequencies above fs/2 removed", stacklevel=3)
105
+ freq_arr = np.delete(freq_arr, idx)
106
+ freq_d_arr = np.delete(freq_d_arr, idx)
107
+ freq_u_arr = np.delete(freq_u_arr, idx)
108
+
109
+ return freq_arr.tolist(), freq_d_arr.tolist(), freq_u_arr.tolist()
110
+
111
+
112
+ def _genfreqs(limits: List[float], fraction: float, fs: int) -> Tuple[List[float], List[float], List[float]]:
113
+ """
114
+ Determine band frequencies within limits.
115
+
116
+ :param limits: [f_min, f_max].
117
+ :param fraction: Bandwidth fraction.
118
+ :param fs: Sample rate.
119
+ :return: Tuple of center, lower, and upper frequencies.
120
+ """
121
+ freq, freq_d, freq_u = getansifrequencies(fraction, limits)
122
+ return _deleteouters(freq, freq_d, freq_u, fs)
123
+
124
+
125
+ def normalizedfreq(fraction: int) -> List[float]:
126
+ """
127
+ Get standardized IEC center frequencies.
128
+
129
+ :param fraction: 1 or 3 (Octave or 1/3 Octave).
130
+ :return: List of standard frequencies.
131
+ """
132
+ predefined = {
133
+ 1: [16, 31.5, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000],
134
+ 3: [
135
+ 12.5, 16, 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500,
136
+ 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000,
137
+ 12500, 16000, 20000,
138
+ ],
139
+ }
140
+ if fraction not in predefined:
141
+ raise ValueError("Normalized frequencies only available for fraction=1 or 3")
142
+ return predefined[fraction]