Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1bb270f
Fix spurious "file does not exist in zip" error in configure.sh
OmniTroid Jun 23, 2026
2854068
minimum qt version 6.8
OmniTroid Jun 23, 2026
521dda2
shadow WrapOpenGL
OmniTroid Jun 23, 2026
decea55
Add FindWrapOpenGL.cmake override to actually shadow Qt's
OmniTroid Jun 23, 2026
8f2344d
Always re-fetch downloaded libs; skip Discord RPC on macOS arm64
OmniTroid Jun 23, 2026
735e253
Restore skip-if-already-present checks for downloaded libs
OmniTroid Jun 23, 2026
497c8d6
Add QT_PATH= CLI override to bypass Qt auto-detection
OmniTroid Jun 23, 2026
02165f4
Add Qt6::Multimedia to the build
OmniTroid Jun 23, 2026
e1e8712
Migrate AOBlipPlayer from BASS to QSoundEffect
OmniTroid Jun 23, 2026
5afbfc6
Migrate AOSfxPlayer from BASS to QSoundEffect
OmniTroid Jun 23, 2026
b71ecd3
Migrate AOMusicPlayer from BASS to QMediaPlayer + QAudioOutput
OmniTroid Jun 23, 2026
70e65b0
Populate audio output picker via QMediaDevices
OmniTroid Jun 23, 2026
70a79a4
Delete BASS plumbing; route players through QAudioDevice
OmniTroid Jun 23, 2026
6f7ac0b
Drop bass/bassopus from CMakeLists and configure.sh
OmniTroid Jun 23, 2026
9b70bc3
CI: have build.yml call configure.sh instead of duplicating deps work
OmniTroid Jun 23, 2026
0380367
Drop QT_ROOT= CLI override; derive it from QT_PATH
OmniTroid Jun 23, 2026
ca9fe6d
Apply clang-format to the migration touchpoints
OmniTroid Jun 23, 2026
1f074a6
configure.sh: don't trip set -e when bundled cmake is absent
OmniTroid Jun 23, 2026
0b65c38
CI Windows: install MinGW alongside Qt; tolerate missing Tools dir
OmniTroid Jun 23, 2026
1482a31
CI Windows: use MinGW 13.1 and stage runtime DLLs
OmniTroid Jun 23, 2026
2c14c8f
Add loop-sidecar parser unit tests + run ctest in CI
OmniTroid Jun 23, 2026
70d2af0
CI Linux: run tests under the offscreen QPA
OmniTroid Jun 23, 2026
d0ef7f4
configure.sh: drive the build via cmake --build --parallel
OmniTroid Jun 23, 2026
1afd16d
Revert "configure.sh: drive the build via cmake --build --parallel"
OmniTroid Jun 23, 2026
09e2544
Merge branch 'master' into qtmultimedia
OmniTroid Aug 24, 2026
e3b26fd
Revert build.yml
OmniTroid Aug 24, 2026
f928bf7
Merge branch 'master' into qtmultimedia
OmniTroid Aug 24, 2026
147456c
add qtmultimedia to CI
OmniTroid Aug 24, 2026
1251aae
Rename sidecar to metadata
OmniTroid Aug 24, 2026
ef7dbf1
update gitignore
OmniTroid Aug 24, 2026
64b288a
Change stream audio device if changed
OmniTroid Aug 24, 2026
e62e030
Merge branch 'master' into qtmultimedia
OmniTroid Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
tools: 'tools_mingw1310'
cache: true
cache-key-prefix: install-qt-action
modules: 'qtimageformats qtwebsockets'
modules: 'qtimageformats qtwebsockets qtmultimedia'

- name: Configure
shell: bash
Expand Down Expand Up @@ -86,7 +86,7 @@ jobs:
arch: 'gcc_64'
cache: true
cache-key-prefix: install-qt-action
modules: 'qtimageformats qtwebsockets'
modules: 'qtimageformats qtwebsockets qtmultimedia'

