jevsnes.git / third-party / rust / jgenesis / scripts / kaiser-lpf.py

/// script dependencies = [ "matplotlib", "scipy", ] ///

8"""
9Generate an FIR low-pass filter designed for an oversampled signal using the windowing method with a Kaiser window, and
10plot the frequency response.
11"""
13import argparse
14from math import pi
15
16import matplotlib.pyplot as plt
17import numpy as np
18from scipy.signal import firwin, freqz, kaiser_beta
19
20
21def main():
22    args = parse_args()
23
24    beta = kaiser_beta(args.sba)
25    taps = args.n * (2 * args.nz + 1)
26    coefficients = firwin(
27        taps, (args.fc / (args.cutoff_denominator_freq / 2)) / args.n, window=("kaiser", beta)
28    )
29
30    print(f"{taps} taps")
31
32    with open(args.o, "w") as f:
33        coeffs_to_write = coefficients
34        if not args.write_full:
35            coeffs_to_write = coefficients[int(len(coefficients) / 2) :]
36        for coefficient in coeffs_to_write:
37            f.write(f"{coefficient},\n")
38
39    w, h = freqz(coefficients, worN=1 << 20)
40
41    figure = plt.figure()
42    axes = figure.add_subplot(
43        title="Filter Frequency Response",
44        xlabel="Frequency (Hz)",
45        ylabel="Gain (dB)",
46        xlim=(0, min(args.fs, 4 * args.fc)),
47        ylim=(-(args.sba + 20), 20),
48    )
49    axes.grid(visible=True)
50
51    axes.plot(w * args.n * (args.fs / 2) / pi, 20 * np.log10(abs(h)))
52    axes.plot(2 * [args.fs / 2], [-300, 300])
53    axes.plot([0, args.fs], 2 * [-3])
54
55    plt.show(block=True)
56
57
58def parse_args():
59    arg_parser = argparse.ArgumentParser(
60        description="Generate a FIR low-pass filter using a Kaiser window"
61    )
62    arg_parser.add_argument(
63        "-sba",
64        default=80,
65        type=float,
66        required=False,
67        help="Stopband attenuation (dB) (default=80)",
68    )
69    arg_parser.add_argument(
70        "-n",
71        default=512,
72        type=int,
73        required=False,
74        help="Samples per zero crossing (default=512)",
75    )
76    arg_parser.add_argument(
77        "-nz", type=int, required=True, help="Number of zero crossings"
78    )
79    arg_parser.add_argument(
80        "-fs",
81        default=48000,
82        type=float,
83        required=False,
84        help="Source frequency for plotting (Hz) (default=48000)",
85    )
86    arg_parser.add_argument(
87        "-fc",
88        default=20000,
89        type=float,
90        required=False,
91        help="Cutoff frequency (Hz) (default=20000)",
92    )
93    arg_parser.add_argument(
94        "--cutoff-denominator-freq",
95        default=48000,
96        type=float,
97        required=False,
98        help="Denominator when computing cutoff value (Hz) (default=48000)"
99    )
100    arg_parser.add_argument(
101        "-o",
102        default="kaiser-fir.txt",
103        type=str,
104        required=False,
105        help="Output file (default=kaiser-fir.txt)",
106    )
107    arg_parser.add_argument(
108        "--write-full",
109        action="store_true",
110        required=False,
111        help="Write out the entire filter instead of only the right half",
112    )
113    return arg_parser.parse_args()
114
115
116if __name__ == "__main__":
117    main()