aboutsummaryrefslogtreecommitdiff
path: root/src/aomusicplayer.cpp
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 /src/aomusicplayer.cpp
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.
Diffstat (limited to 'src/aomusicplayer.cpp')
-rw-r--r--src/aomusicplayer.cpp330
1 files changed, 120 insertions, 210 deletions
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());
}