aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOsmium Sorcerer <os@sof.beauty>2026-03-24 02:56:23 +0000
committerOsmium Sorcerer <os@sof.beauty>2026-03-29 22:22:25 +0000
commit3f6fb17deddd1b366d16db5a2531c82407ced9db (patch)
tree5dd78ee66a67398274d08f598b79f8b663791c1f
parent8475cfb7ac19f8698d2b96a0367f4735a016c375 (diff)
Rewrite audio engine: replace BASS with miniaudio
SFX and blip players largely remain the same. For the music player, we now have to implement network streaming natively, we no longer have a convenient function that did everything for us. I introduced QNetworkRequest to download the stream in memory and signal when it's ready to be decoded and played back. The size is guarded to prevent the client from accidentally downloading terabytes of audio. Delete QFutureWatcher, we no longer need it for concurrency. miniaudio uses a separate audio thread. Network donwloads and communication with the track name display are handled by Qt signals. Also, delete an odd "music.txt" feature. Its purpose was specifying offsets for loops in a text file per track, but it remained obscure and unused in practice. Unsupported: - Large streams, including unbounded ones (radio). We'll need a ring buffer for that, and a mechanism to write to it from the network and feed it to the audio thread. - Effect flags: fade in, fade out, sync pos. Ignored. - Audio device selection.
-rw-r--r--CMakeLists.txt15
-rw-r--r--src/aoapplication.cpp92
-rw-r--r--src/aoapplication.h15
-rw-r--r--src/aoblipplayer.cpp47
-rw-r--r--src/aoblipplayer.h14
-rw-r--r--src/aomusicplayer.cpp330
-rw-r--r--src/aomusicplayer.h47
-rw-r--r--src/aosfxplayer.cpp63
-rw-r--r--src/aosfxplayer.h10
-rw-r--r--src/courtroom.cpp31
-rw-r--r--src/courtroom.h2
-rw-r--r--src/lobby.cpp2
-rw-r--r--src/networkmanager.cpp6
-rw-r--r--src/networkmanager.h2
-rw-r--r--src/widgets/aooptionsdialog.cpp32
-rw-r--r--src/widgets/aooptionsdialog.h1
16 files changed, 291 insertions, 418 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 49427365..c7700c93 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,6 +16,9 @@ option(AO_ENABLE_DISCORD_RPC "Enable Discord Rich Presence" OFF)
find_package(QT NAMES Qt6)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Network Widgets Concurrent WebSockets UiTools)
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(Vorbisfile REQUIRED IMPORTED_TARGET vorbisfile)
+pkg_check_modules(Opusfile REQUIRED IMPORTED_TARGET opusfile)
qt_add_executable(Attorney_Online
src/aoapplication.cpp
@@ -103,6 +106,12 @@ qt_add_executable(Attorney_Online
src/screenslidetimer.h src/screenslidetimer.cpp
src/moderation_functions.h src/moderation_functions.cpp
src/network/serverinfo.h src/network/serverinfo.cpp
+ third_party/miniaudio.h
+ third_party/miniaudio.c
+ third_party/miniaudio_libvorbis.h
+ third_party/miniaudio_libvorbis.c
+ third_party/miniaudio_libopus.h
+ third_party/miniaudio_libopus.c
)
if(CMAKE_BUILD_TYPE STREQUAL "Dev")
@@ -131,7 +140,7 @@ if(WIN32)
endif()
endif()
-target_include_directories(Attorney_Online PRIVATE src lib)
+target_include_directories(Attorney_Online PRIVATE src lib third_party)
target_link_directories(Attorney_Online PRIVATE lib)
target_link_libraries(Attorney_Online PRIVATE
Qt${QT_VERSION_MAJOR}::Core
@@ -141,8 +150,8 @@ target_link_libraries(Attorney_Online PRIVATE
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::WebSockets
Qt${QT_VERSION_MAJOR}::UiTools
- bass
- bassopus
+ PkgConfig::Vorbisfile
+ PkgConfig::Opusfile
)
if(AO_ENABLE_DISCORD_RPC)
diff --git a/src/aoapplication.cpp b/src/aoapplication.cpp
index 774816bb..ecfd97aa 100644
--- a/src/aoapplication.cpp
+++ b/src/aoapplication.cpp
@@ -26,6 +26,13 @@ AOApplication::AOApplication(QObject *parent)
message_handler_context = this;
original_message_handler = qInstallMessageHandler(message_handler);
+
+ mus_decoders[0] = ma_decoding_backend_libopus;
+ mus_decoders[1] = ma_decoding_backend_libvorbis;
+ mus_decoder_config = ma_decoder_config_init_default();
+ mus_decoder_config.pCustomBackendUserData = nullptr;
+ mus_decoder_config.customBackendCount = 2;
+ mus_decoder_config.ppCustomBackendVTables = mus_decoders;
}
AOApplication::~AOApplication()
@@ -175,24 +182,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.";
@@ -203,35 +192,33 @@ void AOApplication::server_connected()
courtroom_loaded = false;
}
-void AOApplication::initBASS()
+void AOApplication::initAudio()
{
- 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")
+ ma_context ctx;
+ if (ma_context_init(nullptr, 0, nullptr, &ctx) != MA_SUCCESS)
{
- BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, nullptr, nullptr);
- load_bass_plugins();
+ qCritical("Failed to initialize audio context.");
}
- else
+ // TODO: Support multiple devices
+
+ ma_resource_manager_config rm_config = ma_resource_manager_config_init();
+ rm_config.decodedFormat = ma_format_f32;
+ rm_config.decodedChannels = 2;
+ rm_config.decodedSampleRate = 48000;
+ ma_decoding_backend_vtable *decoders[] = {ma_decoding_backend_libopus, ma_decoding_backend_libvorbis};
+ rm_config.ppCustomDecodingBackendVTables = decoders;
+ rm_config.customDecodingBackendCount = sizeof(decoders) / sizeof(decoders[0]);
+ rm_config.pCustomDecodingBackendUserData = nullptr;
+ if (ma_resource_manager_init(&rm_config, &audio_rm) != MA_SUCCESS)
{
- for (a = 0; BASS_GetDeviceInfo(a, &info); a++)
- {
- if (Options::getInstance().audioOutputDevice() == info.name)
- {
- 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;
- }
- }
- BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, nullptr, nullptr);
- load_bass_plugins();
+ qCritical("Failed to initialize audio resource manager.");
+ }
+
+ ma_engine_config engine_config = ma_engine_config_init();
+ engine_config.pResourceManager = &audio_rm;
+ if (ma_engine_init(&engine_config, &audio_engine) != MA_SUCCESS)
+ {
+ qCritical("Failed to initialize audio engine.");
}
}
@@ -262,22 +249,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
diff --git a/src/aoapplication.h b/src/aoapplication.h
index 5e67fc86..3773f16e 100644
--- a/src/aoapplication.h
+++ b/src/aoapplication.h
@@ -7,7 +7,9 @@
#include "serverdata.h"
#include "widgets/aooptionsdialog.h"
-#include <bass.h>
+#include "miniaudio.h"
+#include "miniaudio_libvorbis.h"
+#include "miniaudio_libopus.h"
#include <QColor>
#include <QCryptographicHash>
@@ -330,19 +332,22 @@ public:
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();
+ void initAudio();
QElapsedTimer demo_timer;
DemoServer *demo_server = nullptr;
+ ma_engine audio_engine;
+ // Vorbis and Opus decoders.
+ ma_decoding_backend_vtable *mus_decoders[2];
+ ma_decoder_config mus_decoder_config;
+
private:
QVector<ServerInfo> server_list;
QHash<size_t, QString> asset_lookup_cache;
QHash<size_t, QString> dir_listing_cache;
QSet<size_t> dir_listing_exist_cache;
+ ma_resource_manager audio_rm;
public Q_SLOTS:
void server_connected();
diff --git a/src/aoblipplayer.cpp b/src/aoblipplayer.cpp
index 3a13d787..f6531f5c 100644
--- a/src/aoblipplayer.cpp
+++ b/src/aoblipplayer.cpp
@@ -4,9 +4,14 @@ AOBlipPlayer::AOBlipPlayer(AOApplication *ao_app)
: ao_app(ao_app)
{}
+AOBlipPlayer::~AOBlipPlayer()
+{
+ ma_sound_uninit(&m_stream[0]);
+}
+
void AOBlipPlayer::setVolume(int value)
{
- m_volume = value;
+ m_volume = value / 100.0f;
updateInternalVolume();
}
@@ -18,37 +23,37 @@ void AOBlipPlayer::setMuted(bool enabled)
void AOBlipPlayer::setBlip(QString blip)
{
+ if (m_initialized)
+ {
+ ma_sound_uninit(&m_stream[0]);
+ m_initialized = false;
+ }
+ // ma_sound_init_copy?
QString path = ao_app->get_sfx_suffix(ao_app->get_sounds_path(blip));
- for (int i = 0; i < STREAM_COUNT; ++i)
+ ma_result r = ma_sound_init_from_file(&ao_app->audio_engine, qPrintable(path), MA_SOUND_FLAG_ASYNC | MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_NO_SPATIALIZATION | MA_SOUND_FLAG_NO_PITCH, nullptr, nullptr, &m_stream[0]);
+ if (r == MA_SUCCESS)
{
- 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_initialized = true;
+ updateInternalVolume();
+ }
+ else
+ {
+ qWarning() << "Failed to init blip" << path << "error" << r;
}
-
- 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;
+ if (m_initialized)
+ {
+ ma_sound_start(&m_stream[0]);
+ }
}
void AOBlipPlayer::updateInternalVolume()
{
- float volume = m_muted ? 0.0f : (m_volume * 0.01);
- for (int i = 0; i < STREAM_COUNT; ++i)
+ if (m_initialized)
{
- BASS_ChannelSetAttribute(m_stream[i], BASS_ATTRIB_VOL, volume);
+ ma_sound_set_volume(&m_stream[0], m_muted ? 0.0f : qBound(0.0f, m_volume, 1.0f));
}
}
diff --git a/src/aoblipplayer.h b/src/aoblipplayer.h
index 92b43d29..d85cc36f 100644
--- a/src/aoblipplayer.h
+++ b/src/aoblipplayer.h
@@ -2,9 +2,6 @@
#include "aoapplication.h"
-#include <bass.h>
-#include <bassopus.h>
-
#include <QDebug>
#include <QElapsedTimer>
#include <QWidget>
@@ -17,6 +14,7 @@ public:
static constexpr int STREAM_COUNT = 5;
AOBlipPlayer(AOApplication *ao_app);
+ ~AOBlipPlayer();
void setVolume(int value);
void setMuted(bool enabled);
@@ -25,13 +23,13 @@ public:
void playBlip();
+ void updateInternalVolume();
+
private:
AOApplication *ao_app;
- int m_volume = 0;
+ float m_volume = 0.0f;
bool m_muted = false;
- HSTREAM m_stream[STREAM_COUNT]{};
- int m_cycle = 0;
-
- void updateInternalVolume();
+ bool m_initialized = false;
+ ma_sound m_stream[STREAM_COUNT]{};
};
diff --git a/src/aomusicplayer.cpp b/src/aomusicplayer.cpp
index c4a562cb..766effac 100644
--- a/src/aomusicplayer.cpp
+++ b/src/aomusicplayer.cpp
@@ -1,12 +1,14 @@
#include "aomusicplayer.h"
#include "file_functions.h"
+#include "networkmanager.h"
#include "options.h"
-#include <bass.h>
-
#include <QDebug>
+#include <QFileInfo>
#include <QFuture>
+#include <QNetworkReply>
+#include <QNetworkRequest>
#include <QWidget>
AOMusicPlayer::AOMusicPlayer(AOApplication *ao_app)
@@ -15,271 +17,179 @@ AOMusicPlayer::AOMusicPlayer(AOApplication *ao_app)
AOMusicPlayer::~AOMusicPlayer()
{
- for (int n_stream = 0; n_stream < STREAM_COUNT; ++n_stream)
+ for (int i = 0; i < STREAM_COUNT; ++i)
{
- BASS_ChannelStop(m_stream_list[n_stream]);
+ stop(i);
}
}
-QString AOMusicPlayer::playStream(QString song, int streamId, bool loopEnabled, int effectFlags)
+void AOMusicPlayer::setMuted(bool enabled)
{
- if (!ensureValidStreamId(streamId))
+ m_muted = enabled;
+ for (int i = 0; i < STREAM_COUNT; ++i)
{
- return "[ERROR] Invalid Channel";
+ setStreamVolume(m_volume[i], i);
}
+}
- quint32 flags = BASS_STREAM_AUTOFREE;
- if (loopEnabled)
+void AOMusicPlayer::setStreamVolume(int value, int streamId)
+{
+ if (!ensureValidStreamId(streamId))
{
- flags |= BASS_SAMPLE_LOOP;
+ qWarning().noquote() << QObject::tr("Invalid stream ID '%2'").arg(streamId);
+ return;
}
- QString f_path = song;
- HSTREAM newstream;
- if (f_path.startsWith("http"))
- {
- if (!Options::getInstance().streamingEnabled())
- {
- BASS_ChannelStop(m_stream_list[streamId]);
- return QObject::tr("[MISSING] Streaming disabled.");
- }
- QUrl l_url = QUrl(f_path);
- newstream = BASS_StreamCreateURL(l_url.toEncoded().toStdString().c_str(), 0, flags, nullptr, 0);
- }
- else
- {
- flags |= BASS_STREAM_PRESCAN | BASS_UNICODE | BASS_ASYNCFILE;
+ m_volume[streamId] = m_muted ? 0.0f : qBound(0.0f, value / 100.0f, 1.0f);
+ ma_sound_set_volume(&m_stream[streamId].sound, m_volume[streamId]);
+}
- f_path = ao_app->get_real_path(ao_app->get_music_path(song));
- newstream = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, flags);
- }
+bool AOMusicPlayer::ensureValidStreamId(int streamId)
+{
+ return streamId >= 0 && streamId < STREAM_COUNT;
+}
- int error = BASS_ErrorGetCode();
- if (Options::getInstance().audioOutputDevice() != "default")
+void AOMusicPlayer::play_from_url(const QString &url, int id, bool looping, int effect_flags)
+{
+ auto &s = m_stream[id];
+ stop(id);
+ if (!Options::getInstance().streamingEnabled())
{
- BASS_ChannelSetDevice(m_stream_list[streamId], BASS_GetDevice());
+ emit track_ready(QStringLiteral("[DISABLED] Streaming disabled"));
+ return;
}
- m_loop_start[streamId] = 0;
- m_loop_end[streamId] = 0;
+ // Reusing network manager that already exists in ao_app.
+ s.reply = ao_app->net_manager->get_audio_url(QUrl(url));
- QString d_path = f_path + ".txt";
- if (loopEnabled && file_exists(d_path)) // Contains loop/etc. information file
+ QVariant len = s.reply->header(QNetworkRequest::ContentLengthHeader);
+ bool ok = false;
+ qint64 content_length = len.toLongLong(&ok);
+ static const qint64 max_stream_size = 256 * 1024 * 1024;
+ if (ok && content_length > max_stream_size)
{
- QStringList lines = ao_app->read_file(d_path).split("\n");
- bool seconds_mode = false;
- foreach (QString line, lines)
- {
- QStringList args = line.split("=");
- if (args.size() < 2)
- {
- continue;
- }
- QString arg = args[0].trimmed();
- if (arg == "seconds")
- {
- if (args[1].trimmed() == "true")
- {
- seconds_mode = true; // Use new epic behavior
- continue;
- }
-
- continue;
- }
-
- float sample_rate;
- BASS_ChannelGetAttribute(newstream, BASS_ATTRIB_FREQ, &sample_rate);
-
- // Grab number of bytes for sample size
- int sample_size = 16 / 8;
-
- // number of channels (stereo/mono)
- int num_channels = 2;
-
- // Calculate the bytes for loop_start/loop_end to use with the sync proc
- QWORD bytes;
- if (seconds_mode)
- {
- bytes = BASS_ChannelSeconds2Bytes(newstream, args[1].trimmed().toDouble());
- }
- else
- {
- bytes = static_cast<QWORD>(args[1].trimmed().toUInt() * sample_size * num_channels);
- }
- if (arg == "loop_start")
- {
- m_loop_start[streamId] = bytes;
- }
- else if (arg == "loop_length")
- {
- m_loop_end[streamId] = m_loop_start[streamId] + bytes;
- }
- else if (arg == "loop_end")
- {
- m_loop_end[streamId] = bytes;
- }
- }
- qDebug() << "Found data file for song" << song << "length" << BASS_ChannelGetLength(newstream, BASS_POS_BYTE) << "loop start" << m_loop_start[streamId] << "loop end" << m_loop_end[streamId];
+ s.reply->abort();
+ s.reply->deleteLater();
+ emit track_ready(QStringLiteral("[ERROR] Stream exceeds maximum size, refusing to download"));
+ return;
}
-
- if (BASS_ChannelIsActive(m_stream_list[streamId]) == BASS_ACTIVE_PLAYING)
+ if (ok && content_length > 0)
{
- DWORD oldstream = m_stream_list[streamId];
-
- if (effectFlags & SYNC_POS)
+ m_stream[id].buffer.reserve(content_length);
+ }
+ connect(s.reply, &QIODevice::readyRead, this, [this, id] {
+ auto &s = m_stream[id];
+ QByteArray chunk = s.reply->readAll();
+ if (s.buffer.size() + chunk.size() > max_stream_size)
{
- BASS_ChannelLock(oldstream, true);
- // Sync it with the new sample
- BASS_ChannelSetPosition(newstream, BASS_ChannelGetPosition(oldstream, BASS_POS_BYTE), BASS_POS_BYTE);
- BASS_ChannelLock(oldstream, false);
+ s.reply->abort();
+ s.reply->deleteLater();
+ s.reply = nullptr;
+ s.buffer.clear();
+ emit track_ready(QStringLiteral("[ERROR] Attempted to download beyond buffer size, aborting"));
+ return;
}
+ s.buffer.append(chunk);
+ });
+ connect(s.reply, &QNetworkReply::finished, this, [this, id, looping] {
+ auto &s = m_stream[id];
+ on_url_download_finished(id, s.reply, looping);
+ });
+}
- if ((effectFlags & FADE_OUT) && m_volume[streamId] > 0)
- {
- // Fade out the other sample and stop it (due to -1)
- BASS_ChannelSlideAttribute(oldstream, BASS_ATTRIB_VOL | BASS_SLIDE_LOG, -1, 4000);
- }
- else
- {
- BASS_ChannelStop(oldstream); // Stop the sample since we don't need it anymore
- }
- }
- else
- {
- BASS_ChannelStop(m_stream_list[streamId]);
- }
+void AOMusicPlayer::on_url_download_finished(int id, QNetworkReply *reply, bool looping)
+{
+ auto &s = m_stream[id];
- m_stream_list[streamId] = newstream;
- BASS_ChannelPlay(newstream, false);
- if (effectFlags & FADE_IN)
+ if (reply->error() != QNetworkReply::NoError)
{
- // Fade in our sample
- BASS_ChannelSetAttribute(newstream, BASS_ATTRIB_VOL, 0);
- BASS_ChannelSlideAttribute(newstream, BASS_ATTRIB_VOL, static_cast<float>(m_volume[streamId] / 100.0f), 1000);
- }
- else
- {
- this->setStreamVolume(m_volume[streamId], streamId);
- }
-
- BASS_ChannelSetSync(newstream, BASS_SYNC_DEV_FAIL, 0, ao_app->BASSreset, 0);
-
- this->setStreamLooping(loopEnabled, streamId); // Have to do this here due to any
- // crossfading-related changes, etc.
-
- bool is_stop = (song == "~stop.mp3");
- QString p_song_clear = QUrl(song).fileName();
- p_song_clear = p_song_clear.left(p_song_clear.lastIndexOf('.'));
-
- if (is_stop && streamId == 0)
- { // don't send text on channels besides 0
- return QObject::tr("None");
- }
-
- if (error == BASS_ERROR_HANDLE)
- { // Cheap hack to see if file missing
- return QObject::tr("[MISSING] %1").arg(p_song_clear);
+ reply->deleteLater();
+ emit track_ready(QStringLiteral("[ERROR] Unable to stream audio due to network error"));
+ return;
}
- if (song.startsWith("http") && streamId == 0)
+ if (ma_decoder_init_memory(s.buffer.data(), s.buffer.size(), &ao_app->mus_decoder_config, &s.decoder) != MA_SUCCESS)
{
- return QObject::tr("[STREAM] %1").arg(p_song_clear);
+ reply->deleteLater();
+ emit track_ready(QStringLiteral("[ERROR] Invalid audio format (stream decoding failed)"));
+ return;
}
+ s.has_decoder = true;
- if (streamId == 0)
+ ma_uint32 flags = m_flags | (looping ? MA_SOUND_FLAG_LOOPING : 0);
+ if (ma_sound_init_from_data_source(&ao_app->audio_engine, &s.decoder, flags, nullptr, &s.sound) != MA_SUCCESS)
{
- return p_song_clear;
+ reply->deleteLater();
+ emit track_ready(QStringLiteral("[ERROR] Failed to initialize audio"));
+ return;
}
- return "";
+ start_playback(id);
+
+ QString track_path = QUrl(reply->url()).path();
+ reply->deleteLater();
+ emit track_ready(QString("[STREAM] %1").arg(QFileInfo(track_path).completeBaseName()));
}
-void AOMusicPlayer::setMuted(bool enabled)
+void AOMusicPlayer::start_playback(int id)
{
- m_muted = enabled;
- // Update all volume based on the mute setting
- for (int n_stream = 0; n_stream < STREAM_COUNT; ++n_stream)
- {
- setStreamVolume(m_volume[n_stream], n_stream);
- }
+ auto &s = m_stream[id];
+ ma_sound_set_volume(&s.sound, m_volume[id]);
+ ma_sound_start(&s.sound);
+ s.state = Stream::playing;
}
-void AOMusicPlayer::setStreamVolume(int value, int streamId)
+void AOMusicPlayer::stop(int id)
{
- if (!ensureValidStreamId(streamId))
+ auto &s = m_stream[id];
+
+ if (s.state == Stream::playing)
{
- qWarning().noquote() << QObject::tr("Invalid stream ID '%2'").arg(streamId);
- return;
+ ma_sound_stop(&s.sound);
+ ma_sound_uninit(&s.sound);
}
- m_volume[streamId] = value;
- // If muted, volume will always be 0
- float volume = (m_volume[streamId] / 100.0f) * !m_muted;
- if (streamId < 0)
+ if (s.has_decoder)
{
- for (int n_stream = 0; n_stream < STREAM_COUNT; ++n_stream)
- {
- BASS_ChannelSetAttribute(m_stream_list[n_stream], BASS_ATTRIB_VOL, volume);
- }
+ ma_decoder_uninit(&s.decoder);
+ s.has_decoder = false;
}
- else
+
+ if (s.reply)
{
- BASS_ChannelSetAttribute(m_stream_list[streamId], BASS_ATTRIB_VOL, volume);
+ s.reply->abort();
+ s.reply->deleteLater();
+ s.reply = nullptr;
}
-}
-void CALLBACK loopProc(HSYNC handle, DWORD channel, DWORD data, void *user)
-{
- Q_UNUSED(handle);
- Q_UNUSED(data);
- QWORD loop_start = *(static_cast<unsigned *>(user));
- BASS_ChannelLock(channel, true);
- BASS_ChannelSetPosition(channel, loop_start, BASS_POS_BYTE);
- BASS_ChannelLock(channel, false);
+ s.buffer.clear();
+ s.state = Stream::standby;
}
-void AOMusicPlayer::setStreamLooping(bool enabled, int streamId)
+void AOMusicPlayer::play_from_file(const QString &path, int id, bool looping, int effect_flags)
{
- if (!ensureValidStreamId(streamId))
- {
- qWarning().noquote() << QObject::tr("Invalid stream ID '%2'").arg(streamId);
- return;
- }
+ auto &s = m_stream[id];
+ stop(id);
+
+ ma_uint32 flags = m_flags | (looping ? MA_SOUND_FLAG_LOOPING : 0);
- if (!enabled)
+ bool is_stop = (path == "~stop.mp3");
+ // Temporary is_stop stub.
+ // Fades and effects are to be considered when implemented.
+ if (is_stop)
{
- if (BASS_ChannelFlags(m_stream_list[streamId], 0, 0) & BASS_SAMPLE_LOOP)
- {
- BASS_ChannelFlags(m_stream_list[streamId], 0,
- BASS_SAMPLE_LOOP); // remove the LOOP flag
- }
- BASS_ChannelRemoveSync(m_stream_list[streamId], m_loop_sync[streamId]);
- m_loop_sync[streamId] = 0;
+ emit track_ready(QStringLiteral("None"));
return;
}
- BASS_ChannelFlags(m_stream_list[streamId], BASS_SAMPLE_LOOP,
- BASS_SAMPLE_LOOP); // set the LOOP flag
- if (m_loop_sync[streamId] != 0)
+ QString f_path = ao_app->get_real_path(ao_app->get_music_path(path));
+ if (ma_sound_init_from_file(&ao_app->audio_engine, qPrintable(f_path), flags, nullptr, nullptr, &s.sound) != MA_SUCCESS)
{
- BASS_ChannelRemoveSync(m_stream_list[streamId],
- m_loop_sync[streamId]); // remove the sync
- m_loop_sync[streamId] = 0;
+ emit track_ready(QStringLiteral("[ERROR] Failed to initialize sound"));
+ return;
}
- if (m_loop_start[streamId] < m_loop_end[streamId])
- {
- // Loop when the endpoint is reached.
- m_loop_sync[streamId] = BASS_ChannelSetSync(m_stream_list[streamId], BASS_SYNC_POS | BASS_SYNC_MIXTIME, m_loop_end[streamId], loopProc, &m_loop_start[streamId]);
- }
- else
- {
- // Loop when the end of the file is reached.
- m_loop_sync[streamId] = BASS_ChannelSetSync(m_stream_list[streamId], BASS_SYNC_END | BASS_SYNC_MIXTIME, 0, loopProc, &m_loop_start[streamId]);
- }
-}
+ start_playback(id);
-bool AOMusicPlayer::ensureValidStreamId(int streamId)
-{
- return (streamId >= 0 && streamId < STREAM_COUNT);
+ emit track_ready(QFileInfo(f_path).completeBaseName());
}
diff --git a/src/aomusicplayer.h b/src/aomusicplayer.h
index 707d64ad..b45c22fe 100644
--- a/src/aomusicplayer.h
+++ b/src/aomusicplayer.h
@@ -2,17 +2,20 @@
#include "aoapplication.h"
-#include <QFutureWatcher>
+#include <QNetworkAccessManager>
+#include <QPointer>
-class AOMusicPlayer
+class AOMusicPlayer : public QObject
{
+ Q_OBJECT
+
public:
// 0 = music
// 1 = ambience
static constexpr int STREAM_COUNT = 2;
explicit AOMusicPlayer(AOApplication *ao_app);
- virtual ~AOMusicPlayer();
+ ~AOMusicPlayer();
void setMuted(bool enabled);
@@ -21,18 +24,46 @@ public:
void setStreamVolume(int value, int streamId);
void setStreamLooping(bool enabled, int streamId);
- QFutureWatcher<QString> m_watcher;
+ void play_from_url(const QString &url, int id, bool looping, int flags);
+ void play_from_file(const QString &path, int id, bool looping, int flags);
private:
+ struct Stream
+ {
+ ma_sound sound;
+ QByteArray buffer;
+ ma_decoder decoder;
+ // I've had use-after-free crashes on `stop` due to QNetworkReply's lifetime
+ // being unclear (specifically, calling `abort` on it). QPointer is a
+ // bandaid fix.
+ QPointer<QNetworkReply> reply;
+ bool has_decoder;
+ enum
+ {
+ standby,
+ playing,
+ } state = standby;
+ };
+
+ // Default flags for music: STREAM decodes incrementally in 2-second chunks,
+ // the other two disable 3D positioning and Doppler effect.
+ const ma_uint32 m_flags = MA_SOUND_FLAG_STREAM | MA_SOUND_FLAG_NO_SPATIALIZATION | MA_SOUND_FLAG_NO_PITCH;
+
AOApplication *ao_app;
+ float m_volume[STREAM_COUNT]{};
bool m_muted = false;
-
- int m_volume[STREAM_COUNT]{};
- HSTREAM m_stream_list[STREAM_COUNT]{};
- HSYNC m_loop_sync[STREAM_COUNT]{};
+ Stream m_stream[STREAM_COUNT]{};
+ ma_pcm_rb m_audio_ring;
quint32 m_loop_start[STREAM_COUNT]{};
quint32 m_loop_end[STREAM_COUNT]{};
bool ensureValidStreamId(int streamId);
+
+ void on_url_download_finished(int id, QNetworkReply *reply, bool looping);
+ void start_playback(int id);
+ void stop(int id);
+
+signals:
+ void track_ready(QString label);
};
diff --git a/src/aosfxplayer.cpp b/src/aosfxplayer.cpp
index ac3b8513..00fac678 100644
--- a/src/aosfxplayer.cpp
+++ b/src/aosfxplayer.cpp
@@ -6,14 +6,23 @@ AOSfxPlayer::AOSfxPlayer(AOApplication *ao_app)
: ao_app(ao_app)
{}
-int AOSfxPlayer::volume()
+AOSfxPlayer::~AOSfxPlayer()
{
- return m_volume;
+ for (int i = 0; i < STREAM_COUNT; ++i)
+ {
+ ma_sound_uninit(&m_stream[i]);
+ }
}
void AOSfxPlayer::setVolume(int value)
{
- m_volume = value;
+ m_volume = value / 100.0f;
+ updateInternalVolume();
+}
+
+void AOSfxPlayer::setMuted(bool enabled)
+{
+ m_muted = enabled;
updateInternalVolume();
}
@@ -21,7 +30,7 @@ void AOSfxPlayer::play(QString path)
{
for (int i = 0; i < STREAM_COUNT; ++i)
{
- if (BASS_ChannelIsActive(m_stream[i]) == BASS_ACTIVE_PLAYING)
+ if (ma_sound_is_playing(&m_stream[i]))
{
m_current_stream_id = (i + 1) % STREAM_COUNT;
}
@@ -31,21 +40,15 @@ void AOSfxPlayer::play(QString path)
break;
}
}
-
- if (path.endsWith(".opus"))
- {
- m_stream[m_current_stream_id] = BASS_OPUS_StreamCreateFile(FALSE, path.utf16(), 0, 0, BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE);
- }
- else
+ ma_sound_uninit(&m_stream[m_current_stream_id]);
+ if (ma_sound_init_from_file(&ao_app->audio_engine, qPrintable(path), MA_SOUND_FLAG_ASYNC | MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_NO_SPATIALIZATION | MA_SOUND_FLAG_NO_PITCH, nullptr, nullptr, &m_stream[m_current_stream_id]) != MA_SUCCESS)
{
- m_stream[m_current_stream_id] = BASS_StreamCreateFile(FALSE, path.utf16(), 0, 0, BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE);
+ return;
}
updateInternalVolume();
- BASS_ChannelSetDevice(m_stream[m_current_stream_id], BASS_GetDevice());
- BASS_ChannelPlay(m_stream[m_current_stream_id], false);
- BASS_ChannelSetSync(m_stream[m_current_stream_id], BASS_SYNC_DEV_FAIL, 0, ao_app->BASSreset, 0);
+ ma_sound_start(&m_stream[m_current_stream_id]);
}
void AOSfxPlayer::findAndPlaySfx(QString sfx)
@@ -81,7 +84,7 @@ void AOSfxPlayer::stopAllLoopingStream()
{
for (int i = 0; i < STREAM_COUNT; ++i)
{
- if (BASS_ChannelFlags(m_stream[i], 0, 0) & BASS_SAMPLE_LOOP)
+ if (ma_sound_is_looping(&m_stream[i]))
{
stop(i);
}
@@ -97,22 +100,14 @@ void AOSfxPlayer::stop(int streamId)
return;
}
- BASS_ChannelStop(m_stream[streamId]);
-}
-
-void AOSfxPlayer::setMuted(bool toggle)
-{
- m_muted = toggle;
- // Update the audio volume
- updateInternalVolume();
+ ma_sound_stop(&m_stream[streamId]);
}
void AOSfxPlayer::updateInternalVolume()
{
- float volume = m_muted ? 0.0f : (m_volume * 0.01);
for (int i = 0; i < STREAM_COUNT; ++i)
{
- BASS_ChannelSetAttribute(m_stream[i], BASS_ATTRIB_VOL, volume);
+ ma_sound_set_volume(&m_stream[i], m_muted ? 0.0f : qBound(0.0f, m_volume, 1.0f));
}
}
@@ -125,23 +120,7 @@ void AOSfxPlayer::setLooping(bool toggle, int streamId)
return;
}
- m_looping = toggle;
- if (BASS_ChannelFlags(m_stream[streamId], 0, 0) & BASS_SAMPLE_LOOP)
- {
- if (m_looping == false)
- {
- BASS_ChannelFlags(m_stream[streamId], 0,
- BASS_SAMPLE_LOOP); // remove the LOOP flag
- }
- }
- else
- {
- if (m_looping == true)
- {
- BASS_ChannelFlags(m_stream[streamId], BASS_SAMPLE_LOOP,
- BASS_SAMPLE_LOOP); // set the LOOP flag
- }
- }
+ ma_sound_set_looping(&m_stream[streamId], toggle);
}
int AOSfxPlayer::maybeFetchCurrentStreamId(int streamId)
diff --git a/src/aosfxplayer.h b/src/aosfxplayer.h
index 7669f0de..b86e00b8 100644
--- a/src/aosfxplayer.h
+++ b/src/aosfxplayer.h
@@ -2,9 +2,6 @@
#include "aoapplication.h"
-#include <bass.h>
-#include <bassopus.h>
-
#include <QDebug>
#include <QWidget>
@@ -14,8 +11,8 @@ public:
static constexpr int STREAM_COUNT = 5;
AOSfxPlayer(AOApplication *ao_app);
+ ~AOSfxPlayer();
- int volume();
void setVolume(int value);
void play(QString path);
@@ -33,10 +30,9 @@ public:
private:
AOApplication *ao_app;
- int m_volume = 0;
+ float m_volume = 0.0f;
bool m_muted = false;
- bool m_looping = true;
- HSTREAM m_stream[STREAM_COUNT]{};
+ ma_sound m_stream[STREAM_COUNT]{};
int m_current_stream_id = 0;
int maybeFetchCurrentStreamId(int streamId);
diff --git a/src/courtroom.cpp b/src/courtroom.cpp
index 7646794d..9d872a28 100644
--- a/src/courtroom.cpp
+++ b/src/courtroom.cpp
@@ -4,6 +4,7 @@
#include "moderation_functions.h"
#include "options.h"
+#include <QFileInfo>
#include <QtConcurrent/QtConcurrent>
// #define DEBUG_TRANSITION
@@ -16,7 +17,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app)
setWindowFlags((this->windowFlags() | Qt::CustomizeWindowHint) & ~Qt::WindowMaximizeButtonHint);
setObjectName("courtroom");
- ao_app->initBASS();
+ ao_app->initAudio();
keepalive_timer = new QTimer(this);
keepalive_timer->start(45000);
@@ -33,7 +34,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app)
music_player = new AOMusicPlayer(ao_app);
music_player->setMuted(true);
- connect(&music_player->m_watcher, &QFutureWatcher<QString>::finished, this, &Courtroom::update_ui_music_name, Qt::QueuedConnection);
+ connect(music_player, &AOMusicPlayer::track_ready, this, [this](QString label) { ui_music_name->setText(label); });
sfx_player = new AOSfxPlayer(ao_app);
sfx_player->setMuted(true);
@@ -538,6 +539,8 @@ Courtroom::~Courtroom()
delete sfx_player;
delete objection_player;
delete blip_player;
+
+ ma_engine_uninit(&ao_app->audio_engine);
}
void Courtroom::on_application_state_changed(Qt::ApplicationState state)
@@ -4720,8 +4723,7 @@ void Courtroom::handle_song(QStringList *p_contents)
int effect_flags = 0; // No effects by default - vanilla functionality
QString f_song = f_contents.at(0);
- QString f_song_clear = QUrl(f_song).fileName();
- f_song_clear = f_song_clear.left(f_song_clear.lastIndexOf('.'));
+ QString f_song_clear = QFileInfo(f_song).completeBaseName();
int n_char = f_contents.at(1).toInt(&ok);
if (!ok)
@@ -4791,24 +4793,17 @@ void Courtroom::handle_song(QStringList *p_contents)
{
// Current song UI only displays the song playing, not other channels.
// Any other music playing is irrelevant.
- if (music_player->m_watcher.isRunning())
- {
- music_player->m_watcher.cancel();
- }
- ui_music_name->setText(tr("[LOADING] %1").arg(f_song_clear));
+ ui_music_name->setText("[LOADING]");
}
- music_player->m_watcher.setFuture(QtConcurrent::run([=, this]() -> QString { return music_player->playStream(f_song, channel, looping, effect_flags); }));
-}
-
-void Courtroom::update_ui_music_name()
-{
- QString result = music_player->m_watcher.result();
- if (result.isEmpty())
+ if (f_song.startsWith("http"))
{
- return;
+ music_player->play_from_url(f_song, channel, looping, effect_flags);
+ }
+ else
+ {
+ music_player->play_from_file(f_song, channel, looping, effect_flags);
}
- ui_music_name->setText(result);
}
void Courtroom::handle_wtce(QString p_wtce, int variant)
diff --git a/src/courtroom.h b/src/courtroom.h
index dc8cdf3a..095d0ea1 100644
--- a/src/courtroom.h
+++ b/src/courtroom.h
@@ -818,8 +818,6 @@ public Q_SLOTS:
void on_reload_theme_clicked();
- void update_ui_music_name();
-
private Q_SLOTS:
void start_chat_ticking();
void play_sfx();
diff --git a/src/lobby.cpp b/src/lobby.cpp
index 05dd9ce4..424f634b 100644
--- a/src/lobby.cpp
+++ b/src/lobby.cpp
@@ -302,7 +302,7 @@ void Lobby::on_about_clicked()
"is copyright (c) 2016-2022 Attorney Online developers. Open-source "
"licenses apply. All other assets are the property of their "
"respective owners."
- "<p>Running on Qt version %2 with the BASS audio engine.<br>"
+ "<p>Running on Qt version %2 with the miniaudio audio engine.<br>"
"APNG plugin loaded: %3"
"<p>Built on %4")
.arg(ao_app->get_version_string())
diff --git a/src/networkmanager.cpp b/src/networkmanager.cpp
index 1202fec8..02d5961e 100644
--- a/src/networkmanager.cpp
+++ b/src/networkmanager.cpp
@@ -204,3 +204,9 @@ void NetworkManager::handle_server_packet(AOPacket packet)
#endif
ao_app->server_packet_received(packet);
}
+
+QNetworkReply *NetworkManager::get_audio_url(const QUrl &url)
+{
+ QNetworkRequest req(url);
+ return http->get(req);
+}
diff --git a/src/networkmanager.h b/src/networkmanager.h
index 8f2d8d42..31b046d1 100644
--- a/src/networkmanager.h
+++ b/src/networkmanager.h
@@ -40,6 +40,8 @@ public Q_SLOTS:
void request_document(MSDocumentType document_type, const std::function<void(QString)> &cb);
void send_heartbeat();
+ QNetworkReply *get_audio_url(const QUrl &url);
+
Q_SIGNALS:
void server_connected(bool state);
diff --git a/src/widgets/aooptionsdialog.cpp b/src/widgets/aooptionsdialog.cpp
index 81d5eb70..eeab53b2 100644
--- a/src/widgets/aooptionsdialog.cpp
+++ b/src/widgets/aooptionsdialog.cpp
@@ -7,7 +7,6 @@
#include "networkmanager.h"
#include "options.h"
-#include <bass.h>
#include <QCollator>
#include <QDoubleSpinBox>
@@ -26,16 +25,8 @@ AOOptionsDialog::AOOptionsDialog(AOApplication *p_ao_app, QWidget *parent)
void AOOptionsDialog::populateAudioDevices()
{
ui_audio_device_combobox->clear();
- if (needsDefaultAudioDevice())
- {
- ui_audio_device_combobox->addItem("default", "default");
- }
-
- BASS_DEVICEINFO info;
- for (int a = 0; BASS_GetDeviceInfo(a, &info); a++)
- {
- ui_audio_device_combobox->addItem(info.name, info.name);
- }
+ ui_audio_device_combobox->addItem("Device selection not implemented");
+ ui_audio_device_combobox->setEnabled(false);
}
template <>
@@ -621,22 +612,3 @@ void AOOptionsDialog::timestampCbChanged(int state)
{
ui_log_timestamp_format_combobox->setDisabled(state == 0);
}
-
-#if (defined(_WIN32) || defined(_WIN64))
-bool AOOptionsDialog::needsDefaultAudioDevice()
-{
- return true;
-}
-#elif (defined(LINUX) || defined(__linux__))
-bool AOOptionsDialog::needsDefaultAudioDevice()
-{
- return false;
-}
-#elif defined __APPLE__
-bool AOOptionsDialog::needsDefaultAudioDevice()
-{
- return true;
-}
-#else
-#error This operating system is not supported.
-#endif
diff --git a/src/widgets/aooptionsdialog.h b/src/widgets/aooptionsdialog.h
index b89607dc..ab59916b 100644
--- a/src/widgets/aooptionsdialog.h
+++ b/src/widgets/aooptionsdialog.h
@@ -125,7 +125,6 @@ private:
bool asset_cache_dirty = false;
- bool needsDefaultAudioDevice();
void populateAudioDevices();
void updateValues();