Skip to content

Commit

Permalink
Use PlaySound on Windows for notification sounds
Browse files Browse the repository at this point in the history
Because on at least one Windows 7 system the Qt implementation causes a
crash, which is not acceptable for the application making a bloop noise.
The implentation is now split between Qt5, Qt6 and Windows, where the
latter uses PlaySound from Winmm. The function is loaded dynamically, in
case there's systems without that DLL or something, in which case we
just don't play back any sound. Since the function doesn't support
setting a volume, we load the WAV file into memory and manipulate its
samples to change their volume, which is a bit silly, but works.
  • Loading branch information
askmeaboutlo0m committed Jan 16, 2025
1 parent 23b52d5 commit 6f46b7f
Show file tree
Hide file tree
Showing 16 changed files with 429 additions and 62 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Unreleased Version 2.2.2-pre
* Fix: Adjust minimum resolution for desktop-screen mode to be a bit larger, to avoid getting a cut-off desktop UI on some phones.
* Fix: Don't cut off the bottom of dialogs on some Android phones. Thanks Anonymous, Bluestrings and Molderche for reporting.
* Fix: Work around Gaomon tablets reporting pen buttons as mouse inputs. They were getting ignored because of an earlier Huion bug workaround.
* Fix: Replace sound playback implementation on Windows to avoid mysterious crashes on some systems. Thanks Anonymous for reporting.

2024-11-06 Version 2.2.2-beta.4
* Fix: Solve rendering glitches with selection outlines that happen on some systems. Thanks xxxx for reporting.
Expand Down
14 changes: 14 additions & 0 deletions src/desktop/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ endif()
find_package(QtColorWidgets QUIET)
find_package(${QT_PACKAGE_NAME}Keychain QUIET)

add_compile_definitions(
WIN32_LEAN_AND_MEAN
NOMINMAX
)

set(gui_identifier net.drawpile.DrawpileClient)

if(ANDROID)
Expand Down Expand Up @@ -318,6 +323,7 @@ target_sources(drawpile PRIVATE
utils/qtguicompat.h
utils/recents.cpp
utils/recents.h
utils/soundplayer.h
utils/tabletfilter.h
utils/touchhandler.cpp
utils/touchhandler.h
Expand Down Expand Up @@ -496,6 +502,14 @@ if(WIN32 AND QT_VERSION_MAJOR VERSION_EQUAL 6)
"${Qt6Gui_PRIVATE_INCLUDE_DIRS}")
endif()

if(WIN32)
target_sources(drawpile PRIVATE utils/soundplayer_win32.cpp)
elseif(QT_VERSION_MAJOR VERSION_LESS 6)
target_sources(drawpile PRIVATE utils/soundplayer_qt5.cpp)
else()
target_sources(drawpile PRIVATE utils/soundplayer_qt6.cpp)
endif()

