From 47f4359cefc835250dcca700dd0e5cba8f32461b Mon Sep 17 00:00:00 2001 From: billy Date: Wed, 9 Apr 2025 10:45:56 -0400 Subject: [PATCH] visualizer continues to fall when playback is paused. moved visualizer code into AudioVisualizer component --- components/AudioVisualizer.py | 99 +++++++++++++++++++++++++++++++++-- main.py | 75 ++------------------------ 2 files changed, 99 insertions(+), 75 deletions(-) diff --git a/components/AudioVisualizer.py b/components/AudioVisualizer.py index 09f9346..568b579 100644 --- a/components/AudioVisualizer.py +++ b/components/AudioVisualizer.py @@ -1,16 +1,22 @@ +from PyQt5.QtCore import QTimer +from PyQt5.QtMultimedia import QAudioProbe, QMediaPlayer import numpy as np from PyQt5 import QtWidgets +from pyqtgraph.Qt.QtWidgets import QWidget from utils import FFTAnalyser +from pyqtgraph import mkBrush class AudioVisualizer(QtWidgets.QWidget): """Audio Visualizer component""" - def __init__(self, media_player): + def __init__(self, player, probe, PlotWidget): super().__init__() - self.media_player = media_player + self.player: QMediaPlayer = player + self.probe: QAudioProbe = probe + self.PlotWidget: QWidget = PlotWidget self.x_resolution = 100 - self.fft_analyser = FFTAnalyser(self.media_player, self.x_resolution) + self.fft_analyser = FFTAnalyser(self.player, self.x_resolution) self.fft_analyser.calculatedVisual.connect(self.set_amplitudes) self.fft_analyser.calculatedVisualRs.connect(self.set_rs) self.fft_analyser.start() @@ -26,6 +32,48 @@ class AudioVisualizer(QtWidgets.QWidget): np.log10(self.min_freq), np.log10(self.max_freq), self.x_resolution ) + # Audio probe for processing audio signal in real time + self.probe.setSource(self.player) + self.probe.audioBufferProbed.connect(self.process_probe) + + self.is_playing = False + self.visualizer_timer = QTimer(self) + self.visualizer_timer.setInterval(20) # Update every 100ms (adjust as needed) + self.visualizer_timer.timeout.connect(self.update_audio_visualization) + + # Graphics plot + # Make sure PlotWidget doesn't exceed album art height + # Adjust to leave room for playback controls + self.PlotWidget.setFixedHeight(225) + # x range + self.PlotWidget.setXRange( + 0, self.get_x_resolution(), padding=0 + ) + # y axis range for decibals (-96db to 0db) + self.PlotWidget.setYRange(-96, 0, padding=0) + # Logarithmic x-axis for frequency display + self.PlotWidget.setLogMode(x=False, y=False) + self.PlotWidget.setMouseEnabled(x=False, y=False) + self.PlotWidget.showGrid(x=True, y=True) + # Performance optimizations + self.PlotWidget.setAntialiasing(False) + self.PlotWidget.setDownsampling(auto=True, mode="peak") + self.PlotWidget.setClipToView(True) + + # Add tick marks for common decibel values (expanded range) + y_ticks = [ + (-84, "-84dB"), + (-60, "-60dB"), + (-36, "-36dB"), + (-12, "-12dB"), + (0, "0dB"), + ] + self.PlotWidget.getAxis("left").setTicks([y_ticks]) + + # Add frequency ticks on x-axis + freq_ticks = self.get_frequency_ticks() + self.PlotWidget.getAxis("bottom").setTicks([freq_ticks]) + def get_x_resolution(self): """Returns the resolution for the graphics plot""" return self.x_resolution @@ -71,7 +119,7 @@ class AudioVisualizer(QtWidgets.QWidget): With a noise floor cutoff at around -96dB (for very small values) """ # Avoid log(0) by adding a small epsilon - epsilon = 1e-30 + epsilon = 1e-7 amplitudes = np.maximum(self.amps, epsilon) # Convert to decibels (20*log10 is the standard formula for amplitude to dB) db_values = 20 * np.log10(amplitudes) @@ -90,3 +138,46 @@ class AudioVisualizer(QtWidgets.QWidget): # self.amps = np.maximum(np.array(amps), 1e-12) # Set a very small threshold # print(self.amps) self.amps = np.array(amps) + + + def process_probe(self, buff): + """Audio visualizer buffer processing""" + # buff.startTime() # what is this? why need? dont need... + self.is_playing = True + + # If music is playing, reset the timer + if not self.visualizer_timer.isActive(): + self.visualizer_timer.start() + + def update_audio_visualization(self): + """Update the visualization for audio signal""" + if not self.is_playing: + # If music stopped, continue updating visualizer for a few seconds + self.visualizer_timer.stop() # Stop the timer once it's done + + # Assuming y is the decibel data + y = self.get_decibels() + + if len(y) == 0: + return + + # Clear the previous plot + self.PlotWidget.clear() + + # Plot new values + self._plot_item = self.PlotWidget.plot( + self._x_data, + y, + pen="b", + fillLevel=-96 if self.use_decibels else 0, + fillBrush=mkBrush("b"), + ) + + # If the visualizer is done, stop updating + if all(val == -96 for val in y): + self.is_playing = False + self.visualizer_timer.stop() + + def clear_audio_visualization(self) -> None: + self.PlotWidget.clear() + self._plot_item = None diff --git a/main.py b/main.py index 1efa42c..a264092 100644 --- a/main.py +++ b/main.py @@ -172,9 +172,9 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # widget bits self.album_art_scene: QGraphicsScene = QGraphicsScene() self.player: QMediaPlayer = QMediaPlayer() # Audio player object - self.probe: QAudioProbe = QAudioProbe() # Gets audio data - self.audio_visualizer: AudioVisualizer = AudioVisualizer(self.player) - self.timer = QTimer(self) # Audio timing things + self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data + self.audio_visualizer: AudioVisualizer = AudioVisualizer(self.player, self.probe, self.PlotWidget) + self.timer = QTimer(self) # for playback slider and such # sharing functions with other classes and that self.tableView.load_qapp(self) @@ -186,9 +186,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.volumeLabel.setText(str(self.current_volume)) self.volumeSlider.setValue(self.current_volume) - # Audio probe for processing audio signal in real time - self.probe.setSource(self.player) - self.probe.audioBufferProbed.connect(self.process_probe) # Slider Timer (realtime playback feedback horizontal bar) self.timer.start(100) self.timer.timeout.connect(self.move_slider) @@ -196,39 +193,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # Set fixed size for album art self.albumGraphicsView.setFixedSize(250, 250) - # Graphics plot - # Make sure PlotWidget doesn't exceed album art height - # Adjust to leave room for playback controls - self.PlotWidget.setFixedHeight(225) - # x range - self.PlotWidget.setXRange( - 0, self.audio_visualizer.get_x_resolution(), padding=0 - ) - # y axis range for decibals (-96db to 0db) - self.PlotWidget.setYRange(-96, 0, padding=0) - # Logarithmic x-axis for frequency display - self.PlotWidget.setLogMode(x=False, y=False) - self.PlotWidget.setMouseEnabled(x=False, y=False) - self.PlotWidget.showGrid(x=True, y=True) - # Performance optimizations - self.PlotWidget.setAntialiasing(False) - self.PlotWidget.setDownsampling(auto=True, mode="peak") - self.PlotWidget.setClipToView(True) - - # Add tick marks for common decibel values (expanded range) - y_ticks = [ - (-84, "-84dB"), - (-60, "-60dB"), - (-36, "-36dB"), - (-12, "-12dB"), - (0, "0dB"), - ] - self.PlotWidget.getAxis("left").setTicks([y_ticks]) - - # Add frequency ticks on x-axis - freq_ticks = self.audio_visualizer.get_frequency_ticks() - self.PlotWidget.getAxis("bottom").setTicks([freq_ticks]) - # Connections self.playbackSlider.sliderReleased.connect( lambda: self.player.setPosition(self.playbackSlider.value()) @@ -237,6 +201,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.speedSlider.sliderMoved.connect( lambda: self.on_speed_changed(self.speedSlider.value()) ) + # self.speedSlider.doubleClicked.connect(lambda: self.on_speed_changed(1)) self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause self.previousButton.clicked.connect(self.on_previous_clicked) self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song @@ -482,38 +447,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): album_art_data = get_album_art(None) self.albumGraphicsView.load_album_art(album_art_data) - def process_probe(self, buff) -> None: - """Audio visualizer buffer processing""" - buff.startTime() - self.update_audio_visualization() - - def update_audio_visualization(self) -> None: - """Handles updating points on the pyqtgraph visual""" - # Use decibel values instead of raw amplitudes - y = self.audio_visualizer.get_decibels() - if len(y) == 0: - return - - # if self.audio_visualizer._plot_item is None: - # thanks cursor sonnet whatever - self.PlotWidget.clear() - # Use the actual frequency values for x-axis - self.audio_visualizer._plot_item = self.PlotWidget.plot( - self.audio_visualizer._x_data, # We'll keep using indices for drawing - y, - pen="b", # Use pen instead of fill for better performance - fillLevel=-96 - if self.audio_visualizer.use_decibels - else 0, # Fill from -96dB for decibel scale - fillBrush=mkBrush("b"), - ) - # else: - # self.audio_visualizer._plot_item.setData(self.audio_visualizer._x_data, y) - - def clear_audio_visualization(self) -> None: - self.PlotWidget.clear() - self.audio_visualizer._plot_item = None - def move_slider(self) -> None: """Handles moving the playback slider""" if self.player.state() == QMediaPlayer.State.StoppedState: