diff --git a/src/launcher/CMakeLists.txt b/src/launcher/CMakeLists.txt index 4f0622a46..35a8bc18a 100644 --- a/src/launcher/CMakeLists.txt +++ b/src/launcher/CMakeLists.txt @@ -21,7 +21,8 @@ add_executable(launcher WIN32 ) -target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version common) +target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static + LibAtrac9 kyty_git_version common) if(LINUX) find_package(Qt6 COMPONENTS DBus REQUIRED) target_link_libraries(launcher Qt6::DBus) diff --git a/src/launcher/include/titleSound.h b/src/launcher/include/titleSound.h new file mode 100644 index 000000000..32448c67b --- /dev/null +++ b/src/launcher/include/titleSound.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +struct SDL_AudioStream; + +class TitleSound final: public QObject { +public: + explicit TitleSound(QObject* parent = nullptr); + ~TitleSound() override; + + void SetTitle(const QString& base); + +private: + struct Sound { + std::vector pcm; + int channels = 0; + int rate = 0; + }; + + static Sound Decode(const QByteArray& data, const QPromise& promise); + void Stop(); + + QThreadPool m_decode_pool; + QFutureWatcher m_decode; + QString m_base; + SDL_AudioStream* m_stream = nullptr; + bool m_audio_initialized = false; +}; diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index af6358671..93b6c27ff 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -6,6 +6,7 @@ #include "configurationListWidget.h" #include "controllerPreview.h" #include "gameContent.h" +#include "titleSound.h" #include "updateChecker.h" #include @@ -90,6 +91,7 @@ private: MainDialog* m_main_dialog = nullptr; UpdateChecker* m_update_checker = nullptr; ControllerPreview m_controller_preview; + TitleSound m_title_sound; QString m_interpreter; QProcess m_process; @@ -124,7 +126,13 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) { connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter, Qt::QueuedConnection); - connect(m_ui->widget, &ConfigurationListWidget::Select, this, &MainDialogPrivate::Update); + connect(m_ui->widget, &ConfigurationListWidget::Select, this, [this]() { + Update(); + const auto* item = m_ui->widget->GetSelectedItem(); + m_title_sound.SetTitle(m_process.state() == QProcess::NotRunning && item != nullptr + ? item->GetInfo().basedir + : QString()); + }); connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this, [this](const QString& color) { if (m_process.state() == QProcess::NotRunning) { @@ -530,6 +538,7 @@ void MainDialogPrivate::Run() { m_running_item->SetRunning(true); m_controller_preview.SetSuspended(true); + m_title_sound.SetTitle({}); auto info = m_ui->widget->CreateConfiguration(*m_running_item); m_main_dialog->RunInterpreter(&m_process, *info); diff --git a/src/launcher/src/titleSound.cpp b/src/launcher/src/titleSound.cpp new file mode 100644 index 000000000..5bb759155 --- /dev/null +++ b/src/launcher/src/titleSound.cpp @@ -0,0 +1,106 @@ +#include "titleSound.h" + +#include "gameContent.h" +#include "libs/ajm/atrac9_decoder.h" + +#include +#include + +#include +#include + +TitleSound::Sound TitleSound::Decode(const QByteArray& data, const QPromise& promise) { + using namespace Libs::Audio::Ajm; + AjmAt9Decoder decoder(0, 0, AjmSampleEncoding::Float, + AJM_INSTANCE_FLAG_DEC_AT9_PARSE_RIFF_HEADER); + AjmGaplessState gapless; + std::array batch; + Sound sound; + size_t offset = 0; + while (offset < static_cast(data.size()) && !gapless.IsEnd()) { + if (promise.isCanceled()) { + return {}; + } + const auto result = decoder.Decode(data.constData() + offset, data.size() - offset, + batch.data(), sizeof(batch), true, &gapless); + if ((result.result != 0 && result.result != AJM_RESULT_NOT_ENOUGH_ROOM) || + result.input_consumed == 0) { + return {}; + } + const auto samples = result.output_written / sizeof(float); + constexpr size_t MaxSamples = (128u << 20u) / sizeof(float); + if (samples > MaxSamples - sound.pcm.size()) { + return {}; + } + sound.pcm.insert(sound.pcm.end(), batch.begin(), batch.begin() + samples); + offset += result.input_consumed; + } + if (promise.isCanceled()) { + return {}; + } + const auto format = decoder.GetFormat(); + sound.channels = static_cast(format.channel_num); + sound.rate = static_cast(format.sampling_frequency); + return sound; +} + +TitleSound::TitleSound(QObject* parent): QObject(parent) { + m_decode_pool.setMaxThreadCount(1); + connect(&m_decode, &QFutureWatcher::finished, this, [this] { + if (m_decode.isCanceled() || m_decode.future().resultCount() == 0) { + return; + } + auto sound = m_decode.future().takeResult(); + if (sound.pcm.empty()) { + qWarning() << "Cannot decode title sound:" << m_base; + return; + } + m_audio_initialized = SDL_InitSubSystem(SDL_INIT_AUDIO); + if (m_audio_initialized) { + const SDL_AudioSpec spec {SDL_AUDIO_F32, sound.channels, sound.rate}; + m_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, + nullptr); + } + if (m_stream == nullptr || + !SDL_PutAudioStreamData(m_stream, sound.pcm.data(), + static_cast(sound.pcm.size() * sizeof(float))) || + !SDL_FlushAudioStream(m_stream) || !SDL_ResumeAudioStreamDevice(m_stream)) { + qWarning() << "Cannot play title sound:" << SDL_GetError(); + Stop(); + } + }); +} + +TitleSound::~TitleSound() { + m_decode.cancel(); + Stop(); +} + +void TitleSound::SetTitle(const QString& base) { + if (base == m_base) { + return; + } + m_decode.cancel(); + Stop(); + m_base = base; + if (!base.isEmpty()) { + m_decode.setFuture(QtConcurrent::run(&m_decode_pool, [base](QPromise& promise) { + const auto data = + GameContent::ReadFile(base, QStringLiteral("sce_sys/snd0.at9"), 32u << 20u); + if (!data.isEmpty() && !promise.isCanceled()) { + promise.addResult(Decode(data, promise)); + } + })); + } +} + +void TitleSound::Stop() { + if (m_stream != nullptr) { + SDL_DestroyAudioStream(m_stream); + m_stream = nullptr; + } + if (m_audio_initialized) { + SDL_QuitSubSystem(SDL_INIT_AUDIO); + m_audio_initialized = false; + } +}