Play snd0.at9 during the startup splash screen (#1545)

launcher: play snd0.at9 on title selection
This commit is contained in:
Felipe
2026-10-16 10:59:03 -03:00
committed by GitHub
parent 717b50155a
commit 584c2e8ca6
4 changed files with 154 additions and 2 deletions
+2 -1
View File
@@ -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)
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <QByteArray>
#include <QFutureWatcher>
#include <QObject>
#include <QPromise>
#include <QString>
#include <QThreadPool>
#include <vector>
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<float> pcm;
int channels = 0;
int rate = 0;
};
static Sound Decode(const QByteArray& data, const QPromise<Sound>& promise);
void Stop();
QThreadPool m_decode_pool;
QFutureWatcher<Sound> m_decode;
QString m_base;
SDL_AudioStream* m_stream = nullptr;
bool m_audio_initialized = false;
};
+10 -1
View File
@@ -6,6 +6,7 @@
#include "configurationListWidget.h"
#include "controllerPreview.h"
#include "gameContent.h"
#include "titleSound.h"
#include "updateChecker.h"
#include <QApplication>
@@ -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);
+106
View File
@@ -0,0 +1,106 @@
#include "titleSound.h"
#include "gameContent.h"
#include "libs/ajm/atrac9_decoder.h"
#include <QDebug>
#include <QtConcurrentRun>
#include <SDL3/SDL.h>
#include <array>
TitleSound::Sound TitleSound::Decode(const QByteArray& data, const QPromise<Sound>& promise) {
using namespace Libs::Audio::Ajm;
AjmAt9Decoder decoder(0, 0, AjmSampleEncoding::Float,
AJM_INSTANCE_FLAG_DEC_AT9_PARSE_RIFF_HEADER);
AjmGaplessState gapless;
std::array<float, 16 * 1024> batch;
Sound sound;
size_t offset = 0;
while (offset < static_cast<size_t>(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<int>(format.channel_num);
sound.rate = static_cast<int>(format.sampling_frequency);
return sound;
}
TitleSound::TitleSound(QObject* parent): QObject(parent) {
m_decode_pool.setMaxThreadCount(1);
connect(&m_decode, &QFutureWatcher<Sound>::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<int>(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<Sound>& 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;
}
}