#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 #include #include #include namespace { using namespace chatterino; void miniaudioLogCallback(void *userData, ma_uint32 level, const char *pMessage) { (void)userData; QString message{pMessage}; switch (level) { case MA_LOG_LEVEL_DEBUG: { qCDebug(chatterinoSound).noquote() << "ma debug: " << message.trimmed(); } break; case MA_LOG_LEVEL_INFO: { qCDebug(chatterinoSound).noquote() << "ma info: " << message.trimmed(); } break; case MA_LOG_LEVEL_WARNING: { qCWarning(chatterinoSound).noquote() << "ma warning:" << message.trimmed(); } break; case MA_LOG_LEVEL_ERROR: { qCWarning(chatterinoSound).noquote() << "ma error: " << message.trimmed(); } break; default: { qCWarning(chatterinoSound).noquote() << "ma unknown:" << message.trimmed(); } break; } } } // namespace 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()) , resourceManager(std::make_unique()) , engine(std::make_unique()) { } void SoundController::initialize(Settings &settings, Paths &paths) { (void)(settings); (void)(paths); ma_result result{}; // We are leaking this log object on purpose auto *logger = new ma_log; result = ma_log_init(nullptr, logger); if (result != MA_SUCCESS) { qCWarning(chatterinoSound) << "Error initializing logger:" << result; return; } result = ma_log_register_callback( logger, ma_log_callback_init(miniaudioLogCallback, nullptr)); if (result != MA_SUCCESS) { qCWarning(chatterinoSound) << "Error registering logger callback:" << result; return; } auto contextConfig = ma_context_config_init(); contextConfig.pLog = logger; /// Initialize context result = ma_context_init(nullptr, 0, &contextConfig, 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 if (!this->recreateDevice()) { qCWarning(chatterinoSound) << "Failed to create the initial device"; 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(); auto snd = std::make_unique(); 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()); if (this->device) { ma_device_uninit(this->device.get()); this->device.reset(); } 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; } auto deviceState = ma_device_get_state(this->device.get()); if (deviceState != ma_device_state_started) { // Device state is not as it should be, try to restart it qCWarning(chatterinoSound) << "Sound device was not started, attempting to restart it" << deviceState; auto result = ma_device_start(this->device.get()); if (result != MA_SUCCESS) { qCWarning(chatterinoSound) << "Failed to start the sound device" << result; if (!this->recreateDevice()) { qCWarning(chatterinoSound) << "Failed to recreate device"; return; } } qCInfo(chatterinoSound) << "Successfully restarted the sound device"; } 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; } } bool SoundController::recreateDevice() { ma_result result{}; if (this->device) { // Release the previous device first qCDebug(chatterinoSound) << "Uniniting previously created device"; ma_device_uninit(this->device.get()); } this->device = std::make_unique(); 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 false; } result = ma_device_start(this->device.get()); if (result != MA_SUCCESS) { qCWarning(chatterinoSound) << "Error starting device:" << result; return false; } return true; } } // namespace chatterino