9
\$\begingroup\$
import os, errnoimport pyaudiofrom scipy.signal import lfilterimport numpyfrom tkinter import *from tkinter.ttk import *from tk_tools import *from tkinter import messageboxroot=Tk()root.title('Decibel Meter')root.grid()gaugedb = RotaryScale(root, max_value=120.0, unit=' dBA')gaugedb.grid(column=1, row=1)led = Led(root, size=50)led.grid(column=3, row=1)led.to_red(on=False)Label(root, text='Too Loud').grid(column=3, row=0)Label(root, text='Max').grid(column=2, row=0)Label(root, text='Calibration (dB)').grid(column=4, row=0)maxdb_display=SevenSegmentDigits(root, digits=3, digit_color='#00ff00', background='black')maxdb_display.grid(column=2, row=1)CHUNKS = [4096, 9600]CHUNK = CHUNKS[1]FORMAT = pyaudio.paInt16CHANNEL = 1 RATES = [44300, 48000]RATE = RATES[1]offset=StringVar()offset.set('0')spinbox=Spinbox(root, from_=-20, to=20, textvariable=offset, state='readonly')spinbox.grid(column=4, row=1)appclosed=Falsefrom scipy.signal import bilineardef close(): global appclosed root.destroy() appclosed=True stream.stop_stream() stream.close() pa.terminate()def A_weighting(fs): f1 = 20.598997 f2 = 107.65265 f3 = 737.86223 f4 = 12194.217 A1000 = 1.9997 NUMs = [(2*numpy.pi * f4)**2 * (10**(A1000/20)), 0, 0, 0, 0] DENs = numpy.polymul([1, 4*numpy.pi * f4, (2*numpy.pi * f4)**2],                [1, 4*numpy.pi * f1, (2*numpy.pi * f1)**2]) DENs = numpy.polymul(numpy.polymul(DENs, [1, 2*numpy.pi * f3]),                              [1, 2*numpy.pi * f2]) return bilinear(NUMs, DENs, fs)NUMERATOR, DENOMINATOR = A_weighting(RATE)def rms_flat(a): return numpy.sqrt(numpy.mean(numpy.absolute(a)**2))pa = pyaudio.PyAudio()stream = pa.open(format = FORMAT,             channels = CHANNEL,             rate = RATE,             input = True,             frames_per_buffer = CHUNK)def update_max_if_new_is_larger_than_max(new, max): if new > max:     return new else:     return maxdef listen(old=0, error_count=0, min_decibel=100, max_decibel=0): global appclosed while True:     try:         try:             block = stream.read(CHUNK)         except IOError as e:             if not appclosed:                 error_count += 1                 messagebox.showerror("Error, ", " (%d) Error recording: %s" % (error_count, e))         else:             decoded_block = numpy.fromstring(block, numpy.int16)             y = lfilter(NUMERATOR, DENOMINATOR, decoded_block)             new_decibel = 20*numpy.log10(rms_flat(y))+int(offset.get())             old = new_decibel             gaugedb.set_value(float('{:.2f}'.format(new_decibel)))             max_decibel = update_max_if_new_is_larger_than_max(new_decibel, max_decibel)             maxdb_display.set_value(str(int(float(str(max_decibel)))))             if new_decibel>85:                 led.to_red(on=True)             else:                 led.to_red(on=False)         root.update()     except TclError:         breakroot.protocol('WM_DELETE_WINDOW', close)listen()

Is this an accurate dBA meter? My code works fine, but I want to check if itactually mirrors the ambient sound level from the microphone.

