Compare commits
8 Commits
moodbar
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2afa8fd66 | ||
|
|
4da4c9e267 | ||
|
|
02c1596ff4 | ||
|
|
597f983c92 | ||
|
|
0e0117b19b | ||
|
|
4e0cc1c0da | ||
|
|
f77e92d634 | ||
|
|
f25cdb3431 |
@@ -295,6 +295,12 @@ if(UNIX AND NOT APPLE)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
optional_component(WINDOWS_MEDIA_CONTROLS ON "Windows Media Transport Controls"
|
||||
DEPENDS "MSVC compiler" MSVC
|
||||
)
|
||||
endif()
|
||||
|
||||
optional_component(SONGFINGERPRINTING ON "Song fingerprinting and tracking"
|
||||
DEPENDS "chromaprint" CHROMAPRINT_FOUND
|
||||
)
|
||||
@@ -1294,6 +1300,7 @@ endif()
|
||||
optional_source(HAVE_ALSA SOURCES src/engine/alsadevicefinder.cpp src/engine/alsapcmdevicefinder.cpp)
|
||||
optional_source(HAVE_PULSE SOURCES src/engine/pulsedevicefinder.cpp)
|
||||
optional_source(MSVC SOURCES src/engine/uwpdevicefinder.cpp src/engine/asiodevicefinder.cpp)
|
||||
optional_source(HAVE_WINDOWS_MEDIA_CONTROLS SOURCES src/core/windowsmediacontroller.cpp HEADERS src/core/windowsmediacontroller.h)
|
||||
optional_source(HAVE_CHROMAPRINT SOURCES src/engine/chromaprinter.cpp)
|
||||
|
||||
optional_source(HAVE_MUSICBRAINZ
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#cmakedefine HAVE_GIO_UNIX
|
||||
#cmakedefine HAVE_DBUS
|
||||
#cmakedefine HAVE_MPRIS2
|
||||
#cmakedefine HAVE_WINDOWS_MEDIA_CONTROLS
|
||||
#cmakedefine HAVE_UDISKS2
|
||||
#cmakedefine HAVE_AUDIOCD
|
||||
#cmakedefine HAVE_MTP
|
||||
|
||||
@@ -110,32 +110,21 @@ bool FilesystemMusicStorage::CopyToStorage(const CopyJob &job, QString &error_te
|
||||
|
||||
bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
|
||||
|
||||
const QString path = job.metadata_.url().toLocalFile();
|
||||
const QFileInfo fileInfo(path);
|
||||
QString path = job.metadata_.url().toLocalFile();
|
||||
QFileInfo fileInfo(path);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
||||
if (job.use_trash_ && QFile::supportsMoveToTrash()) {
|
||||
#else
|
||||
if (job.use_trash_) {
|
||||
#endif
|
||||
if (QFile::moveToTrash(path)) {
|
||||
return true;
|
||||
}
|
||||
qLog(Warning) << "Moving file to trash failed for" << path << ", falling back to direct deletion";
|
||||
return QFile::moveToTrash(path);
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (fileInfo.isDir()) {
|
||||
success = Utilities::RemoveRecursive(path);
|
||||
}
|
||||
else {
|
||||
success = QFile::remove(path);
|
||||
return Utilities::RemoveRecursive(path);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
qLog(Error) << "Failed to delete file" << path;
|
||||
}
|
||||
|
||||
return success;
|
||||
return QFile::remove(path);
|
||||
|
||||
}
|
||||
|
||||
@@ -98,9 +98,6 @@ SongLoader::SongLoader(const SharedPtr<UrlHandlers> url_handlers,
|
||||
|
||||
QObject::connect(timeout_timer_, &QTimer::timeout, this, &SongLoader::Timeout);
|
||||
|
||||
QObject::connect(playlist_parser_, &PlaylistParser::Error, this, &SongLoader::ParserError);
|
||||
QObject::connect(cue_parser_, &CueParser::Error, this, &SongLoader::ParserError);
|
||||
|
||||
}
|
||||
|
||||
SongLoader::~SongLoader() {
|
||||
@@ -109,10 +106,6 @@ SongLoader::~SongLoader() {
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::ParserError(const QString &error) {
|
||||
errors_ << error;
|
||||
}
|
||||
|
||||
SongLoader::Result SongLoader::Load(const QUrl &url) {
|
||||
|
||||
if (url.isEmpty()) return Result::Error;
|
||||
@@ -294,7 +287,6 @@ SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
|
||||
}
|
||||
|
||||
if (parser) { // It's a playlist!
|
||||
QObject::connect(parser, &ParserBase::Error, this, &SongLoader::ParserError, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
|
||||
qLog(Debug) << "Parsing using" << parser->name();
|
||||
LoadPlaylist(parser, filename);
|
||||
return Result::Success;
|
||||
@@ -714,10 +706,6 @@ void SongLoader::MagicReady() {
|
||||
StopTypefindAsync(true);
|
||||
}
|
||||
|
||||
if (parser_) {
|
||||
QObject::connect(parser_, &ParserBase::Error, this, &SongLoader::ParserError, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
|
||||
}
|
||||
|
||||
state_ = State::WaitingForData;
|
||||
|
||||
if (!IsPipelinePlaying()) {
|
||||
|
||||
@@ -99,7 +99,6 @@ class SongLoader : public QObject {
|
||||
void ScheduleTimeout();
|
||||
void Timeout();
|
||||
void StopTypefind();
|
||||
void ParserError(const QString &error);
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
void AudioCDTracksLoadErrorSlot(const QString &error);
|
||||
|
||||
303
src/core/windowsmediacontroller.cpp
Normal file
303
src/core/windowsmediacontroller.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
// Undefine 'interface' macro from windows.h before including WinRT headers
|
||||
#pragma push_macro("interface")
|
||||
#undef interface
|
||||
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Media.h>
|
||||
#include <winrt/Windows.Storage.h>
|
||||
#include <winrt/Windows.Storage.Streams.h>
|
||||
|
||||
#pragma pop_macro("interface")
|
||||
|
||||
// Include the interop header for ISystemMediaTransportControlsInterop
|
||||
#include <systemmediatransportcontrolsinterop.h>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "windowsmediacontroller.h"
|
||||
|
||||
#include "core/song.h"
|
||||
#include "core/player.h"
|
||||
#include "engine/enginebase.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "covermanager/currentalbumcoverloader.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
|
||||
using namespace winrt;
|
||||
using namespace Windows::Foundation;
|
||||
using namespace Windows::Media;
|
||||
using namespace Windows::Storage;
|
||||
using namespace Windows::Storage::Streams;
|
||||
|
||||
// Helper struct to hold the WinRT object
|
||||
struct WindowsMediaControllerPrivate {
|
||||
SystemMediaTransportControls smtc{nullptr};
|
||||
};
|
||||
|
||||
WindowsMediaController::WindowsMediaController(HWND hwnd,
|
||||
const SharedPtr<Player> player,
|
||||
const SharedPtr<PlaylistManager> playlist_manager,
|
||||
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
player_(player),
|
||||
playlist_manager_(playlist_manager),
|
||||
current_albumcover_loader_(current_albumcover_loader),
|
||||
smtc_(nullptr),
|
||||
apartment_initialized_(false) {
|
||||
|
||||
try {
|
||||
// Initialize WinRT apartment if not already initialized
|
||||
// Qt or other components may have already initialized it
|
||||
try {
|
||||
winrt::init_apartment(winrt::apartment_type::single_threaded);
|
||||
apartment_initialized_ = true;
|
||||
}
|
||||
catch (const hresult_error &e) {
|
||||
// Apartment already initialized - this is fine, continue
|
||||
if (e.code() != RPC_E_CHANGED_MODE) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Create private implementation
|
||||
auto *priv = new WindowsMediaControllerPrivate();
|
||||
smtc_ = priv;
|
||||
|
||||
// Get the SystemMediaTransportControls instance for this window
|
||||
// Use the interop interface
|
||||
auto interop = winrt::get_activation_factory<SystemMediaTransportControls, ISystemMediaTransportControlsInterop>();
|
||||
|
||||
if (!interop) {
|
||||
qLog(Warning) << "Failed to get ISystemMediaTransportControlsInterop";
|
||||
delete priv;
|
||||
smtc_ = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get SMTC for the window
|
||||
winrt::com_ptr<IInspectable> inspectable;
|
||||
HRESULT hr = interop->GetForWindow(hwnd, winrt::guid_of<SystemMediaTransportControls>(), inspectable.put_void());
|
||||
|
||||
if (FAILED(hr) || !inspectable) {
|
||||
qLog(Warning) << "Failed to get SystemMediaTransportControls for window, HRESULT:" << Qt::hex << static_cast<unsigned int>(hr);
|
||||
delete priv;
|
||||
smtc_ = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to SystemMediaTransportControls
|
||||
priv->smtc = inspectable.as<SystemMediaTransportControls>();
|
||||
|
||||
if (!priv->smtc) {
|
||||
qLog(Warning) << "Failed to cast to SystemMediaTransportControls";
|
||||
delete priv;
|
||||
smtc_ = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable the controls
|
||||
priv->smtc.IsEnabled(true);
|
||||
priv->smtc.IsPlayEnabled(true);
|
||||
priv->smtc.IsPauseEnabled(true);
|
||||
priv->smtc.IsStopEnabled(true);
|
||||
priv->smtc.IsNextEnabled(true);
|
||||
priv->smtc.IsPreviousEnabled(true);
|
||||
|
||||
// Setup button handlers
|
||||
SetupButtonHandlers();
|
||||
|
||||
// Connect signals from Player
|
||||
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &WindowsMediaController::EngineStateChanged);
|
||||
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &WindowsMediaController::CurrentSongChanged);
|
||||
QObject::connect(&*current_albumcover_loader_, &CurrentAlbumCoverLoader::AlbumCoverLoaded, this, &WindowsMediaController::AlbumCoverLoaded);
|
||||
|
||||
qLog(Info) << "Windows Media Transport Controls initialized successfully";
|
||||
}
|
||||
catch (const hresult_error &e) {
|
||||
qLog(Warning) << "Failed to initialize Windows Media Transport Controls:" << QString::fromWCharArray(e.message().c_str());
|
||||
if (smtc_) {
|
||||
delete static_cast<WindowsMediaControllerPrivate*>(smtc_);
|
||||
smtc_ = nullptr;
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
qLog(Warning) << "Failed to initialize Windows Media Transport Controls: unknown error";
|
||||
if (smtc_) {
|
||||
delete static_cast<WindowsMediaControllerPrivate*>(smtc_);
|
||||
smtc_ = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowsMediaController::~WindowsMediaController() {
|
||||
if (smtc_) {
|
||||
auto *priv = static_cast<WindowsMediaControllerPrivate*>(smtc_);
|
||||
if (priv->smtc) {
|
||||
priv->smtc.IsEnabled(false);
|
||||
}
|
||||
delete priv;
|
||||
smtc_ = nullptr;
|
||||
}
|
||||
// Only uninit if we initialized the apartment
|
||||
if (apartment_initialized_) {
|
||||
winrt::uninit_apartment();
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsMediaController::SetupButtonHandlers() {
|
||||
if (!smtc_) return;
|
||||
|
||||
auto *priv = static_cast<WindowsMediaControllerPrivate*>(smtc_);
|
||||
if (!priv->smtc) return;
|
||||
|
||||
// Handle button pressed events
|
||||
priv->smtc.ButtonPressed([this](const SystemMediaTransportControls &, const SystemMediaTransportControlsButtonPressedEventArgs &args) {
|
||||
switch (args.Button()) {
|
||||
case SystemMediaTransportControlsButton::Play:
|
||||
player_->Play();
|
||||
break;
|
||||
case SystemMediaTransportControlsButton::Pause:
|
||||
player_->Pause();
|
||||
break;
|
||||
case SystemMediaTransportControlsButton::Stop:
|
||||
player_->Stop();
|
||||
break;
|
||||
case SystemMediaTransportControlsButton::Next:
|
||||
player_->Next();
|
||||
break;
|
||||
case SystemMediaTransportControlsButton::Previous:
|
||||
player_->Previous();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void WindowsMediaController::EngineStateChanged(EngineBase::State newState) {
|
||||
UpdatePlaybackStatus(newState);
|
||||
}
|
||||
|
||||
void WindowsMediaController::UpdatePlaybackStatus(EngineBase::State state) {
|
||||
if (!smtc_) return;
|
||||
|
||||
auto *priv = static_cast<WindowsMediaControllerPrivate*>(smtc_);
|
||||
if (!priv->smtc) return;
|
||||
|
||||
try {
|
||||
switch (state) {
|
||||
case EngineBase::State::Playing:
|
||||
priv->smtc.PlaybackStatus(MediaPlaybackStatus::Playing);
|
||||
break;
|
||||
case EngineBase::State::Paused:
|
||||
priv->smtc.PlaybackStatus(MediaPlaybackStatus::Paused);
|
||||
break;
|
||||
case EngineBase::State::Empty:
|
||||
case EngineBase::State::Idle:
|
||||
priv->smtc.PlaybackStatus(MediaPlaybackStatus::Stopped);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (const hresult_error &e) {
|
||||
qLog(Warning) << "Failed to update playback status:" << QString::fromWCharArray(e.message().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsMediaController::CurrentSongChanged(const Song &song) {
|
||||
if (!song.is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update metadata immediately with what we have
|
||||
UpdateMetadata(song, QUrl());
|
||||
|
||||
// Album cover will be updated via AlbumCoverLoaded signal
|
||||
}
|
||||
|
||||
void WindowsMediaController::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
|
||||
if (!song.is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update metadata with album cover
|
||||
UpdateMetadata(song, result.temp_cover_url.isEmpty() ? result.album_cover.cover_url : result.temp_cover_url);
|
||||
}
|
||||
|
||||
void WindowsMediaController::UpdateMetadata(const Song &song, const QUrl &art_url) {
|
||||
if (!smtc_) return;
|
||||
|
||||
auto *priv = static_cast<WindowsMediaControllerPrivate*>(smtc_);
|
||||
if (!priv->smtc) return;
|
||||
|
||||
try {
|
||||
// Get the updater
|
||||
SystemMediaTransportControlsDisplayUpdater updater = priv->smtc.DisplayUpdater();
|
||||
updater.Type(MediaPlaybackType::Music);
|
||||
|
||||
// Get the music properties
|
||||
auto musicProperties = updater.MusicProperties();
|
||||
|
||||
// Set basic metadata
|
||||
if (!song.title().isEmpty()) {
|
||||
musicProperties.Title(winrt::hstring(song.title().toStdWString()));
|
||||
}
|
||||
if (!song.artist().isEmpty()) {
|
||||
musicProperties.Artist(winrt::hstring(song.artist().toStdWString()));
|
||||
}
|
||||
if (!song.album().isEmpty()) {
|
||||
musicProperties.AlbumTitle(winrt::hstring(song.album().toStdWString()));
|
||||
}
|
||||
|
||||
// Set album art if available
|
||||
if (art_url.isValid() && art_url.isLocalFile()) {
|
||||
QString artPath = art_url.toLocalFile();
|
||||
if (!artPath.isEmpty()) {
|
||||
try {
|
||||
// Use file:// URI to avoid async blocking in STA thread
|
||||
QString fileUri = QUrl::fromLocalFile(artPath).toString();
|
||||
auto thumbnailStream = RandomAccessStreamReference::CreateFromUri(
|
||||
winrt::Windows::Foundation::Uri(winrt::hstring(fileUri.toStdWString()))
|
||||
);
|
||||
updater.Thumbnail(thumbnailStream);
|
||||
current_song_art_url_ = artPath;
|
||||
}
|
||||
catch (const hresult_error &e) {
|
||||
qLog(Debug) << "Failed to set album art:" << QString::fromWCharArray(e.message().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the display
|
||||
updater.Update();
|
||||
}
|
||||
catch (const hresult_error &e) {
|
||||
qLog(Warning) << "Failed to update metadata:" << QString::fromWCharArray(e.message().c_str());
|
||||
}
|
||||
}
|
||||
69
src/core/windowsmediacontroller.h
Normal file
69
src/core/windowsmediacontroller.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef WINDOWSMEDIACONTROLLER_H
|
||||
#define WINDOWSMEDIACONTROLLER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "engine/enginebase.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
|
||||
class Player;
|
||||
class PlaylistManager;
|
||||
class CurrentAlbumCoverLoader;
|
||||
class Song;
|
||||
|
||||
class WindowsMediaController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WindowsMediaController(HWND hwnd,
|
||||
const SharedPtr<Player> player,
|
||||
const SharedPtr<PlaylistManager> playlist_manager,
|
||||
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
|
||||
QObject *parent = nullptr);
|
||||
~WindowsMediaController() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result = AlbumCoverLoaderResult());
|
||||
void EngineStateChanged(EngineBase::State newState);
|
||||
void CurrentSongChanged(const Song &song);
|
||||
|
||||
private:
|
||||
void UpdatePlaybackStatus(EngineBase::State state);
|
||||
void UpdateMetadata(const Song &song, const QUrl &art_url);
|
||||
void SetupButtonHandlers();
|
||||
|
||||
private:
|
||||
const SharedPtr<Player> player_;
|
||||
const SharedPtr<PlaylistManager> playlist_manager_;
|
||||
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader_;
|
||||
void *smtc_; // Pointer to SystemMediaTransportControls (opaque to avoid WinRT headers in public header)
|
||||
QString current_song_art_url_;
|
||||
bool apartment_initialized_; // Track if we initialized the WinRT apartment
|
||||
};
|
||||
|
||||
#endif // WINDOWSMEDIACONTROLLER_H
|
||||
@@ -178,6 +178,7 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
|
||||
audiobin_(nullptr),
|
||||
audiosink_(nullptr),
|
||||
audioqueue_(nullptr),
|
||||
audioqueueconverter_(nullptr),
|
||||
volume_(nullptr),
|
||||
volume_sw_(nullptr),
|
||||
volume_fading_(nullptr),
|
||||
@@ -186,7 +187,6 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
|
||||
equalizer_(nullptr),
|
||||
equalizer_preamp_(nullptr),
|
||||
eventprobe_(nullptr),
|
||||
bufferprobe_(nullptr),
|
||||
logged_unsupported_analyzer_format_(false),
|
||||
about_to_finish_(false),
|
||||
finish_requested_(false),
|
||||
@@ -436,7 +436,7 @@ void GstEnginePipeline::Disconnect() {
|
||||
}
|
||||
|
||||
if (buffer_probe_cb_id_.has_value()) {
|
||||
GstPad *pad = gst_element_get_static_pad(bufferprobe_, "src");
|
||||
GstPad *pad = gst_element_get_static_pad(audioqueueconverter_, "src");
|
||||
if (pad) {
|
||||
gst_pad_remove_probe(pad, buffer_probe_cb_id_.value());
|
||||
gst_object_unref(pad);
|
||||
@@ -674,13 +674,8 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GstElement *audioqueueconverter = CreateElement(u"audioconvert"_s, u"audioqueueconverter"_s, audiobin_, error);
|
||||
if (!audioqueueconverter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GstElement *audioqueueresampler = CreateElement(u"audioresample"_s, u"audioqueueresampler"_s, audiobin_, error);
|
||||
if (!audioqueueresampler) {
|
||||
audioqueueconverter_ = CreateElement(u"audioconvert"_s, u"audioqueueconverter"_s, audiobin_, error);
|
||||
if (!audioqueueconverter_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -689,11 +684,6 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GstElement *audiosinkresampler = CreateElement(u"audioresample"_s, u"audiosinkresampler"_s, audiobin_, error);
|
||||
if (!audiosinkresampler) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the volume element if it's enabled.
|
||||
if (volume_enabled_ && !volume_) {
|
||||
volume_sw_ = CreateElement(u"volume"_s, u"volume_sw"_s, audiobin_, error);
|
||||
@@ -771,8 +761,7 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
|
||||
|
||||
}
|
||||
|
||||
eventprobe_ = audioqueueconverter;
|
||||
bufferprobe_ = audioqueueconverter;
|
||||
eventprobe_ = audioqueueconverter_;
|
||||
|
||||
// Create the replaygain elements if it's enabled.
|
||||
GstElement *rgvolume = nullptr;
|
||||
@@ -858,17 +847,12 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
|
||||
|
||||
// Link all elements
|
||||
|
||||
if (!gst_element_link(audioqueue_, audioqueueconverter)) {
|
||||
if (!gst_element_link(audioqueue_, audioqueueconverter_)) {
|
||||
error = u"Failed to link audio queue to audio queue converter."_s;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!gst_element_link(audioqueueconverter, audioqueueresampler)) {
|
||||
error = u"Failed to link audio queue converter to audio queue resampler."_s;
|
||||
return false;
|
||||
}
|
||||
|
||||
GstElement *element_link = audioqueueresampler; // The next element to link from.
|
||||
GstElement *element_link = audioqueueconverter_; // The next element to link from.
|
||||
|
||||
// Link replaygain elements if enabled.
|
||||
if (rg_enabled_ && rgvolume && rglimiter && rgconverter) {
|
||||
@@ -944,11 +928,6 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!gst_element_link(audiosinkconverter, audiosinkresampler)) {
|
||||
error = "Failed to link audio sink converter to audio sink resampler."_L1;
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
|
||||
if (!caps) {
|
||||
@@ -959,16 +938,16 @@ bool GstEnginePipeline::InitAudioBin(QString &error) {
|
||||
qLog(Debug) << "Setting channels to" << channels_;
|
||||
gst_caps_set_simple(caps, "channels", G_TYPE_INT, channels_, nullptr);
|
||||
}
|
||||
const bool link_filtered_result = gst_element_link_filtered(audiosinkresampler, audiosink_, caps);
|
||||
const bool link_filtered_result = gst_element_link_filtered(audiosinkconverter, audiosink_, caps);
|
||||
gst_caps_unref(caps);
|
||||
if (!link_filtered_result) {
|
||||
error = "Failed to link audio sink resampler to audio sink with filter for "_L1 + output_;
|
||||
error = "Failed to link audio sink converter to audio sink with filter for "_L1 + output_;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
{ // Add probes and handlers.
|
||||
GstPad *pad = gst_element_get_static_pad(bufferprobe_, "src");
|
||||
GstPad *pad = gst_element_get_static_pad(audioqueueconverter_, "src");
|
||||
if (pad) {
|
||||
buffer_probe_cb_id_ = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, BufferProbeCallback, this, nullptr);
|
||||
gst_object_unref(pad);
|
||||
|
||||
@@ -355,6 +355,7 @@ class GstEnginePipeline : public QObject {
|
||||
GstElement *audiobin_;
|
||||
GstElement *audiosink_;
|
||||
GstElement *audioqueue_;
|
||||
GstElement *audioqueueconverter_;
|
||||
GstElement *volume_;
|
||||
GstElement *volume_sw_;
|
||||
GstElement *volume_fading_;
|
||||
@@ -363,7 +364,6 @@ class GstEnginePipeline : public QObject {
|
||||
GstElement *equalizer_;
|
||||
GstElement *equalizer_preamp_;
|
||||
GstElement *eventprobe_;
|
||||
GstElement *bufferprobe_;
|
||||
|
||||
std::optional<gulong> upstream_events_probe_cb_id_;
|
||||
std::optional<gulong> buffer_probe_cb_id_;
|
||||
|
||||
@@ -93,6 +93,10 @@
|
||||
# include "discord/richpresence.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_WINDOWS_MEDIA_CONTROLS
|
||||
# include "core/windowsmediacontroller.h"
|
||||
#endif
|
||||
|
||||
#include "core/iconloader.h"
|
||||
#include "core/commandlineoptions.h"
|
||||
#include "core/networkproxyfactory.h"
|
||||
@@ -365,6 +369,11 @@ int main(int argc, char *argv[]) {
|
||||
#endif
|
||||
options);
|
||||
|
||||
#ifdef HAVE_WINDOWS_MEDIA_CONTROLS
|
||||
// Initialize Windows Media Transport Controls
|
||||
WindowsMediaController windows_media_controller(reinterpret_cast<HWND>(w.winId()), app.player(), app.playlist_manager(), app.current_albumcover_loader());
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
mac::EnableFullScreen(w);
|
||||
#endif // Q_OS_MACOS
|
||||
|
||||
@@ -22,9 +22,6 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <glib-object.h>
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
|
||||
@@ -64,29 +61,6 @@ void MoodbarBuilder::Init(const int bands, const int rate_hz) {
|
||||
|
||||
}
|
||||
|
||||
void MoodbarBuilder::AddFrame(const GValue *magnitudes, const int size) {
|
||||
|
||||
if (size > barkband_table_.length()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate total magnitudes for different bark bands.
|
||||
double bands[sBarkBandCount]{};
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const GValue *magnitude = gst_value_list_get_value(magnitudes, i);
|
||||
bands[barkband_table_[i]] += g_value_get_float(magnitude);
|
||||
}
|
||||
|
||||
// Now divide the bark bands into thirds and compute their total amplitudes.
|
||||
double rgb[] = { 0, 0, 0 };
|
||||
for (int i = 0; i < sBarkBandCount; ++i) {
|
||||
rgb[(i * 3) / sBarkBandCount] += bands[i] * bands[i];
|
||||
}
|
||||
|
||||
frames_.append(Rgb(sqrt(rgb[0]), sqrt(rgb[1]), sqrt(rgb[2])));
|
||||
|
||||
}
|
||||
|
||||
void MoodbarBuilder::AddFrame(const double *magnitudes, const int size) {
|
||||
|
||||
if (size > barkband_table_.length()) {
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
#ifndef MOODBARBUILDER_H
|
||||
#define MOODBARBUILDER_H
|
||||
|
||||
#include <glib-object.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
@@ -33,7 +31,6 @@ class MoodbarBuilder {
|
||||
explicit MoodbarBuilder();
|
||||
|
||||
void Init(const int bands, const int rate_hz);
|
||||
void AddFrame(const GValue *magnitudes, const int size);
|
||||
void AddFrame(const double *magnitudes, const int size);
|
||||
QByteArray Finish(const int width);
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ void MoodbarPipeline::Start() {
|
||||
|
||||
GstElement *decodebin = CreateElement("uridecodebin");
|
||||
convert_element_ = CreateElement("audioconvert");
|
||||
GstElement *spectrum = CreateElement("spectrum");
|
||||
GstElement *spectrum = CreateElement("strawberry-fastspectrum");
|
||||
GstElement *fakesink = CreateElement("fakesink");
|
||||
|
||||
if (!decodebin || !convert_element_ || !spectrum || !fakesink) {
|
||||
@@ -122,7 +122,7 @@ void MoodbarPipeline::Start() {
|
||||
return;
|
||||
}
|
||||
|
||||
//builder_ = make_unique<MoodbarBuilder>();
|
||||
builder_ = make_unique<MoodbarBuilder>();
|
||||
|
||||
// Set properties
|
||||
|
||||
@@ -130,16 +130,8 @@ void MoodbarPipeline::Start() {
|
||||
g_object_set(decodebin, "uri", gst_url.constData(), nullptr);
|
||||
g_object_set(spectrum, "bands", kBands, nullptr);
|
||||
|
||||
//GstStrawberryFastSpectrum *fastspectrum = reinterpret_cast<GstStrawberryFastSpectrum*>(spectrum);
|
||||
//fastspectrum->output_callback = [this](double *magnitudes, const int size) { builder_->AddFrame(magnitudes, size); };
|
||||
|
||||
{
|
||||
GstPad *pad = gst_element_get_static_pad(fakesink, "src");
|
||||
if (pad) {
|
||||
buffer_probe_cb_id_ = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, BufferProbeCallback, this, nullptr);
|
||||
gst_object_unref(pad);
|
||||
}
|
||||
}
|
||||
GstStrawberryFastSpectrum *fastspectrum = reinterpret_cast<GstStrawberryFastSpectrum*>(spectrum);
|
||||
fastspectrum->output_callback = [this](double *magnitudes, const int size) { builder_->AddFrame(magnitudes, size); };
|
||||
|
||||
// Connect signals
|
||||
CHECKED_GCONNECT(decodebin, "pad-added", &NewPadCallback, this);
|
||||
@@ -220,20 +212,6 @@ GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus *bus, GstMessage *messag
|
||||
if (!instance->running_) return GST_BUS_PASS;
|
||||
|
||||
switch (GST_MESSAGE_TYPE(message)) {
|
||||
#if 0
|
||||
case GST_MESSAGE_ELEMENT:{
|
||||
const GstStructure *structure = gst_message_get_structure(message);
|
||||
const gchar *name = gst_structure_get_name(structure);
|
||||
if (strcmp(name, "spectrum") == 0) {
|
||||
const GValue *magnitudes = gst_structure_get_value(structure, "magnitude");
|
||||
if (instance->builder_) {
|
||||
instance->builder_->AddFrame(magnitudes, kBands);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
case GST_MESSAGE_EOS:
|
||||
instance->Stop(true);
|
||||
break;
|
||||
@@ -251,24 +229,6 @@ GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus *bus, GstMessage *messag
|
||||
|
||||
}
|
||||
|
||||
GstPadProbeReturn MoodbarPipeline::BufferProbeCallback(GstPad *pad, GstPadProbeInfo *info, gpointer self) {
|
||||
|
||||
Q_UNUSED(pad)
|
||||
|
||||
MoodbarPipeline *instance = reinterpret_cast<MoodbarPipeline*>(self);
|
||||
|
||||
GstBuffer *buffer = gst_pad_probe_info_get_buffer(info);
|
||||
|
||||
GstMapInfo map;
|
||||
gst_buffer_map(buffer, &map, GST_MAP_READ);
|
||||
instance->data_.append(reinterpret_cast<const char*>(map.data), map.size);
|
||||
gst_buffer_unmap(buffer, &map);
|
||||
gst_buffer_unref(buffer);
|
||||
|
||||
return GST_PAD_PROBE_OK;
|
||||
|
||||
}
|
||||
|
||||
void MoodbarPipeline::Stop(const bool success) {
|
||||
|
||||
running_ = false;
|
||||
|
||||
@@ -63,7 +63,6 @@ class MoodbarPipeline : public QObject {
|
||||
|
||||
static void NewPadCallback(GstElement *element, GstPad *pad, gpointer self);
|
||||
static GstBusSyncReply BusCallbackSync(GstBus *bus, GstMessage *message, gpointer self);
|
||||
static GstPadProbeReturn BufferProbeCallback(GstPad *pad, GstPadProbeInfo *info, gpointer self);
|
||||
|
||||
private:
|
||||
QUrl url_;
|
||||
@@ -75,7 +74,6 @@ class MoodbarPipeline : public QObject {
|
||||
bool success_;
|
||||
bool running_;
|
||||
QByteArray data_;
|
||||
gint buffer_probe_cb_id_;
|
||||
};
|
||||
|
||||
using MoodbarPipelinePtr = QSharedPointer<MoodbarPipeline>;
|
||||
|
||||
@@ -80,13 +80,12 @@ void SongLoaderInserter::Load(Playlist *destination, const int row, const bool p
|
||||
songs_ << loader->songs();
|
||||
playlist_name_ = loader->playlist_name();
|
||||
}
|
||||
|
||||
// Always check for errors, even on success (e.g., playlist parsed but some songs failed to load)
|
||||
const QStringList errors = loader->errors();
|
||||
for (const QString &error : errors) {
|
||||
Q_EMIT Error(error);
|
||||
else {
|
||||
const QStringList errors = loader->errors();
|
||||
for (const QString &error : errors) {
|
||||
Q_EMIT Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
delete loader;
|
||||
}
|
||||
|
||||
@@ -193,13 +192,11 @@ void SongLoaderInserter::AsyncLoad() {
|
||||
const SongLoader::Result result = loader->LoadFilenamesBlocking();
|
||||
task_manager_->SetTaskProgress(async_load_id, static_cast<quint64>(++async_progress));
|
||||
|
||||
// Always check for errors, even on success (e.g., playlist parsed but some songs failed to load)
|
||||
const QStringList errors = loader->errors();
|
||||
for (const QString &error : errors) {
|
||||
Q_EMIT Error(error);
|
||||
}
|
||||
|
||||
if (result == SongLoader::Result::Error) {
|
||||
const QStringList errors = loader->errors();
|
||||
for (const QString &error : errors) {
|
||||
Q_EMIT Error(error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,18 +112,10 @@ void ParserBase::LoadSong(const QString &filename_or_url, const qint64 beginning
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the file exists before trying to read it
|
||||
if (!QFile::exists(filename)) {
|
||||
qLog(Error) << "File does not exist:" << filename;
|
||||
Q_EMIT Error(tr("File %1 does not exist").arg(filename));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tagreader_client_) {
|
||||
const TagReaderResult result = tagreader_client_->ReadFileBlocking(filename, song);
|
||||
if (!result.success()) {
|
||||
qLog(Error) << "Could not read file" << filename << result.error_string();
|
||||
Q_EMIT Error(tr("Could not read file %1: %2").arg(filename, result.error_string()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5526,7 +5526,7 @@ Are you sure you want to continue?</source>
|
||||
<name>RadioParadiseService</name>
|
||||
<message>
|
||||
<source>Getting %1 channels</source>
|
||||
<translation>Получение каналов %1</translation>
|
||||
<translation>Получение %1 каналов</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -6191,7 +6191,7 @@ Are you sure you want to continue?</source>
|
||||
<name>SomaFMService</name>
|
||||
<message>
|
||||
<source>Getting %1 channels</source>
|
||||
<translation>Получение каналов %1</translation>
|
||||
<translation>Получение %1 каналов</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
|
||||
@@ -1187,7 +1187,7 @@ If there are no matches then it will use the largest image in the directory.</tr
|
||||
</message>
|
||||
<message>
|
||||
<source>Queue to play next</source>
|
||||
<translation>Sıradaki yap</translation>
|
||||
<translation>Sıradaki Yap</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Search for this</source>
|
||||
@@ -3496,7 +3496,7 @@ If there are no matches then it will use the largest image in the directory.</tr
|
||||
</message>
|
||||
<message>
|
||||
<source>Queue to play next</source>
|
||||
<translation>Sıradaki yap</translation>
|
||||
<translation>Sıradaki Yap</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unskip track</source>
|
||||
@@ -4431,7 +4431,7 @@ If there are no matches then it will use the largest image in the directory.</tr
|
||||
</message>
|
||||
<message>
|
||||
<source>&Hide %1</source>
|
||||
<translation>&%1 ögesini sakla</translation>
|
||||
<translation>&%1'i sakla</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -6407,7 +6407,7 @@ Devam etmek istediğinizden emin misiniz?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Queue to play next</source>
|
||||
<translation>Sıradaki yap</translation>
|
||||
<translation>Sıradaki Yap</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Remove from favorites</source>
|
||||
|
||||
Reference in New Issue
Block a user