From cf90e2d70c3b4e097614be9f12914111f9179c1b Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Sun, 13 Oct 2024 21:07:45 +0800 Subject: [PATCH] feat: initial fft spectrum visualization support The final goal is actually still clone ShadowPlayer's spectrum visualization, tho. --- CMakeLists.txt | 24 ++++++++-- README.md | 15 +++++- fftspectrum.cpp | 123 ++++++++++++++++++++++++++++++++++++++++++++++++ fftspectrum.h | 31 ++++++++++++ mainwindow.cpp | 3 ++ mainwindow.h | 2 + 6 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 fftspectrum.cpp create mode 100644 fftspectrum.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 79ed581..22a4f61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,8 @@ cmake_minimum_required(VERSION 3.12) project(pineapple-music LANGUAGES CXX) include (GNUInstallDirs) -include (FeatureSummary) +include (FeatureSummary) +include (FetchContent) set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -18,7 +19,18 @@ option(USE_QTEXTCODEC "Use QTextCodec instead of QStringConverter, in case Qt is find_package(Qt6 6.6 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED) find_package(TagLib 2.0.0) -find_package(KF6Codecs 6.1.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) @@ -31,7 +43,8 @@ set (PMUSIC_CPP_FILES playlistmanager.cpp singleapplicationmanager.cpp lrcbar.cpp - lyricsmanager.cpp + lyricsmanager.cpp + fftspectrum.cpp ) set (PMUSIC_HEADER_FILES @@ -40,7 +53,8 @@ set (PMUSIC_HEADER_FILES playlistmanager.h singleapplicationmanager.h lrcbar.h - lyricsmanager.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) diff --git a/README.md b/README.md index ead5236..ae2780d 100644 --- a/README.md +++ b/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. diff --git a/fftspectrum.cpp b/fftspectrum.cpp new file mode 100644 index 0000000..6567c61 --- /dev/null +++ b/fftspectrum.cpp @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2024 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "fftspectrum.h" + +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) +#include +#endif // QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) +#include + +#include + +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 fft(frameCount, false); + std::vector::cpx_t> samples(frameCount); + std::vector::cpx_t> mass(frameCount); + if (sampleFormat == QAudioFormat::Int16 && supportedChannelConfig.testFlag(channelConfig)) { + if (channelConfig == QAudioFormat::ChannelConfigMono) { + const QAudioBuffer::S16M* data = buffer.constData(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(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)); + } + } +} + + diff --git a/fftspectrum.h b/fftspectrum.h new file mode 100644 index 0000000..e9c3342 --- /dev/null +++ b/fftspectrum.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Gary Wang +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +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 m_freq; +}; diff --git a/mainwindow.cpp b/mainwindow.cpp index 537cb8a..28c4262 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -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()); diff --git a/mainwindow.h b/mainwindow.h index a6a770b..86e68fe 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -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;