mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Change sound backend from Qt to miniaudio (#4334)
Thanks Greenlandicsmiley, Nerixyz, Yoitsu, and helmak for helping debug & test this * Remove QMediaPlayer includes * Prefer local path when generating the sound path * Update changelog entry number * Disable pitch & spatialization control
This commit is contained in:
parent
adf58d2770
commit
4958d08036
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -35,3 +35,6 @@
|
||||||
[submodule "lib/googletest"]
|
[submodule "lib/googletest"]
|
||||||
path = lib/googletest
|
path = lib/googletest
|
||||||
url = https://github.com/google/googletest.git
|
url = https://github.com/google/googletest.git
|
||||||
|
[submodule "lib/miniaudio"]
|
||||||
|
path = lib/miniaudio
|
||||||
|
url = https://github.com/mackron/miniaudio.git
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
- Bugfix: Fixed the split "Search" menu action not opening the correct search window. (#4305)
|
- Bugfix: Fixed the split "Search" menu action not opening the correct search window. (#4305)
|
||||||
- Bugfix: Fixed an issue on Windows when opening links in incognito mode that contained forward slashes in hash (#4307)
|
- Bugfix: Fixed an issue on Windows when opening links in incognito mode that contained forward slashes in hash (#4307)
|
||||||
- Bugfix: Fixed an issue where beta versions wouldn't update to stable versions correctly. (#4329)
|
- Bugfix: Fixed an issue where beta versions wouldn't update to stable versions correctly. (#4329)
|
||||||
|
- Dev: Changed sound backend from Qt to miniaudio. (#4334)
|
||||||
- Dev: Remove protocol from QApplication's Organization Domain (so changed from `https://www.chatterino.com` to `chatterino.com`). (#4256)
|
- Dev: Remove protocol from QApplication's Organization Domain (so changed from `https://www.chatterino.com` to `chatterino.com`). (#4256)
|
||||||
- Dev: Ignore `WM_SHOWWINDOW` hide events, causing fewer attempted rescales. (#4198)
|
- Dev: Ignore `WM_SHOWWINDOW` hide events, causing fewer attempted rescales. (#4198)
|
||||||
- Dev: Migrated to C++ 20 (#4252, #4257)
|
- Dev: Migrated to C++ 20 (#4252, #4257)
|
||||||
|
|
1
lib/miniaudio
Submodule
1
lib/miniaudio
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit c153a947919808419b0bf3f56b6f2ee606d6c5f4
|
16
resources/licenses/miniaudio.txt
Normal file
16
resources/licenses/miniaudio.txt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
Copyright 2020 David Reid
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -10,6 +10,7 @@
|
||||||
#include "controllers/hotkeys/HotkeyController.hpp"
|
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||||
#include "controllers/ignores/IgnoreController.hpp"
|
#include "controllers/ignores/IgnoreController.hpp"
|
||||||
#include "controllers/notifications/NotificationController.hpp"
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
|
#include "controllers/sound/SoundController.hpp"
|
||||||
#include "controllers/userdata/UserDataController.hpp"
|
#include "controllers/userdata/UserDataController.hpp"
|
||||||
#include "debug/AssertInGuiThread.hpp"
|
#include "debug/AssertInGuiThread.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
#include "widgets/splits/Split.hpp"
|
#include "widgets/splits/Split.hpp"
|
||||||
#include "widgets/Window.hpp"
|
#include "widgets/Window.hpp"
|
||||||
|
|
||||||
|
#include <miniaudio.h>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
@ -82,6 +84,7 @@ Application::Application(Settings &_settings, Paths &_paths)
|
||||||
, ffzBadges(&this->emplace<FfzBadges>())
|
, ffzBadges(&this->emplace<FfzBadges>())
|
||||||
, seventvBadges(&this->emplace<SeventvBadges>())
|
, seventvBadges(&this->emplace<SeventvBadges>())
|
||||||
, userData(&this->emplace<UserDataController>())
|
, userData(&this->emplace<UserDataController>())
|
||||||
|
, sound(&this->emplace<SoundController>())
|
||||||
, logging(&this->emplace<Logging>())
|
, logging(&this->emplace<Logging>())
|
||||||
{
|
{
|
||||||
this->instance = this;
|
this->instance = this;
|
||||||
|
|
|
@ -19,6 +19,7 @@ class HighlightController;
|
||||||
class HotkeyController;
|
class HotkeyController;
|
||||||
class IUserDataController;
|
class IUserDataController;
|
||||||
class UserDataController;
|
class UserDataController;
|
||||||
|
class SoundController;
|
||||||
|
|
||||||
class Theme;
|
class Theme;
|
||||||
class WindowManager;
|
class WindowManager;
|
||||||
|
@ -92,6 +93,7 @@ public:
|
||||||
FfzBadges *const ffzBadges{};
|
FfzBadges *const ffzBadges{};
|
||||||
SeventvBadges *const seventvBadges{};
|
SeventvBadges *const seventvBadges{};
|
||||||
UserDataController *const userData{};
|
UserDataController *const userData{};
|
||||||
|
SoundController *const sound{};
|
||||||
|
|
||||||
/*[[deprecated]]*/ Logging *const logging{};
|
/*[[deprecated]]*/ Logging *const logging{};
|
||||||
|
|
||||||
|
|
|
@ -140,6 +140,9 @@ set(SOURCE_FILES
|
||||||
controllers/userdata/UserDataController.hpp
|
controllers/userdata/UserDataController.hpp
|
||||||
controllers/userdata/UserData.hpp
|
controllers/userdata/UserData.hpp
|
||||||
|
|
||||||
|
controllers/sound/SoundController.cpp
|
||||||
|
controllers/sound/SoundController.hpp
|
||||||
|
|
||||||
debug/Benchmark.cpp
|
debug/Benchmark.cpp
|
||||||
debug/Benchmark.hpp
|
debug/Benchmark.hpp
|
||||||
|
|
||||||
|
@ -771,6 +774,17 @@ target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
# semver dependency https://github.com/Neargye/semver
|
# semver dependency https://github.com/Neargye/semver
|
||||||
target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/semver/include)
|
target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/semver/include)
|
||||||
|
|
||||||
|
# miniaudio dependency https://github.com/mackron/miniaudio
|
||||||
|
target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/miniaudio)
|
||||||
|
|
||||||
|
if (UNIX)
|
||||||
|
if (CMAKE_DL_LIBS)
|
||||||
|
# libdl is a requirement for miniaudio on Linux
|
||||||
|
message(STATUS "Linking with CMake DL libs: '${CMAKE_DL_LIBS}'")
|
||||||
|
target_link_libraries(${LIBRARY_PROJECT} PUBLIC ${CMAKE_DL_LIBS})
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (WinToast_FOUND)
|
if (WinToast_FOUND)
|
||||||
target_link_libraries(${LIBRARY_PROJECT}
|
target_link_libraries(${LIBRARY_PROJECT}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
|
|
@ -64,7 +64,6 @@
|
||||||
# include <QListView>
|
# include <QListView>
|
||||||
# include <QListWidget>
|
# include <QListWidget>
|
||||||
# include <QMap>
|
# include <QMap>
|
||||||
# include <QMediaPlayer>
|
|
||||||
# include <QMenu>
|
# include <QMenu>
|
||||||
# include <QMessageBox>
|
# include <QMessageBox>
|
||||||
# include <QMimeData>
|
# include <QMimeData>
|
||||||
|
|
|
@ -39,6 +39,7 @@ Q_LOGGING_CATEGORY(chatterinoSettings, "chatterino.settings", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoSeventv, "chatterino.seventv", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoSeventv, "chatterino.seventv", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoSeventvEventAPI, "chatterino.seventv.eventapi",
|
Q_LOGGING_CATEGORY(chatterinoSeventvEventAPI, "chatterino.seventv.eventapi",
|
||||||
logThreshold);
|
logThreshold);
|
||||||
|
Q_LOGGING_CATEGORY(chatterinoSound, "chatterino.sound", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode",
|
Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode",
|
||||||
logThreshold);
|
logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
|
||||||
|
|
|
@ -29,6 +29,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventv);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventv);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventvEventAPI);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventvEventAPI);
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoSound);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include "common/Outcome.hpp"
|
#include "common/Outcome.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "controllers/notifications/NotificationModel.hpp"
|
#include "controllers/notifications/NotificationModel.hpp"
|
||||||
|
#include "controllers/sound/SoundController.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
@ -21,7 +22,6 @@
|
||||||
|
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QMediaPlayer>
|
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
@ -97,25 +97,13 @@ void NotificationController::removeChannelNotification(
|
||||||
}
|
}
|
||||||
void NotificationController::playSound()
|
void NotificationController::playSound()
|
||||||
{
|
{
|
||||||
static auto player = new QMediaPlayer;
|
|
||||||
static QUrl currentPlayerUrl;
|
|
||||||
|
|
||||||
QUrl highlightSoundUrl =
|
QUrl highlightSoundUrl =
|
||||||
getSettings()->notificationCustomSound
|
getSettings()->notificationCustomSound
|
||||||
? QUrl::fromLocalFile(
|
? QUrl::fromLocalFile(
|
||||||
getSettings()->notificationPathSound.getValue())
|
getSettings()->notificationPathSound.getValue())
|
||||||
: QUrl("qrc:/sounds/ping2.wav");
|
: QUrl("qrc:/sounds/ping2.wav");
|
||||||
|
|
||||||
// Set media if the highlight sound url has changed, or if media is buffered
|
getApp()->sound->play(highlightSoundUrl);
|
||||||
if (currentPlayerUrl != highlightSoundUrl ||
|
|
||||||
player->mediaStatus() == QMediaPlayer::BufferedMedia)
|
|
||||||
{
|
|
||||||
player->setMedia(highlightSoundUrl);
|
|
||||||
|
|
||||||
currentPlayerUrl = highlightSoundUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
player->play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationModel *NotificationController::createModel(QObject *parent,
|
NotificationModel *NotificationController::createModel(QObject *parent,
|
||||||
|
|
216
src/controllers/sound/SoundController.cpp
Normal file
216
src/controllers/sound/SoundController.cpp
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
#include "controllers/sound/SoundController.hpp"
|
||||||
|
|
||||||
|
#include "common/QLogging.hpp"
|
||||||
|
#include "debug/Benchmark.hpp"
|
||||||
|
#include "singletons/Paths.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include <miniaudio.h>
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
// NUM_SOUNDS specifies how many simultaneous default ping sounds & decoders to create
|
||||||
|
constexpr const auto NUM_SOUNDS = 4;
|
||||||
|
|
||||||
|
SoundController::SoundController()
|
||||||
|
: context(std::make_unique<ma_context>())
|
||||||
|
, resourceManager(std::make_unique<ma_resource_manager>())
|
||||||
|
, device(std::make_unique<ma_device>())
|
||||||
|
, engine(std::make_unique<ma_engine>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoundController::initialize(Settings &settings, Paths &paths)
|
||||||
|
{
|
||||||
|
(void)(settings);
|
||||||
|
(void)(paths);
|
||||||
|
|
||||||
|
ma_result result{};
|
||||||
|
|
||||||
|
/// Initialize context
|
||||||
|
result = ma_context_init(nullptr, 0, nullptr, this->context.get());
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound) << "Error initializing context:" << result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize resource manager
|
||||||
|
auto resourceManagerConfig = ma_resource_manager_config_init();
|
||||||
|
resourceManagerConfig.decodedFormat = ma_format_f32;
|
||||||
|
// Use native channel count
|
||||||
|
resourceManagerConfig.decodedChannels = 0;
|
||||||
|
resourceManagerConfig.decodedSampleRate = 48000;
|
||||||
|
|
||||||
|
result = ma_resource_manager_init(&resourceManagerConfig,
|
||||||
|
this->resourceManager.get());
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound)
|
||||||
|
<< "Error initializing resource manager:" << result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load default sound
|
||||||
|
QFile defaultPingFile(":/sounds/ping2.wav");
|
||||||
|
if (!defaultPingFile.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound) << "Error loading default ping sound";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->defaultPingData = defaultPingFile.readAll();
|
||||||
|
|
||||||
|
/// Initialize a sound device
|
||||||
|
auto deviceConfig = ma_device_config_init(ma_device_type_playback);
|
||||||
|
deviceConfig.playback.pDeviceID = nullptr;
|
||||||
|
deviceConfig.playback.format = this->resourceManager->config.decodedFormat;
|
||||||
|
deviceConfig.playback.channels = 0;
|
||||||
|
deviceConfig.pulse.pStreamNamePlayback = "Chatterino MA";
|
||||||
|
deviceConfig.sampleRate = this->resourceManager->config.decodedSampleRate;
|
||||||
|
deviceConfig.dataCallback = ma_engine_data_callback_internal;
|
||||||
|
deviceConfig.pUserData = this->engine.get();
|
||||||
|
|
||||||
|
result =
|
||||||
|
ma_device_init(this->context.get(), &deviceConfig, this->device.get());
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound) << "Error initializing device:" << result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_device_start(this->device.get());
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound) << "Error starting device:" << result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize engine
|
||||||
|
auto engineConfig = ma_engine_config_init();
|
||||||
|
engineConfig.pResourceManager = this->resourceManager.get();
|
||||||
|
engineConfig.pDevice = this->device.get();
|
||||||
|
engineConfig.pContext = this->context.get();
|
||||||
|
|
||||||
|
result = ma_engine_init(&engineConfig, this->engine.get());
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound) << "Error initializing engine:" << result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize default ping sounds
|
||||||
|
{
|
||||||
|
// TODO: Can we optimize this?
|
||||||
|
BenchmarkGuard b("init sounds");
|
||||||
|
|
||||||
|
ma_uint32 soundFlags = 0;
|
||||||
|
// Decode the sound during loading instead of during playback
|
||||||
|
soundFlags |= MA_SOUND_FLAG_DECODE;
|
||||||
|
// Disable pitch control (we don't use it, so this saves some performance)
|
||||||
|
soundFlags |= MA_SOUND_FLAG_NO_PITCH;
|
||||||
|
// Disable spatialization control, this brings the volume up to "normal levels"
|
||||||
|
soundFlags |= MA_SOUND_FLAG_NO_SPATIALIZATION;
|
||||||
|
|
||||||
|
auto decoderConfig = ma_decoder_config_init(ma_format_f32, 0, 48000);
|
||||||
|
// This must match the encoding format of our default ping sound
|
||||||
|
decoderConfig.encodingFormat = ma_encoding_format_wav;
|
||||||
|
|
||||||
|
for (auto i = 0; i < NUM_SOUNDS; ++i)
|
||||||
|
{
|
||||||
|
auto dec = std::make_unique<ma_decoder>();
|
||||||
|
auto snd = std::make_unique<ma_sound>();
|
||||||
|
|
||||||
|
result = ma_decoder_init_memory(
|
||||||
|
(void *)this->defaultPingData.data(),
|
||||||
|
this->defaultPingData.size() * sizeof(char), &decoderConfig,
|
||||||
|
dec.get());
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound)
|
||||||
|
<< "Error initializing default ping decoder from memory:"
|
||||||
|
<< result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_sound_init_from_data_source(
|
||||||
|
this->engine.get(), dec.get(), soundFlags, nullptr, snd.get());
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound)
|
||||||
|
<< "Error initializing default sound from data source:"
|
||||||
|
<< result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->defaultPingDecoders.emplace_back(std::move(dec));
|
||||||
|
this->defaultPingSounds.emplace_back(std::move(snd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qCInfo(chatterinoSound) << "miniaudio sound system initialized";
|
||||||
|
|
||||||
|
this->initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundController::~SoundController()
|
||||||
|
{
|
||||||
|
// NOTE: This destructor is never called because the `runGui` function calls _exit before that happens
|
||||||
|
// I have manually called the destructor prior to _exit being called to ensure this logic is sound
|
||||||
|
|
||||||
|
for (const auto &snd : this->defaultPingSounds)
|
||||||
|
{
|
||||||
|
ma_sound_uninit(snd.get());
|
||||||
|
}
|
||||||
|
for (const auto &dec : this->defaultPingDecoders)
|
||||||
|
{
|
||||||
|
ma_decoder_uninit(dec.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_engine_uninit(this->engine.get());
|
||||||
|
ma_device_uninit(this->device.get());
|
||||||
|
ma_resource_manager_uninit(this->resourceManager.get());
|
||||||
|
ma_context_uninit(this->context.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoundController::play(const QUrl &sound)
|
||||||
|
{
|
||||||
|
static size_t i = 0;
|
||||||
|
|
||||||
|
this->tgPlay.guard();
|
||||||
|
|
||||||
|
if (!this->initialized)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound) << "Can't play sound, sound controller "
|
||||||
|
"didn't initialize correctly";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sound.isLocalFile())
|
||||||
|
{
|
||||||
|
auto soundPath = sound.toLocalFile();
|
||||||
|
auto result = ma_engine_play_sound(this->engine.get(),
|
||||||
|
qPrintable(soundPath), nullptr);
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound) << "Failed to play sound" << sound
|
||||||
|
<< soundPath << ":" << result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play default sound, loaded from our resources in the constructor
|
||||||
|
auto &snd = this->defaultPingSounds[++i % NUM_SOUNDS];
|
||||||
|
ma_sound_seek_to_pcm_frame(snd.get(), 0);
|
||||||
|
auto result = ma_sound_start(snd.get());
|
||||||
|
if (result != MA_SUCCESS)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoSound) << "Failed to play default ping" << result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
70
src/controllers/sound/SoundController.hpp
Normal file
70
src/controllers/sound/SoundController.hpp
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/Singleton.hpp"
|
||||||
|
#include "util/ThreadGuard.hpp"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct ma_engine;
|
||||||
|
struct ma_device;
|
||||||
|
struct ma_resource_manager;
|
||||||
|
struct ma_context;
|
||||||
|
struct ma_sound;
|
||||||
|
struct ma_decoder;
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Settings;
|
||||||
|
class Paths;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handles sound loading & playback
|
||||||
|
**/
|
||||||
|
class SoundController : public Singleton
|
||||||
|
{
|
||||||
|
SoundController();
|
||||||
|
|
||||||
|
void initialize(Settings &settings, Paths &paths) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
~SoundController() override;
|
||||||
|
|
||||||
|
// Play a sound from the given url
|
||||||
|
// If the url points to something that isn't a local file, it will play
|
||||||
|
// the default sound initialized in the initialize method
|
||||||
|
void play(const QUrl &sound);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Used for selecting & initializing an appropriate sound backend
|
||||||
|
std::unique_ptr<ma_context> context;
|
||||||
|
// Used for storing & reusing sounds to be played
|
||||||
|
std::unique_ptr<ma_resource_manager> resourceManager;
|
||||||
|
// The sound device we're playing sound into
|
||||||
|
std::unique_ptr<ma_device> device;
|
||||||
|
// The engine is a high-level API for playing sounds from paths in a simple & efficient-enough manner
|
||||||
|
std::unique_ptr<ma_engine> engine;
|
||||||
|
|
||||||
|
// Stores the data of our default ping sounds
|
||||||
|
QByteArray defaultPingData;
|
||||||
|
// Stores N decoders for simultaneous default ping playback.
|
||||||
|
// We can't use the engine API for this as this requires direct access to a custom data_source
|
||||||
|
std::vector<std::unique_ptr<ma_decoder>> defaultPingDecoders;
|
||||||
|
// Stores N sounds for simultaneous default ping playback
|
||||||
|
// We can't use the engine API for this as this requires direct access to a custom data_source
|
||||||
|
std::vector<std::unique_ptr<ma_sound>> defaultPingSounds;
|
||||||
|
|
||||||
|
// Thread guard for the play method
|
||||||
|
// Ensures play is only ever called from the same thread
|
||||||
|
ThreadGuard tgPlay;
|
||||||
|
|
||||||
|
bool initialized{false};
|
||||||
|
|
||||||
|
friend class Application;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -6,6 +6,7 @@
|
||||||
#include "controllers/ignores/IgnoreController.hpp"
|
#include "controllers/ignores/IgnoreController.hpp"
|
||||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||||
#include "controllers/nicknames/Nickname.hpp"
|
#include "controllers/nicknames/Nickname.hpp"
|
||||||
|
#include "controllers/sound/SoundController.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageElement.hpp"
|
#include "messages/MessageElement.hpp"
|
||||||
#include "providers/twitch/TwitchBadge.hpp"
|
#include "providers/twitch/TwitchBadge.hpp"
|
||||||
|
@ -16,34 +17,33 @@
|
||||||
#include "util/StreamerMode.hpp"
|
#include "util/StreamerMode.hpp"
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMediaPlayer>
|
|
||||||
|
|
||||||
namespace chatterino {
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
/**
|
using namespace chatterino;
|
||||||
|
|
||||||
|
/**
|
||||||
* Gets the default sound url if the user set one,
|
* Gets the default sound url if the user set one,
|
||||||
* or the chatterino default ping sound if no url is set.
|
* or the chatterino default ping sound if no url is set.
|
||||||
*/
|
*/
|
||||||
QUrl getFallbackHighlightSound()
|
QUrl getFallbackHighlightSound()
|
||||||
{
|
{
|
||||||
QString path = getSettings()->pathHighlightSound;
|
QString path = getSettings()->pathHighlightSound;
|
||||||
bool fileExists = !path.isEmpty() && QFileInfo::exists(path) &&
|
bool fileExists =
|
||||||
QFileInfo(path).isFile();
|
!path.isEmpty() && QFileInfo::exists(path) && QFileInfo(path).isFile();
|
||||||
|
|
||||||
if (fileExists)
|
if (fileExists)
|
||||||
{
|
{
|
||||||
return QUrl::fromLocalFile(path);
|
return QUrl::fromLocalFile(path);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
return QUrl("qrc:/sounds/ping2.wav");
|
return QUrl("qrc:/sounds/ping2.wav");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
SharedMessageBuilder::SharedMessageBuilder(
|
SharedMessageBuilder::SharedMessageBuilder(
|
||||||
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
||||||
const MessageParseArgs &_args)
|
const MessageParseArgs &_args)
|
||||||
|
@ -198,23 +198,8 @@ void SharedMessageBuilder::appendChannelName()
|
||||||
->setLink(link);
|
->setLink(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline QMediaPlayer *getPlayer()
|
|
||||||
{
|
|
||||||
if (isGuiThread())
|
|
||||||
{
|
|
||||||
static auto player = new QMediaPlayer;
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SharedMessageBuilder::triggerHighlights()
|
void SharedMessageBuilder::triggerHighlights()
|
||||||
{
|
{
|
||||||
static QUrl currentPlayerUrl;
|
|
||||||
|
|
||||||
if (isInStreamerMode() && getSettings()->streamerModeMuteMentions)
|
if (isInStreamerMode() && getSettings()->streamerModeMuteMentions)
|
||||||
{
|
{
|
||||||
// We are in streamer mode with muting mention sounds enabled. Do nothing.
|
// We are in streamer mode with muting mention sounds enabled. Do nothing.
|
||||||
|
@ -232,19 +217,7 @@ void SharedMessageBuilder::triggerHighlights()
|
||||||
|
|
||||||
if (this->highlightSound_ && resolveFocus)
|
if (this->highlightSound_ && resolveFocus)
|
||||||
{
|
{
|
||||||
if (auto player = getPlayer())
|
getApp()->sound->play(this->highlightSoundUrl_);
|
||||||
{
|
|
||||||
// Set media if the highlight sound url has changed, or if media is buffered
|
|
||||||
if (currentPlayerUrl != this->highlightSoundUrl_ ||
|
|
||||||
player->mediaStatus() == QMediaPlayer::BufferedMedia)
|
|
||||||
{
|
|
||||||
player->setMedia(this->highlightSoundUrl_);
|
|
||||||
|
|
||||||
currentPlayerUrl = this->highlightSoundUrl_;
|
|
||||||
}
|
|
||||||
|
|
||||||
player->play();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->highlightAlert_)
|
if (this->highlightAlert_)
|
||||||
|
|
|
@ -35,7 +35,6 @@
|
||||||
#include <boost/variant.hpp>
|
#include <boost/variant.hpp>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QMediaPlayer>
|
|
||||||
#include <QStringRef>
|
#include <QStringRef>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
|
@ -110,6 +110,9 @@ AboutPage::AboutPage()
|
||||||
addLicense(form.getElement(), "semver",
|
addLicense(form.getElement(), "semver",
|
||||||
"https://github.com/Neargye/semver",
|
"https://github.com/Neargye/semver",
|
||||||
":/licenses/semver.txt");
|
":/licenses/semver.txt");
|
||||||
|
addLicense(form.getElement(), "miniaudio",
|
||||||
|
"https://github.com/mackron/miniaudio",
|
||||||
|
":/licenses/miniaudio.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attributions
|
// Attributions
|
||||||
|
|
Loading…
Reference in a new issue