- name: Configure
env:
Expand Down Expand Up @@ -140,7 +140,7 @@ jobs:
arch: 'linux_gcc_arm64'
cache: true
cache-key-prefix: install-qt-action
modules: 'qtimageformats qtwebsockets'
modules: 'qtimageformats qtwebsockets qtmultimedia'

- name: Configure
env:
Expand Down Expand Up @@ -193,7 +193,7 @@ jobs:
arch: 'clang_64'
cache: true
cache-key-prefix: install-qt-action
modules: 'qtimageformats qtwebsockets'
modules: 'qtimageformats qtwebsockets qtmultimedia'

- name: Configure
env:
Expand Down
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ cmake_install.cmake
test/CMakeFiles/
test/CTestTestfile.cmake
test/cmake_install.cmake
test/test_aopacket_autogen/
test/test_aopacket
test/*_autogen/
test/test_*
!test/test_*.cpp
cmake_cmd.txt
build.env
7 changes: 4 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ option(AO_BUILD_TESTS "Build test programs" ON)
option(AO_ENABLE_DISCORD_RPC "Enable Discord Rich Presence" ON)

find_package(QT NAMES Qt6)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Network Widgets Concurrent WebSockets UiTools)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Network Widgets Concurrent WebSockets UiTools Multimedia)

qt_add_executable(AttorneyOnline
src/aoapplication.cpp
Expand Down Expand Up @@ -77,6 +77,8 @@ qt_add_executable(AttorneyOnline
src/hardware_functions.h
src/lobby.cpp
src/lobby.h
src/loopmetadata.cpp
src/loopmetadata.h
src/main.cpp
src/network/websocketconnection.cpp
src/network/websocketconnection.h
Expand Down Expand Up @@ -131,8 +133,7 @@ target_link_libraries(AttorneyOnline PRIVATE
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::WebSockets
Qt${QT_VERSION_MAJOR}::UiTools
bass
bassopus
Qt${QT_VERSION_MAJOR}::Multimedia
)

if(AO_ENABLE_DISCORD_RPC)
Expand Down
71 changes: 11 additions & 60 deletions src/aoapplication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "options.h"
#include "widgets/aooptionsdialog.h"

#include <QMediaDevices>

static QtMessageHandler original_message_handler;
static AOApplication *message_handler_context;

Expand Down Expand Up @@ -200,6 +202,7 @@ void AOApplication::call_settings_menu()
if (is_courtroom_constructed())
{
connect(l_dialog, &AOOptionsDialog::reloadThemeRequest, w_courtroom, &Courtroom::on_reload_theme_clicked);
connect(l_dialog, &AOOptionsDialog::audioDeviceChanged, w_courtroom, &Courtroom::on_audio_device_changed);
}

if (is_lobby_constructed())
Expand All @@ -214,24 +217,6 @@ void AOApplication::call_settings_menu()
delete l_dialog;
}

// Callback for when BASS device is lost
// Only actually used for music syncs
void CALLBACK AOApplication::BASSreset(HSTREAM handle, DWORD channel, DWORD data, void *user)
{
Q_UNUSED(handle);
Q_UNUSED(channel);
Q_UNUSED(data);
Q_UNUSED(user);
doBASSreset();
}

void AOApplication::doBASSreset()
{
BASS_Free();
BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, nullptr, nullptr);
load_bass_plugins();
}

void AOApplication::server_connected()
{
qInfo() << "Established connection to server.";
Expand All @@ -242,36 +227,21 @@ void AOApplication::server_connected()
courtroom_loaded = false;
}

void AOApplication::initBASS()
QAudioDevice AOApplication::currentAudioDevice() const
{
BASS_SetConfig(BASS_CONFIG_DEV_DEFAULT, 1);
BASS_Free();
// Change the default audio output device to be the one the user has given
// in his config.ini file for now.
unsigned int a = 0;
BASS_DEVICEINFO info;

if (Options::getInstance().audioOutputDevice() == "default")
QString pref = Options::getInstance().audioOutputDevice();
if (pref != "default")
{
BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, nullptr, nullptr);
load_bass_plugins();
}
else
{
for (a = 0; BASS_GetDeviceInfo(a, &info); a++)
const QList<QAudioDevice> devices = QMediaDevices::audioOutputs();
for (const QAudioDevice &dev : devices)
{
if (Options::getInstance().audioOutputDevice() == info.name)
if (dev.description() == pref)
{
BASS_SetDevice(a);
BASS_Init(static_cast<int>(a), 48000, BASS_DEVICE_LATENCY, nullptr, nullptr);
load_bass_plugins();
qInfo() << info.name << "was set as the default audio output device.";
return;
return dev;
}
}
BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, nullptr, nullptr);
load_bass_plugins();
}
return QMediaDevices::defaultAudioOutput();
}

bool AOApplication::pointExistsOnScreen(QPoint point)
Expand Down Expand Up @@ -301,22 +271,3 @@ void AOApplication::centerOrMoveWidgetOnPrimaryScreen(QWidget *widget)
widget->move(point->x(), point->y());
}
}

#if (defined(_WIN32) || defined(_WIN64))
void AOApplication::load_bass_plugins()
{
BASS_PluginLoad("bassopus.dll", 0);
}
#elif defined __APPLE__
void AOApplication::load_bass_plugins()
{
BASS_PluginLoad("libbassopus.dylib", 0);
}
#elif (defined(LINUX) || defined(__linux__))
void AOApplication::load_bass_plugins()
{
BASS_PluginLoad("libbassopus.so", 0);
}
#else
#error This operating system is unsupported for BASS plugins.
#endif
10 changes: 4 additions & 6 deletions src/aoapplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
#include "serverdata.h"
#include "widgets/aooptionsdialog.h"

#include <bass.h>

#include <QAudioDevice>
#include <QColor>
#include <QCryptographicHash>
#include <QDebug>
Expand Down Expand Up @@ -335,10 +334,9 @@ class AOApplication : public QObject
bool pointExistsOnScreen(QPoint point);
void centerOrMoveWidgetOnPrimaryScreen(QWidget *widget);

void initBASS();
static void load_bass_plugins();
static void CALLBACK BASSreset(HSTREAM handle, DWORD channel, DWORD data, void *user);
static void doBASSreset();
// Resolve the user-selected audio output device. Returns the default
// output if the preference is "default" or no match is found.
QAudioDevice currentAudioDevice() const;

QElapsedTimer demo_timer;
DemoServer *demo_server = nullptr;
Expand Down
140 changes: 123 additions & 17 deletions src/aoblipplayer.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
#include "aoblipplayer.h"

#include <QAudioBuffer>
#include <QAudioDecoder>
#include <QDir>
#include <QEventLoop>
#include <QFile>
#include <QHash>
#include <QMap>
#include <QUrl>

AOBlipPlayer::AOBlipPlayer(AOApplication *ao_app)
: ao_app(ao_app)
{}
Expand All @@ -19,36 +28,133 @@ void AOBlipPlayer::setMuted(bool enabled)
void AOBlipPlayer::setBlip(QString blip)
{
QString path = ao_app->get_sfx_suffix(ao_app->get_sounds_path(blip));
QString playable = resolveToPlayablePath(path);
QUrl source = QUrl::fromLocalFile(playable);
QAudioDevice device = ao_app->currentAudioDevice();
for (int i = 0; i < STREAM_COUNT; ++i)
{
BASS_StreamFree(m_stream[i]);

if (path.endsWith(".opus"))
{
m_stream[i] = BASS_OPUS_StreamCreateFile(FALSE, path.utf16(), 0, 0, BASS_UNICODE | BASS_ASYNCFILE);
}
else
{
m_stream[i] = BASS_StreamCreateFile(FALSE, path.utf16(), 0, 0, BASS_UNICODE | BASS_ASYNCFILE);
}
m_stream[i].setAudioDevice(device);
m_stream[i].setSource(source);
}

updateInternalVolume();
}

void AOBlipPlayer::playBlip()
{
HSTREAM stream = m_stream[m_cycle];
BASS_ChannelSetDevice(stream, BASS_GetDevice());
BASS_ChannelPlay(stream, false);
m_cycle = ++m_cycle % STREAM_COUNT;
m_stream[m_cycle].play();
m_cycle = (m_cycle + 1) % STREAM_COUNT;
}

void AOBlipPlayer::updateInternalVolume()
{
float volume = m_muted ? 0.0f : (m_volume * 0.01);
float volume = m_muted ? 0.0f : (m_volume * 0.01f);
for (int i = 0; i < STREAM_COUNT; ++i)
{
BASS_ChannelSetAttribute(m_stream[i], BASS_ATTRIB_VOL, volume);
m_stream[i].setVolume(volume);
}
}

namespace
{
// Standard 44-byte WAV header for PCM data.
void writeWavHeader(QFile &out, const QAudioFormat &fmt, qint32 pcmBytes)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we writing wav files

{
const quint32 sampleRate = static_cast<quint32>(fmt.sampleRate());
const quint16 channels = static_cast<quint16>(fmt.channelCount());
const quint16 bitsPerSample = static_cast<quint16>(fmt.bytesPerSample() * 8);
const quint32 byteRate = sampleRate * channels * (bitsPerSample / 8);
const quint16 blockAlign = channels * (bitsPerSample / 8);
const quint32 riffSize = 36 + static_cast<quint32>(pcmBytes);

auto write32 = [&](quint32 v) {
char b[4] = {char(v & 0xff), char((v >> 8) & 0xff), char((v >> 16) & 0xff), char((v >> 24) & 0xff)};
out.write(b, 4);
};
auto write16 = [&](quint16 v) {
char b[2] = {char(v & 0xff), char((v >> 8) & 0xff)};
out.write(b, 2);
};

out.write("RIFF", 4);
write32(riffSize);
out.write("WAVE", 4);
out.write("fmt ", 4);
write32(16);
write16(1); // PCM
write16(channels);
write32(sampleRate);
write32(byteRate);
write16(blockAlign);
write16(bitsPerSample);
out.write("data", 4);
write32(static_cast<quint32>(pcmBytes));
}
} // namespace

QString AOBlipPlayer::resolveToPlayablePath(const QString &sourcePath)
{
if (sourcePath.endsWith(".wav", Qt::CaseInsensitive))
{
return sourcePath;
}

static QMap<QString, QString> cache;
auto cached = cache.constFind(sourcePath);
if (cached != cache.constEnd())
{
return *cached;
}

QAudioDecoder decoder;
decoder.setSource(QUrl::fromLocalFile(sourcePath));

QByteArray pcm;
QAudioFormat format;
bool errored = false;

QEventLoop loop;
QObject::connect(&decoder, &QAudioDecoder::bufferReady, &loop, [&]() {
while (decoder.bufferAvailable())
{
QAudioBuffer buf = decoder.read();
if (!format.isValid())
{
format = buf.format();
}
pcm.append(buf.constData<char>(), buf.byteCount());
}
});
QObject::connect(&decoder, &QAudioDecoder::finished, &loop, &QEventLoop::quit);
QObject::connect(&decoder, qOverload<QAudioDecoder::Error>(&QAudioDecoder::error), &loop, [&](QAudioDecoder::Error) {
errored = true;
loop.quit();
});

decoder.start();
loop.exec();

if (errored || pcm.isEmpty() || !format.isValid())
{
// Decode failed; fall back to the original path so QSoundEffect at least logs a clear error.
cache.insert(sourcePath, sourcePath);
return sourcePath;
}

QString hash = QString::number(qHash(sourcePath), 16);
QDir cacheDir(QDir::current().absoluteFilePath("tmp/blip-cache"));
cacheDir.mkpath(".");
QString outPath = cacheDir.absoluteFilePath(hash + ".wav");

QFile out(outPath);
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
cache.insert(sourcePath, sourcePath);
return sourcePath;
}
writeWavHeader(out, format, pcm.size());
out.write(pcm);
out.close();

cache.insert(sourcePath, outPath);
return outPath;
}
Loading
Loading