200_success's user avatar
200_success
146k22 gold badges191 silver badges481 bronze badges
askedSep 25, 2022 at 19:01
ijknm's user avatar
\$\endgroup\$
5
  • 10
    \$\begingroup\$CodeReview can review your code, but to your main question of whether the psychoacoustics are accurate I'm sure something likephysics, or maybesignal processing would be better-suited. Read their respective/help/on-topic guides first.\$\endgroup\$CommentedSep 25, 2022 at 21:18
  • \$\begingroup\$See loosely related questions likedsp.stackexchange.com/questions/36077/…\$\endgroup\$CommentedSep 25, 2022 at 22:40
  • 6
    \$\begingroup\$You should double check that sampling rate of 44300, that looks wrong (or at least non-standard). I’d expect 44100 in its place.\$\endgroup\$CommentedSep 26, 2022 at 9:52
  • 2
    \$\begingroup\$Define 'accurate'. The only way to know whether a given instsrument meets a given degree of accuracy is to measure its error with a better instrument. Aside from finding problems which make it definitelynot accurate, you are only going to prove that this is accurate if you can get to a real lab and measure real data.\$\endgroup\$CommentedSep 27, 2022 at 12:19
  • \$\begingroup\$(and to answer the chicken-and-egg problem raised: instruments are designed with others, and that, plus some theory and a lot of metaphysics, lets you ratchet your accuracy up by refining your notion of what you're measuring. See Hasok Changinventing temperature for an entertaining discussion of how this works in practice)\$\endgroup\$CommentedSep 27, 2022 at 12:20

1 Answer1

31
\$\begingroup\$

Indent your code with a PEP8-compliant IDE or linter; it's a perfect mess right now.

Move your global code into functions and maybe classes. There are two good use cases for classes here - one for a GUI and one for an audio processor.

offset must not be aStringVar, but instead anIntVar - among other reasons this will obviate the cast inint(offset.get()). Do not leave it nameless and do not leave it orphaned; its parent needs to be the root object.

Move your import ofbilinear up to join your other imports.

Your imports should avoidimport *; that makes a vast swamp out of the global namespace and it doesn't need to be like that. Traditionallynumpy is aliased tonp.

Consider writing a context manager to close off your audio stream.

numpy.absolute(a)**2 is justa**2, right?

Deleteupdate_max_if_new_is_larger_than_max. This is just a call to the built-inmax().

Rather than

         if new_decibel>85:             led.to_red(on=True)         else:             led.to_red(on=False)

just move the boolean expression to the argument of a single call and delete theif.

Add PEP484 typehints.

Convert your lists inA_weighting into immutable tuples.

Listen to the warnings being told to you: your use ofnp.fromstring needs to be replaced withnp.frombuffer.

str(int(float(str(max_decibel)))) is just... majestic. Use a formatting string instead.

As @Seb comments, 44300 should almost certainly be 44100.

polymul isdeprecated. UsePolynomial instead.

An equivalent torms_flat is the more integrated and maybe faster

np.linalg.norm(a) / np.sqrt(len(a))

which, based onthelinalg source, further reduces to a self-dot-product:

np.sqrt(a.dot(a) / len(a))

Suggested

import tkinter as tkimport numpy as npimport pyaudioimport tk_toolsfrom numpy.polynomial import Polynomialfrom scipy.signal import bilinear, lfilterCHUNKS = [4096, 9600]CHUNK = CHUNKS[1]FORMAT = pyaudio.paInt16CHANNEL = 1RATES = [44100, 48000]RATE = RATES[1]def A_weighting(fs: float) -> tuple[np.ndarray, np.ndarray]:    f1 = 20.598997    f2 = 107.65265    f3 = 737.86223    f4 = 12194.217    a1000 = 1.9997    nums = Polynomial(((2*np.pi * f4)**2 * 10**(a1000 / 20), 0,0,0,0))    dens = (        Polynomial((1, 4*np.pi * f4, (2*np.pi * f4)**2)) *        Polynomial((1, 4*np.pi * f1, (2*np.pi * f1)**2)) *        Polynomial((1, 2*np.pi * f3)) *        Polynomial((1, 2*np.pi * f2))    )    return bilinear(nums.coef, dens.coef, fs)def rms_flat(a: np.ndarray) -> float:    return np.sqrt(a.dot(a) / len(a))class Meter:    def __init__(self) -> None:        self.pa = pyaudio.PyAudio()        self.stream = self.pa.open(            format=FORMAT,            channels=CHANNEL,            rate=RATE,            input=True,            frames_per_buffer=CHUNK,        )        self.numerator, self.denominator = A_weighting(RATE)        self.max_decibel = 0    def __enter__(self) -> 'Meter':        return self    def __exit__(self, exc_type, exc_val, exc_tb) -> None:        self.stream.stop_stream()        self.stream.close()        self.pa.terminate()    def listen(self, offset: int) -> float:        block = self.stream.read(CHUNK)        decoded_block = np.frombuffer(block, dtype=np.int16)        y = lfilter(self.numerator, self.denominator, decoded_block)        new_decibel = 20*np.log10(rms_flat(y)) + offset        self.max_decibel = max(self.max_decibel, new_decibel)        return new_decibelclass GUI:    def __init__(self, meter: Meter) -> None:        self.meter = meter        self.root = root = tk.Tk()        root.title('Decibel Meter')        root.grid()        root.protocol('WM_DELETE_WINDOW', self.close)        self.app_closed = False        self.gaugedb = tk_tools.RotaryScale(root, max_value=120, unit=' dBA')        self.gaugedb.grid(column=1, row=1)        self.led = tk_tools.Led(root, size=50)        self.led.grid(column=3, row=1)        self.led.to_red(on=False)        tk.Label(root, text='Too Loud').grid(column=3, row=0)        tk.Label(root, text='Max').grid(column=2, row=0)        tk.Label(root, text='Calibration (dB)').grid(column=4, row=0)        self.maxdb_display = tk_tools.SevenSegmentDigits(root, digits=3, digit_color='#00ff00', background='black')        self.maxdb_display.grid(column=2, row=1)        self.offset = tk.IntVar(root, value=0, name='offset')        spinbox = tk.Spinbox(root, from_=-20, to=20, textvariable=self.offset, state='readonly')        spinbox.grid(column=4, row=1)    def close(self) -> None:        self.app_closed = True    def run(self) -> None:        while not self.app_closed:            new_decibel = self.meter.listen(self.offset.get())            self.update(new_decibel, self.meter.max_decibel)            self.root.update()    def update(self, new_decibel: float, max_decibel: float) -> None:        self.gaugedb.set_value(np.around(new_decibel, 1))        self.maxdb_display.set_value(f'{max_decibel:.1f}')        self.led.to_red(on=new_decibel > 85)def main() -> None:    with Meter() as meter:        gui = GUI(meter)        gui.run()if __name__ == '__main__':    main()

Output

screenshot of meter

Layout

Your layout needs a little love. Since the gauge text is at the bottom, why not put all labels at the bottom? Add some padding for legibility's sake, and add some resize sanity. Unfortunately, in addition to missing variable support,tk_tools widgets seem to have a broken layout behaviour because they ignoresticky resize requests; but oh well:

import tkinter as tkimport numpy as npimport pyaudioimport tk_toolsfrom numpy.polynomial import Polynomialfrom scipy.signal import bilinear, lfilterCHUNKS = [4096, 9600]CHUNK = CHUNKS[1]FORMAT = pyaudio.paInt16CHANNEL = 1RATES = [44100, 48000]RATE = RATES[1]def A_weighting(fs: float) -> tuple[np.ndarray, np.ndarray]:    f1 = 20.598997    f2 = 107.65265    f3 = 737.86223    f4 = 12194.217    a1000 = 1.9997    nums = Polynomial(((2*np.pi * f4)**2 * 10**(a1000 / 20), 0,0,0,0))    dens = (        Polynomial((1, 4*np.pi * f4, (2*np.pi * f4)**2)) *        Polynomial((1, 4*np.pi * f1, (2*np.pi * f1)**2)) *        Polynomial((1, 2*np.pi * f3)) *        Polynomial((1, 2*np.pi * f2))    )    return bilinear(nums.coef, dens.coef, fs)def rms_flat(a: np.ndarray) -> float:    return np.sqrt(a.dot(a) / len(a))class Meter:    def __init__(self) -> None:        self.pa = pyaudio.PyAudio()        self.stream = self.pa.open(            format=FORMAT,            channels=CHANNEL,            rate=RATE,            input=True,            frames_per_buffer=CHUNK,        )        self.numerator, self.denominator = A_weighting(RATE)        self.max_decibel = 0    def __enter__(self) -> 'Meter':        return self    def __exit__(self, exc_type, exc_val, exc_tb) -> None:        self.stream.stop_stream()        self.stream.close()        self.pa.terminate()    def listen(self, offset: int) -> float:        block = self.stream.read(CHUNK)        decoded_block = np.frombuffer(block, dtype=np.int16)        y = lfilter(self.numerator, self.denominator, decoded_block)        new_decibel = 20*np.log10(rms_flat(y)) + offset        self.max_decibel = max(self.max_decibel, new_decibel)        return new_decibelclass GUI:    def __init__(self, meter: Meter) -> None:        self.meter = meter        self.root = root = tk.Tk()        root.title('Decibel Meter')        root.grid()        root.grid_rowconfigure(index=0, weight=1)        root.grid_rowconfigure(index=1, weight=1)        root.grid_columnconfigure(index=0, weight=1)        root.grid_columnconfigure(index=3, weight=1)        root.protocol('WM_DELETE_WINDOW', self.close)        self.app_closed = False        self.gaugedb = tk_tools.RotaryScale(root, max_value=120, unit=' dBA')        # This control does not respect resizing via tk.NSEW.        self.gaugedb.grid(row=0, column=0, rowspan=2, sticky=tk.E)        self.maxdb_display = tk_tools.SevenSegmentDigits(root, digits=3, digit_color='#00ff00', background='black')        self.maxdb_display.grid(row=0, column=1, sticky=tk.S, padx=5)        tk.Label(root, text='Max').grid(row=1, column=1, sticky=tk.N, padx=5)        self.led = tk_tools.Led(root, size=50)        self.led.to_red(on=False)        self.led.grid(row=0, column=2, sticky=tk.S, padx=5)        tk.Label(root, text='Too Loud').grid(row=1, column=2, sticky=tk.N, padx=5)        self.offset = tk.IntVar(root, value=0, name='offset')        spinbox = tk.Spinbox(root, from_=-20, to=20, textvariable=self.offset, state='readonly', width=12)        spinbox.grid(row=0, column=3, sticky=tk.SW, padx=5)        tk.Label(root, text='Calibration (dB)').grid(row=1, column=3, sticky=tk.NW, padx=5)    def close(self) -> None:        self.app_closed = True    def run(self) -> None:        while not self.app_closed:            new_decibel = self.meter.listen(self.offset.get())            self.update(new_decibel, self.meter.max_decibel)            self.root.update()    def update(self, new_decibel: float, max_decibel: float) -> None:        self.gaugedb.set_value(np.around(new_decibel, 1))        self.maxdb_display.set_value(f'{max_decibel:.1f}')        self.led.to_red(on=new_decibel > 85)def main() -> None:    with Meter() as meter:        gui = GUI(meter)        gui.run()if __name__ == '__main__':    main()

modified layout

answeredSep 25, 2022 at 22:32
Reinderien's user avatar
\$\endgroup\$

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.