if(DISABLE_UPDATE_CHECK_DEFAULT)
target_compile_definitions(drawpile PRIVATE DISABLE_UPDATE_CHECK_DEFAULT=1)
endif()
Expand Down
1 change: 1 addition & 0 deletions src/desktop/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@ struct StartupOptions {
QString startPage;
bool singleSession = false;
bool restoreWindowPosition = false;
bool playTestSound = false;
QString autoRecordPath;
QString joinUrl;
QString openPath;
Expand Down
51 changes: 9 additions & 42 deletions src/desktop/notifications.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@ namespace notification {
Notifications::Notifications(QObject *parent)
: QObject(parent)
, m_lastSoundMsec(QDateTime::currentMSecsSinceEpoch())
, m_player(new QMediaPlayer(this))
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
, m_audioOutput(new QAudioOutput(m_player))
#endif
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
m_player->setAudioOutput(m_audioOutput);
#endif
}

void Notifications::preview(
Expand Down Expand Up @@ -154,22 +147,10 @@ bool Notifications::isFlashEnabled(

void Notifications::playSound(Event event, int volume)
{
Sound sound = getSound(event);
if(isSoundValid(sound)) {
m_player->stop();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
m_player->setSource(sound);
m_audioOutput->setVolume(qreal(volume) / 100.0);
#else
m_player->setMedia(sound);
m_player->setVolume(volume);
#endif
m_player->setPosition(0);
m_player->play();
}
m_soundPlayer.playSound(getSound(event), volume);
}

Notifications::Sound Notifications::getSound(Event event)
QString Notifications::getSound(Event event)
{
int key = int(event);
if(m_sounds.contains(key)) {
Expand Down Expand Up @@ -202,39 +183,25 @@ Notifications::Sound Notifications::getSound(Event event)

if(filename.isEmpty()) {
qWarning("Sound effect %d not defined", int(event));
m_sounds[key] = Sound();
return Sound();
m_sounds[key] = QString();
return QString();
}

QString path = utils::paths::locateDataFile(filename);
if(path.isEmpty()) {
qWarning("Sound file '%s' not found", qUtf8Printable(filename));
m_sounds[key] = Sound();
return Sound();
m_sounds[key] = QString();
return QString();
}

Sound sound(QUrl::fromLocalFile(path));
m_sounds[key] = sound;
return sound;
m_sounds[key] = path;
return path;
}
}

bool Notifications::isPlayerAvailable()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
return m_player->playbackState() != QMediaPlayer::PlayingState;
#else
return m_player->state() != QMediaPlayer::PlayingState;
#endif
}

bool Notifications::isSoundValid(const Sound &sound)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
return sound.isValid();
#else
return !sound.isNull();
#endif
return !m_soundPlayer.isPlaying();
}

bool Notifications::isHighPriority(Event event)
Expand Down
25 changes: 5 additions & 20 deletions src/desktop/notifications.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@
#define NOTIFICATIONS_H
#include <QHash>
#include <QObject>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
# include <QUrl>
#else
# include <QMediaContent>
#endif
#include <QUrl>
#include <desktop/utils/soundplayer.h>

class MainWindow;
class QAudioOutput;
class QMediaPlayer;
class QWidget;

namespace desktop {
Expand Down Expand Up @@ -41,12 +36,6 @@ class Notifications : public QObject {
void trigger(QWidget *widget, Event event, const QString &message);

private:
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
using Sound = QUrl;
#else
using Sound = QMediaContent;
#endif

static constexpr qint64 SOUND_DELAY_MSEC = 1500;

void notify(
Expand All @@ -64,19 +53,15 @@ class Notifications : public QObject {
isFlashEnabled(const desktop::settings::Settings &settings, Event event);

void playSound(Event event, int volume);
Sound getSound(Event event);
QString getSound(Event event);

bool isPlayerAvailable();
static bool isSoundValid(const Sound &sound);
static bool isHighPriority(Event event);
static bool isEmittedDuringCatchup(Event event);

qint64 m_lastSoundMsec;
QHash<int, Sound> m_sounds;
QMediaPlayer *m_player;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QAudioOutput *m_audioOutput;
#endif
QHash<int, QString> m_sounds;
SoundPlayer m_soundPlayer;
};

}
Expand Down
25 changes: 25 additions & 0 deletions src/desktop/utils/soundplayer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef DESKTOP_UTILS_SOUNDPLAYER_H
#define DESKTOP_UTILS_SOUNDPLAYER_H
#include "libshared/util/qtcompat.h"
#include <QString>

// Wrapper around different sound playing implementations. Qt5 and Qt6 work
// differently here, so they have separate implementations. On Windows, we use a
// custom implementation that uses PlaySound directly, since Qt's implementation
// is known to crash on at least one Windows 7 system, so this is safer.
class SoundPlayer final {
COMPAT_DISABLE_COPY_MOVE(SoundPlayer)
public:
SoundPlayer();
~SoundPlayer();

void playSound(const QString &path, int volume);
bool isPlaying() const;

private:
struct Private;
Private *d;
};

#endif
43 changes: 43 additions & 0 deletions src/desktop/utils/soundplayer_qt5.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "desktop/utils/soundplayer.h"
#include <QAudioOutput>
#include <QMediaContent>
#include <QMediaPlayer>
#include <QUrl>

struct SoundPlayer::Private {
QMediaPlayer *player = nullptr;
};

SoundPlayer::SoundPlayer()
: d(new Private)
{
}

SoundPlayer::~SoundPlayer()
{
delete d->player;
delete d;
}

void SoundPlayer::playSound(const QString &path, int volume)
{
if(!path.isEmpty()) {
QMediaContent media(QUrl::fromLocalFile(path));
if(!media.isNull()) {
if(!d->player) {
d->player = new QMediaPlayer;
}
d->player->stop();
d->player->setMedia(media);
d->player->setVolume(volume);
d->player->setPosition(0);
d->player->play();
}
}
}

bool SoundPlayer::isPlaying() const
{
return d->player && d->player->state() == QMediaPlayer::PlayingState;
}
47 changes: 47 additions & 0 deletions src/desktop/utils/soundplayer_qt6.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "desktop/utils/soundplayer.h"
#include <QAudioOutput>
#include <QMediaPlayer>
#include <QUrl>

struct SoundPlayer::Private {
QMediaPlayer *player = nullptr;
QAudioOutput *output = nullptr;
};

SoundPlayer::SoundPlayer()
: d(new Private)
{
}

SoundPlayer::~SoundPlayer()
{
delete d->output;
delete d->player;
delete d;
}

void SoundPlayer::playSound(const QString &path, int volume)
{
if(!path.isEmpty()) {
QUrl url = QUrl::fromLocalFile(path);
if(url.isValid()) {
if(!d->player) {
d->player = new QMediaPlayer;
d->output = new QAudioOutput(d->player);
d->player->setAudioOutput(d->output);
}
d->player->stop();
d->player->setSource(url);
d->output->setVolume(qreal(volume) / 100.0);
d->player->setPosition(0);
d->player->play();
}
}
}

bool SoundPlayer::isPlaying() const
{
return d->player &&
d->player->playbackState() == QMediaPlayer::PlayingState;
}
Loading

0 comments on commit 6f46b7f

Please sign in to comment.