diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/aoapplication.cpp | 92 | ||||
| -rw-r--r-- | src/aoapplication.h | 15 | ||||
| -rw-r--r-- | src/aoblipplayer.cpp | 47 | ||||
| -rw-r--r-- | src/aoblipplayer.h | 14 | ||||
| -rw-r--r-- | src/aomusicplayer.cpp | 330 | ||||
| -rw-r--r-- | src/aomusicplayer.h | 47 | ||||
| -rw-r--r-- | src/aosfxplayer.cpp | 63 | ||||
| -rw-r--r-- | src/aosfxplayer.h | 10 | ||||
| -rw-r--r-- | src/courtroom.cpp | 31 | ||||
| -rw-r--r-- | src/courtroom.h | 2 | ||||
| -rw-r--r-- | src/lobby.cpp | 2 | ||||
| -rw-r--r-- | src/networkmanager.cpp | 6 | ||||
| -rw-r--r-- | src/networkmanager.h | 2 | ||||
| -rw-r--r-- | src/widgets/aooptionsdialog.cpp | 32 | ||||
| -rw-r--r-- | src/widgets/aooptionsdialog.h | 1 |
15 files changed, 279 insertions, 415 deletions
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(); |
