Compare commits

..

8 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
d2afa8fd66 Fix STA thread blocking crash on album art loading
Replace async GetFileFromPathAsync().get() with synchronous CreateFromUri() to avoid blocking STA thread. This prevents the "sta thread blocking wait" assertion failure.

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-28 21:51:28 +00:00
copilot-swe-agent[bot]
4da4c9e267 Address code review feedback
- Fix album cover URL: use result.album_cover.cover_url instead of result.cover_url
- Convert WINDOWS_MEDIA_CONTROLS to optional_component for better configuration management

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-28 21:42:26 +00:00
copilot-swe-agent[bot]
02c1596ff4 Fix WinRT apartment initialization crash
Handle case where COM/WinRT apartment is already initialized by Qt or other components. Track whether we initialized the apartment to avoid uninitializing it if we didn't initialize it.

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-28 19:31:43 +00:00
copilot-swe-agent[bot]
597f983c92 Fix WinRT activation factory usage in WindowsMediaController
Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-28 19:46:21 +01:00
copilot-swe-agent[bot]
0e0117b19b Refactor WindowsMediaController with proper WinRT interop
Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-28 19:46:21 +01:00
copilot-swe-agent[bot]
4e0cc1c0da Add HAVE_WINDOWS_MEDIA_CONTROLS configuration flag
Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-28 19:46:21 +01:00
copilot-swe-agent[bot]
f77e92d634 Add Windows SystemMediaTransportControls support for MSVC
Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2025-12-28 19:46:21 +01:00
copilot-swe-agent[bot]
f25cdb3431 Initial plan 2025-12-28 19:46:21 +01:00
18 changed files with 424 additions and 162 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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()) {

View File

@@ -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);

View 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());
}
}

View 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

View File

@@ -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);

View File

@@ -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_;

View File

@@ -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

View File

@@ -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()) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>;

View File

@@ -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;
}

View File

@@ -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()));
}
}

View File

@@ -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>

View File

@@ -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>&amp;Hide %1</source>
<translation>&amp;%1 ögesini sakla</translation>
<translation>&amp;%1&apos;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>