PyQt5 Application for visualizing ambient sound in real-time (codes included)

Utpal Kumar   4 minute read      

A PyQt5 application for retrieving and visualizing sound waveforms in real time. Codes included.

I would like to share with you the Python desktop application for visualizing voice and sound in real time. Currently, this application only take the audio from your system’s microphone and plot it in real time using matlplotlib.

There are several applications one can build upon this code - save the recording, load the recording and play, analyze the recording, remove noise, apply PCA and other analysis algorithms to obtain different informations.

Please note that this code has not been compiled for one click application but it can be easily done for Windows, Mac or Linux using the Pyinstaller.

Install libraries

The application is based on the PyQt5 library and the design is made with the help of QT designer. It uses the sounddevice library to connect with the sound port, and Matplotlib library to plot it. Check sounddevice for details.

Using Anaconda

conda create -n voiceplotter
conda activate voiceplotter
pip install PyQt5 matplotlib sounddevice
python voicePlotter.py

Using venv (in Python 3 only)

python3 -m venv voiceplotter
source voiceplotter/bin/activate
pip install PyQt5 matplotlib sounddevice
python voicePlotter.py

Code description (in short)

See comments:

from PyQt5.QtMultimedia import QAudioDeviceInfo, QAudio
from PyQt5.QtCore import pyqtSlot
from PyQt5 import uic
from PyQt5 import QtCore, QtWidgets
import sounddevice as sd
import numpy as np
import queue
import matplotlib.ticker as ticker
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import sys
import os
import matplotlib
from PyQt5.QtGui import QIcon

matplotlib.use("Qt5Agg")


# uses QAudio to obtain all the available devices on the system
input_audio_deviceInfos = QAudioDeviceInfo.availableDevices(QAudio.AudioInput)

# class with all the specification for plotting the matplotlib figure


class MplCanvas(FigureCanvas):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        fig.patch.set_facecolor("#00B28C")
        self.axes = fig.add_subplot(111)

        super(MplCanvas, self).__init__(fig)
        fig.tight_layout()

