feat: initial fft spectrum visualization support
The final goal is actually still clone ShadowPlayer's spectrum visualization, tho.
This commit is contained in:
		@ -4,6 +4,7 @@ project(pineapple-music LANGUAGES CXX)
 | 
			
		||||
 | 
			
		||||
include (GNUInstallDirs)
 | 
			
		||||
include (FeatureSummary)
 | 
			
		||||
include (FetchContent)
 | 
			
		||||
 | 
			
		||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
 | 
			
		||||
 | 
			
		||||
@ -20,6 +21,17 @@ find_package(Qt6 6.6 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRE
 | 
			
		||||
find_package(TagLib 2.0.0)
 | 
			
		||||
find_package(KF6Codecs 6.1.0)
 | 
			
		||||
 | 
			
		||||
FetchContent_Declare(
 | 
			
		||||
    kissfft
 | 
			
		||||
    GIT_REPOSITORY https://github.com/mborgerding/kissfft.git
 | 
			
		||||
    GIT_TAG f5f2a3b2f2cd02bf80639adb12cbeed125bdf420
 | 
			
		||||
)
 | 
			
		||||
set(KISSFFT_PKGCONFIG OFF CACHE BOOL "dep(kissfft): pkgconfig support")
 | 
			
		||||
set(KISSFFT_STATIC ON CACHE BOOL "dep(kissfft): static linking")
 | 
			
		||||
set(KISSFFT_TEST OFF CACHE BOOL "dep(kissfft): enable testing")
 | 
			
		||||
set(KISSFFT_TOOLS OFF CACHE BOOL "dep(kissfft): build tools")
 | 
			
		||||
FetchContent_MakeAvailable(kissfft)
 | 
			
		||||
 | 
			
		||||
if (USE_QTEXTCODEC)
 | 
			
		||||
    find_package(Qt6 6.6 COMPONENTS Core5Compat REQUIRED)
 | 
			
		||||
endif()
 | 
			
		||||
@ -32,6 +44,7 @@ set (PMUSIC_CPP_FILES
 | 
			
		||||
    singleapplicationmanager.cpp
 | 
			
		||||
    lrcbar.cpp
 | 
			
		||||
    lyricsmanager.cpp
 | 
			
		||||
    fftspectrum.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
set (PMUSIC_HEADER_FILES
 | 
			
		||||
@ -41,6 +54,7 @@ set (PMUSIC_HEADER_FILES
 | 
			
		||||
    singleapplicationmanager.h
 | 
			
		||||
    lrcbar.h
 | 
			
		||||
    lyricsmanager.h
 | 
			
		||||
    fftspectrum.h
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
set (PMUSIC_UI_FILES
 | 
			
		||||
@ -80,7 +94,7 @@ if (TARGET KF6::Codecs)
 | 
			
		||||
    target_link_libraries (${EXE_NAME} PRIVATE KF6::Codecs)
 | 
			
		||||
endif ()
 | 
			
		||||
 | 
			
		||||
target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network)
 | 
			
		||||
target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network kissfft::kissfft)
 | 
			
		||||
 | 
			
		||||
if (USE_QTEXTCODEC)
 | 
			
		||||
    target_compile_definitions(${EXE_NAME} PRIVATE USE_QTEXTCODEC=1)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								README.md
									
									
									
									
									
								
							@ -2,12 +2,23 @@
 | 
			
		||||
 | 
			
		||||
Since **I** just need a simple player which *just works* right now, so I did many things in a dirty way. Don't feel so weird if you saw something I did in this project is using a bad approach.
 | 
			
		||||
 | 
			
		||||
### Feature Notice
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
We have the following features:
 | 
			
		||||
 | 
			
		||||
- [Sidecar](https://en.wikipedia.org/wiki/Sidecar_file) lyrics file (`.lrc`) support with an optional desktop lyrics bar widget
 | 
			
		||||
- Auto-load all audio files in the same folder of the file that you attempted to play, into a playlist
 | 
			
		||||
 | 
			
		||||
But these features are not available, some of them are TBD and others are not planned:
 | 
			
		||||
 | 
			
		||||
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
 | 
			
		||||
  - ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
 | 
			
		||||
- No music library management support and there won't be one!
 | 
			
		||||
  - It'll auto-load music files in the same folder of the file that you attempted to play, so organize your music files on a folder-basis.
 | 
			
		||||
- Limited system integration:
 | 
			
		||||
  - No [SMTC](https://learn.microsoft.com/en-us/uwp/api/windows.media.systemmediatransportcontrols) support under Windows for now
 | 
			
		||||
  - No [MPRIS](https://www.freedesktop.org/wiki/Specifications/mpris-spec/) support under Linux desktop for now
 | 
			
		||||
  - No "playback progress on taskbar icon" and "taskbar thumbnail buttons" support whatever on Windows or Linux desktop for now
 | 
			
		||||
- Limited lyrics (`.lrc`) loading support:
 | 
			
		||||
  - Currently no `.tlrc` (for translated lyrics) or `.rlrc` (for romanized lyrics) support.
 | 
			
		||||
  - Multi-line lyrics and duplicated timestamps are not supported
 | 
			
		||||
@ -20,7 +31,7 @@ Current state, we need:
 | 
			
		||||
 - `cmake` as the build system.
 | 
			
		||||
 - `qt6` with `qt6-multimedia` since we use it for playback.
 | 
			
		||||
 - `taglib` to get the audio file properties.
 | 
			
		||||
 - `pkg-config` to find the installed taglib.
 | 
			
		||||
 - `kissfft` for FFT support (will be downloaded at configure-time by `cmake`).
 | 
			
		||||
 | 
			
		||||
Then we can build it with any proper c++ compiler like g++ or msvc.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										123
									
								
								fftspectrum.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								fftspectrum.cpp
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
#include "fftspectrum.h"
 | 
			
		||||
 | 
			
		||||
#include <QAudioBuffer>
 | 
			
		||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
 | 
			
		||||
#include <QAudioBufferOutput>
 | 
			
		||||
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
 | 
			
		||||
#include <QPainter>
 | 
			
		||||
 | 
			
		||||
#include <kissfft.hh>
 | 
			
		||||
 | 
			
		||||
FFTSpectrum::FFTSpectrum(QWidget* parent)
 | 
			
		||||
    : QWidget(parent)
 | 
			
		||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
 | 
			
		||||
    , m_audioBufferOutput(new QAudioBufferOutput(this))
 | 
			
		||||
#endif // #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
 | 
			
		||||
{
 | 
			
		||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
 | 
			
		||||
    connect(this, &FFTSpectrum::mediaPlayerChanged, this, [=]() {
 | 
			
		||||
        if (m_mediaPlayer) {
 | 
			
		||||
            m_mediaPlayer->setAudioBufferOutput(m_audioBufferOutput);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    connect(m_audioBufferOutput, &QAudioBufferOutput::audioBufferReceived, this, [=](const QAudioBuffer& buffer) {
 | 
			
		||||
        const QAudioFormat& fmt = buffer.format();
 | 
			
		||||
        const QAudioFormat::SampleFormat sampleFormat = fmt.sampleFormat();
 | 
			
		||||
        QAudioFormat::ChannelConfig channelConfig = fmt.channelConfig();
 | 
			
		||||
        const QFlags supportedChannelConfig({ QAudioFormat::ChannelConfigMono, QAudioFormat::ChannelConfigStereo });
 | 
			
		||||
        const int frameCount = buffer.frameCount();
 | 
			
		||||
        kissfft<float> fft(frameCount, false);
 | 
			
		||||
        std::vector<kissfft<float>::cpx_t> samples(frameCount);
 | 
			
		||||
        std::vector<kissfft<float>::cpx_t> mass(frameCount);
 | 
			
		||||
        if (sampleFormat == QAudioFormat::Int16 && supportedChannelConfig.testFlag(channelConfig)) {
 | 
			
		||||
            if (channelConfig == QAudioFormat::ChannelConfigMono) {
 | 
			
		||||
                const QAudioBuffer::S16M* data = buffer.constData<QAudioBuffer::S16M>();
 | 
			
		||||
                for (int i = 0; i < frameCount; ++i) {
 | 
			
		||||
                    samples[i].real(data[i].value(QAudioFormat::FrontCenter) / float(32768));
 | 
			
		||||
                    samples[i].imag(0);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                const QAudioBuffer::S16S* data = buffer.constData<QAudioBuffer::S16S>();
 | 
			
		||||
                for (int i = 0; i < frameCount; ++i) {
 | 
			
		||||
                    samples[i].real(data[i].value(QAudioFormat::FrontLeft) / float(32768));
 | 
			
		||||
                    samples[i].imag(0);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else if (sampleFormat == QAudioFormat::Int32 && supportedChannelConfig.testFlag(channelConfig)) {
 | 
			
		||||
            if (channelConfig == QAudioFormat::ChannelConfigMono) {
 | 
			
		||||
                const QAudioBuffer::S32M* data = buffer.constData<QAudioBuffer::S32M>();
 | 
			
		||||
                for (int i = 0; i < frameCount; ++i) {
 | 
			
		||||
                    samples[i].real(data[i].value(QAudioFormat::FrontCenter) / float(2147483647));
 | 
			
		||||
                    samples[i].imag(0);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                const QAudioBuffer::S32S* data = buffer.constData<QAudioBuffer::S32S>();
 | 
			
		||||
                for (int i = 0; i < frameCount; ++i) {
 | 
			
		||||
                    samples[i].real(data[i].value(QAudioFormat::FrontLeft) / float(2147483647));
 | 
			
		||||
                    samples[i].imag(0);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else if (sampleFormat == QAudioFormat::Float && supportedChannelConfig.testFlag(channelConfig)) {
 | 
			
		||||
            if (channelConfig == QAudioFormat::ChannelConfigMono) {
 | 
			
		||||
                const QAudioBuffer::F32M* data = buffer.constData<QAudioBuffer::F32M>();
 | 
			
		||||
                for (int i = 0; i < frameCount; ++i) {
 | 
			
		||||
                    samples[i].real(data[i].value(QAudioFormat::FrontCenter));
 | 
			
		||||
                    samples[i].imag(0);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                const QAudioBuffer::F32S* data = buffer.constData<QAudioBuffer::F32S>();
 | 
			
		||||
                for (int i = 0; i < frameCount; ++i) {
 | 
			
		||||
                    samples[i].real(data[i].value(QAudioFormat::FrontLeft));
 | 
			
		||||
                    samples[i].imag(0);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            qWarning() << "Unsupported format or channel config:" << sampleFormat << channelConfig;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        fft.transform(samples.data(), mass.data());
 | 
			
		||||
        m_freq.resize(frameCount);
 | 
			
		||||
        for (int i = 0; i < frameCount; i++) {
 | 
			
		||||
            m_freq[i] = sqrt(mass[i].real() * mass[i].real() + mass[i].imag() * mass[i].imag());
 | 
			
		||||
        }
 | 
			
		||||
        for (auto& val : m_freq) {
 | 
			
		||||
            val = log10(val + 1);
 | 
			
		||||
        }
 | 
			
		||||
        update();
 | 
			
		||||
    });
 | 
			
		||||
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
 | 
			
		||||
    resize(490, 420);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FFTSpectrum::~FFTSpectrum()
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FFTSpectrum::setMediaPlayer(QMediaPlayer* player)
 | 
			
		||||
{
 | 
			
		||||
    m_mediaPlayer = player;
 | 
			
		||||
    emit mediaPlayerChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FFTSpectrum::paintEvent(QPaintEvent* e)
 | 
			
		||||
{
 | 
			
		||||
    QPainter painter(this);
 | 
			
		||||
 | 
			
		||||
    if (!m_freq.empty()) {
 | 
			
		||||
        int width = this->width();
 | 
			
		||||
        int height = this->height();
 | 
			
		||||
        int barWidth = std::max(1ULL, width / m_freq.size());
 | 
			
		||||
        for (int i = 0; i < m_freq.size(); i++) {
 | 
			
		||||
            int barHeight = static_cast<int>(sqrt(m_freq[i]) * height * 0.5);
 | 
			
		||||
            painter.fillRect(i * barWidth, height - barHeight, barWidth, barHeight, QColor(70, 130, 180, (int)(140 * m_freq[i]) + 90));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								fftspectrum.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								fftspectrum.h
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <QWidget>
 | 
			
		||||
#include <QMediaPlayer>
 | 
			
		||||
 | 
			
		||||
class QAudioBufferOutput;
 | 
			
		||||
class FFTSpectrum : public QWidget
 | 
			
		||||
{
 | 
			
		||||
    Q_OBJECT
 | 
			
		||||
    Q_PROPERTY(QMediaPlayer * mediaPlayer MEMBER m_mediaPlayer WRITE setMediaPlayer NOTIFY mediaPlayerChanged)
 | 
			
		||||
public:
 | 
			
		||||
    explicit FFTSpectrum(QWidget* parent);
 | 
			
		||||
    ~FFTSpectrum();
 | 
			
		||||
 | 
			
		||||
    void setMediaPlayer(QMediaPlayer* player);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
    void mediaPlayerChanged();
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    void paintEvent(QPaintEvent* e) override;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    QMediaPlayer* m_mediaPlayer = nullptr;
 | 
			
		||||
    QAudioBufferOutput* m_audioBufferOutput = nullptr;
 | 
			
		||||
    std::vector<float> m_freq;
 | 
			
		||||
};
 | 
			
		||||
@ -6,6 +6,7 @@
 | 
			
		||||
#include "./ui_mainwindow.h"
 | 
			
		||||
 | 
			
		||||
#include "playlistmanager.h"
 | 
			
		||||
#include "fftspectrum.h"
 | 
			
		||||
#include "lrcbar.h"
 | 
			
		||||
 | 
			
		||||
// taglib
 | 
			
		||||
@ -41,6 +42,7 @@ MainWindow::MainWindow(QWidget *parent)
 | 
			
		||||
    , m_mediaDevices(new QMediaDevices(this))
 | 
			
		||||
    , m_mediaPlayer(new QMediaPlayer(this))
 | 
			
		||||
    , m_audioOutput(new QAudioOutput(this))
 | 
			
		||||
    , m_fftSpectrum(new FFTSpectrum(this))
 | 
			
		||||
    , m_lrcbar(new LrcBar(nullptr))
 | 
			
		||||
    , m_playlistManager(new PlaylistManager(this))
 | 
			
		||||
{
 | 
			
		||||
@ -48,6 +50,7 @@ MainWindow::MainWindow(QWidget *parent)
 | 
			
		||||
    m_playlistManager->setAutoLoadFilterSuffixes({
 | 
			
		||||
        "*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac", "*.tta"
 | 
			
		||||
    });
 | 
			
		||||
    m_fftSpectrum->setMediaPlayer(m_mediaPlayer);
 | 
			
		||||
    m_mediaPlayer->setAudioOutput(m_audioOutput);
 | 
			
		||||
    m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
 | 
			
		||||
    ui->playlistView->setModel(m_playlistManager->model());
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ class QAudioOutput;
 | 
			
		||||
class QPropertyAnimation;
 | 
			
		||||
QT_END_NAMESPACE
 | 
			
		||||
 | 
			
		||||
class FFTSpectrum;
 | 
			
		||||
class LrcBar;
 | 
			
		||||
class PlaylistManager;
 | 
			
		||||
class MainWindow : public QMainWindow
 | 
			
		||||
@ -92,6 +93,7 @@ private:
 | 
			
		||||
    QMediaDevices *m_mediaDevices;
 | 
			
		||||
    QMediaPlayer *m_mediaPlayer;
 | 
			
		||||
    QAudioOutput *m_audioOutput;
 | 
			
		||||
    FFTSpectrum* m_fftSpectrum;
 | 
			
		||||
    LrcBar *m_lrcbar;
 | 
			
		||||
    QPropertyAnimation *m_fadeOutAnimation;
 | 
			
		||||
    PlaylistManager *m_playlistManager;
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user