0

Currently making an application which allows me to make a lightshow with some custom build LED-Controllers and for that i need to draw the waveform of the song on a widget.

Although I managed to do this it is still VERY slow (especially with .wav files longer than a few seconds). The thing is I don't know how to optimise this or if my approach is correct since i cant find anything on the web.

So my question is: what is the right way to go about this? How do audio editors display the waveform and are able to zoom in and out without lag?

So my current attempt at this is by using QGraphicsView and a QGraphicsScene, the latter one supposedly being made to represent a lot of custom graphics items.

The main function to look at here is drawWav() in class WavDisplay

Showcreator.py:

from PyQt6 import uic

from PyQt6.QtCore import (
    QSize,
    Qt
)

from PyQt6.QtGui import (
    QAction,
    QPen,
    QPixmap,
    QPainter,
    QColor,
    QImage
)

from PyQt6.QtWidgets import (
    QMainWindow,
    QWidget,
    QStatusBar,
    QFileDialog,
    QGraphicsScene,
    QGraphicsView,
    QGridLayout
)

import sys
import wave
import pyaudio
import numpy as np
import threading
import soundfile as sf
import threading

class MainWindow(QMainWindow):
    # audio chunk rate
    CHUNK = 1024

    def __init__(self):
        super().__init__()

        # set window title
        self.setWindowTitle("LED Music Show")

        # create file button
        button_action = QAction("Open .wav file", self)
        button_action.setStatusTip("Open a Wave file to the Editor.")
        button_action.triggered.connect(self.openWav)

        # set status bar
        self.setStatusBar(QStatusBar(self))

        # create menubar
        menu = self.menuBar()
        
        # add file button to status bar
        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)

        # create layout
        layout = QGridLayout()
        layout.setContentsMargins(0,0,0,0)

        # create Wave display object
        self.waveformspace = WavDisplay()

        # add widget to layout
        layout.addWidget(self.waveformspace, 0, 1)

        self.centralWidget = QWidget()
        self.centralWidget.setLayout(layout)
  
        self.setCentralWidget(self.centralWidget)

    def openWav(self):
        # file selection window
        self.filename, check = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","Wave files (*.wav)")
        self.file = None

        # try to open .wav with two methods
        try:
            try:
                self.file = wave.open(self.filename, "rb")
            except:
                print("Failed to open with wave")
                try:
                    self.file, samplerate = sf.read(self.filename, dtype='float32')
                except:
                    print("Failed to open with soundfile")

            # read file and convert it to array
            self.signal = self.file.readframes(-1)
            self.signal = np.fromstring(self.signal, dtype = np.int16)
            
            # set file for drawing
            self.waveformspace.setWavefile(self.signal)
            self.waveformspace.drawWav()

            # return file cursor to start
            self.file.rewind()

            # start thread for the player
            # self.player = threading.Thread(target = self.playWav)
            # try:
            #     self.player.daemon = True
            # except:
            #     print("Failed to set player to Daemon")
            # self.player.start()

        except:
            print("Err opening File")

    def playWav(self):
        lastFile = None
        lastpos = None
        p = pyaudio.PyAudio()

        data = None
        sampwidth = None
        fps = None
        chn = None
        farmes = None
        currentpos = 0
        framespersec = None

        while True:
            if self.file != lastFile:
                # get file info
                sampwidth = self.file.getsampwidth()
                fps = self.file.getframerate()
                chn = self.file.getnchannels()
                frames = self.file.getnframes()
                lastFile = self.file
                # open audio stream
                stream = p.open(format = p.get_format_from_width(sampwidth), channels = chn, rate = fps, output = True)
                # read first frame
                data = self.file.readframes(self.CHUNK)
                framespersec = sampwidth * chn * fps
                print("file changed")

            if self.pos != lastpos:
                # read file for offset
                self.file.readframes(int(self.pos * framespersec))
                lastpos = self.pos
                frames = self.file.getnframes()
                print("pos changed")

            while data and self.running:
                # writing to the stream
                stream.write(data)
                data = self.file.readframes(self.CHUNK)
                currentpos = currentpos + self.CHUNK

        # cleanup stuff.
        self.file.close()
        stream.close()    
        p.terminate()
        return