# The main window that is called to run the application


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        # import the QT designer created ui for the application
        self.ui = uic.loadUi("main.ui", self)
        self.resize(888, 600)  # reset the size
        self.ui.setWindowTitle('Voice Plotter')
        self.ui.setWindowIcon(QIcon(os.path.join('icons', 'sound.png')))

        self.threadpool = QtCore.QThreadPool()
        self.threadpool.setMaxThreadCount(1)
        self.devices_list = []
        for device in input_audio_deviceInfos:
            self.devices_list.append(device.deviceName())

        # add all the available device name to the combo box
        self.comboBox.addItems(self.devices_list)
        # when the combobox selection changes run the function update_now
        self.comboBox.currentIndexChanged["QString"].connect(self.update_now)
        self.comboBox.setCurrentIndex(0)

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        self.canvas.axes.set_facecolor("#D5F9FF")
        self.ui.gridLayout_4.addWidget(self.canvas, 2, 1, 1, 1) #create gridlayout for matplotlib figure in the canvas
        self.reference_plot = None
        self.q = queue.Queue(maxsize=20)

        # plot specifications
        self.window_length = 1000  # for obtaining sound
        self.downsample = 1  # for obtaining sound
        self.channels = [1]
        self.interval = 30  # update plot every 30/1000 second
        self.yrangeMinVal = -0.5
        self.yrangeMaxVal = 0.5
        # self.all_devices = list(sd.query_devices())
        # print(len(self.all_devices))
        self.device_success = 0
        for self.device in range(len(input_audio_deviceInfos)):
            try:
                device_info = sd.query_devices(self.device, "input")
                if device_info:
                    self.device_success = 1
                    break
            except:
                pass
        if self.device_success:  # run if the device connection is successful
            # print(device_info)
            self.samplerate = device_info["default_samplerate"]
            length = int(self.window_length * self.samplerate /
                         (1000 * self.downsample))
            sd.default.samplerate = self.samplerate
            self.plotdata = np.zeros((length, len(self.channels)))
        else:
          # run if no devices found
            self.disable_buttons()
            self.pushButton_2.setEnabled(False)
            self.pushButton_2.setStyleSheet(
                "QPushButton" "{" "background-color : lightblue;" "}"
            )
            self.devices_list.append("No Devices Found")
            self.comboBox.addItems(self.devices_list)

        self.timer = QtCore.QTimer()
        self.timer.setInterval(self.interval)  # msec
        self.timer.timeout.connect(self.update_plot)
        self.timer.start()
        self.data = [0]
        self.lineEdit.textChanged["QString"].connect(self.update_window_length)
        self.lineEdit_2.textChanged.connect(self.update_sample_rate)
        self.spinBox_downsample.valueChanged.connect(self.update_down_sample)
        self.spinBox_updateInterval.valueChanged.connect(self.update_interval)

        self.doubleSpinBox_yrangemin.valueChanged.connect(
            self.update_yrange_min)  # change the yminvalues when the Yrange is changed
        self.doubleSpinBox_yrangemax.valueChanged.connect(
            self.update_yrange_max)

        self.pushButton.clicked.connect(self.start_worker)
        self.pushButton_2.clicked.connect(self.stop_worker)
        self.worker = None
        self.go_on = False

    def getAudio(self):
        try:
            #Processes some pending events for the calling thread
            #One can call this function occasionally when your program is busy performing a long operation 
            QtWidgets.QApplication.processEvents()

            def audio_callback(indata, frames, time, status):
                self.q.put(indata[:: self.downsample, [0]])
            # uses sounddevice to obtain the input stream, check the InputStream for details
            stream = sd.InputStream(
                device=self.device,
                channels=max(self.channels),
                samplerate=self.samplerate,
                callback=audio_callback,
            )
            with stream:

                while True:
                    QtWidgets.QApplication.processEvents()
                    if self.go_on:

                        break
            self.enable_buttons()

        except Exception as e:
            # print("ERROR: ", e)
            self.stop_worker
            pass

    def disable_buttons(self):
        self.lineEdit.setEnabled(False)
        self.lineEdit_2.setEnabled(False)
        self.spinBox_downsample.setEnabled(False)
        self.spinBox_updateInterval.setEnabled(False)
        self.comboBox.setEnabled(False)
        self.pushButton.setEnabled(False)
        self.pushButton.setStyleSheet(
            "QPushButton" "{" "background-color : lightblue;" "}"
        )

        self.canvas.axes.clear()

    def enable_buttons(self):
        self.pushButton.setEnabled(True)
        self.lineEdit.setEnabled(True)
        self.lineEdit_2.setEnabled(True)
        self.spinBox_downsample.setEnabled(True)
        self.spinBox_updateInterval.setEnabled(True)
        self.comboBox.setEnabled(True)

    def start_worker(self):

        self.disable_buttons()

        self.canvas.axes.clear()

        self.go_on = False
        self.worker = Worker(
            self.start_stream,
        )
        self.threadpool.start(self.worker)
        self.reference_plot = None
        self.timer.setInterval(self.interval)  # msec

    def stop_worker(self):

        self.go_on = True
        with self.q.mutex:
            self.q.queue.clear()
        self.pushButton.setStyleSheet(
            "QPushButton"
            "{"
            "background-color : rgb(92, 186, 102);"
            "}"
            "QPushButton"
            "{"
            "color : white;"
            "}"
        )
        self.enable_buttons()

    def start_stream(self):
        self.getAudio()

    def update_now(self, value):
        self.device = self.devices_list.index(value)

    def update_window_length(self, value):
        self.window_length = int(value)
        length = int(self.window_length * self.samplerate /
                     (1000 * self.downsample))
        self.plotdata = np.zeros((length, len(self.channels)))

    def update_sample_rate(self, value):
        try:
            self.samplerate = int(value)
            sd.default.samplerate = self.samplerate
            length = int(
                self.window_length * self.samplerate / (1000 * self.downsample)
            )
            print(self.samplerate, sd.default.samplerate)
            self.plotdata = np.zeros((length, len(self.channels)))
        except:
            pass

    def update_down_sample(self, value):
        self.downsample = int(value)
        length = int(self.window_length * self.samplerate /
                     (1000 * self.downsample))
        self.plotdata = np.zeros((length, len(self.channels)))

    def update_interval(self, value):
        self.interval = int(value)

    def update_yrange_min(self, minval):
        self.yrangeMinVal = float(minval)

    def update_yrange_max(self, maxval):
        self.yrangeMaxVal = float(maxval)

    def update_plot(self):
        try:

            print("ACTIVE THREADS:", self.threadpool.activeThreadCount(), end=" \r")
            self.label_18.setText(f"{self.threadpool.activeThreadCount()}")
            while self.go_on is False:
                QtWidgets.QApplication.processEvents()
                try:
                    self.data = self.q.get_nowait()

                except queue.Empty:
                    break

                shift = len(self.data)
                self.plotdata = np.roll(self.plotdata, -shift, axis=0)
                self.plotdata[-shift:, :] = self.data
                self.ydata = self.plotdata[:]
                self.canvas.axes.set_facecolor("#D5F9FF")

                if self.reference_plot is None:
                    plot_refs = self.canvas.axes.plot(
                        self.ydata, color="green")
                    self.reference_plot = plot_refs[0]
                else:
                    self.reference_plot.set_ydata(self.ydata)

            self.canvas.axes.yaxis.grid(True, linestyle="--")
            start, end = self.canvas.axes.get_ylim()
            self.canvas.axes.yaxis.set_ticks(np.arange(start, end, 0.1))
            self.canvas.axes.yaxis.set_major_formatter(
                ticker.FormatStrFormatter("%0.1f")
            )
            self.canvas.axes.set_ylim(
                ymin=self.yrangeMinVal, ymax=self.yrangeMaxVal)

            self.canvas.draw()
        except Exception as e:
            print("Error:", e)
            pass


class Worker(QtCore.QRunnable):
    def __init__(self, function, *args, **kwargs):
        super(Worker, self).__init__()
        self.function = function
        self.args = args
        self.kwargs = kwargs

    @pyqtSlot()
    def run(self):

        self.function(*self.args, **self.kwargs)


app = QtWidgets.QApplication(sys.argv)

if __name__ == "__main__":
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())

Download codes

Visit my Github Repository: voiceplotter

Download source code from my GitHub Repo: voiceplotter

Voice Plotter Application Screenshot
Voice Plotter Application Screenshot

Icon Courtesy

Icons made by Pixel perfect from www.flaticon.com

Disclaimer of liability

The information provided by the Earth Inversion is made available for educational purposes only.

Whilst we endeavor to keep the information up-to-date and correct. Earth Inversion makes no representations or warranties of any kind, express or implied about the completeness, accuracy, reliability, suitability or availability with respect to the website or the information, products, services or related graphics content on the website for any purpose.

UNDER NO CIRCUMSTANCE SHALL WE HAVE ANY LIABILITY TO YOU FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF THE SITE OR RELIANCE ON ANY INFORMATION PROVIDED ON THE SITE. ANY RELIANCE YOU PLACED ON SUCH MATERIAL IS THEREFORE STRICTLY AT YOUR OWN RISK.


Leave a comment