class WavDisplay(QGraphicsView):
    file = None
    maxAmplitude = 0
    fileset = False

    def __init__(self):
        super().__init__()

    def setWavefile(self, externFile):
        self.file = externFile
        self.fileset = True

        # find the max deviation from 0 db to set draw borders
        if max(self.file) > abs(min(self.file)):
            self.maxAmplitude = max(self.file) * 2
        else:
            self.maxAmplitude = abs(min(self.file)) * 2

    def drawWav(self):
        # only draw when there is a set file
        if self.fileset:
            width = self.frameGeometry().width()
            height = self.frameGeometry().height()

            vStep = height / self.maxAmplitude

            scene = QGraphicsScene(self)

            # to draw on the middle of the widget
            h = height / 2

            # method 1 of drawing: looks at sections of the file and determines the max and min amplitude that would be visible on a single "column" of pixels and draws a vertical line between them
            if width < len(self.file):
                hStep = len(self.file) / width
                drawArray = np.empty((width, 3))
                for i in range(width - 1):
                    buffer = self.file[int(np.ceil(i * hStep)) : int(np.ceil((i + 1) * hStep))]
                    drawArray[i][0] = (min(buffer) * vStep) + h
                    drawArray[i][1] = (max(buffer) * vStep) + h
                for i in range(width - 1):
                    self.line = scene.addLine(i, drawArray[i][0], i, drawArray[i][1])
            # method 2 of drawing: this only happens when the amount of samples to draw is less than the windows width (e.g. when zoomed in and you can see the individual samples) 
            else:
                hStep = width / len(self.file)
                for i in range(len(self.file) - 1):
                    self.line = scene.addLine(i * hStep, int(self.file[i] * vStep + h), (i + 1) * hStep, int(self.file[i + 1] * vStep + h))

            self.setScene(scene)
            self.setContentsMargins(0,0,0,0)
            self.show()

    def resizeEvent(self, event) -> None:
        # has to redraw the wave file if the window gets resized
        self.drawWav()

# class not used yet        
class effectList(QGraphicsView):
    bpm = 130
    trackBeats = 0

    def __init__(self):
        super().__init__()

    def setBeatsAndBpm(self, trackLenght, Bpm):
        self.bpm = Bpm
        self.trackBeats = (trackLenght / 60) * self.bpm

    

main.py:

from PyQt6 import QtCore, QtGui, QtWidgets
from Showcreator import MainWindow

app = QtWidgets.QApplication([])
window = MainWindow()
window.show()
app.exec()

In essence: Where do i need to start to make this wave file view like one in for example Audacity? (Aka a fast rendering view which doesnt take ages)

Btw i have looked at seemingly duplicates of this question and as you can see in the code i have an algorythem that is only drawing as many lines as the window is wide and not all the 100000+ lines for each sample so the main problem i have should be the rendering method i guess.

Edit: I have all the data preloaded as im loading a wave file and convert it into a numpy array. And i need to display the file as a whole but be able to zoom in dynamically-

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • The question is a bit too broad and its purpose vague. For instance, it's unclear if you need to show the wave in realtime (similarly to a live analysis tool) or you can preload the data, or if the zoom is predefined or can change dynamically at runtime (i.e. with mouse interaction). In any case, audio editors use various strategies to display wave forms, often caching the value ranges with different "time zoom factors" (by splitting the data in chunks and only storing the min and max values for each chunk), and eventually dynamically loading data at different ratios as needed. – musicamante Nov 09 '22 at 11:24
  • Also, remember that such programs are normally written in languages that have much better performances than python (C++ is the most common), allowing faster wave computing and displaying, which is fundamental for such big data sizes: for obvious reasons the program doesn't store the whole data in RAM, so it must be able to reanalyze data on demand, and do it as fast as possible, but without blocking the Ui. This normally happens using threading: data is read in another thread, analyzed according to the current scale, and sent back to the Ui for displaying as soon as each chunk is available. – musicamante Nov 09 '22 at 11:33
  • Thanks for replying, i added some details that you said were missing. So what youre saying is that python is to slow? Also wouldn't it still take some time if the data is read in another thread with the only exeption being that the main gui doesnt freeze? – NoWayOut8344 Nov 09 '22 at 12:21

0 Answers0