diff options
| author | Osmium Sorcerer <os@sof.beauty> | 2026-03-24 02:56:23 +0000 |
|---|---|---|
| committer | Osmium Sorcerer <os@sof.beauty> | 2026-03-29 22:22:25 +0000 |
| commit | 3f6fb17deddd1b366d16db5a2531c82407ced9db (patch) | |
| tree | 5dd78ee66a67398274d08f598b79f8b663791c1f /src/aomusicplayer.cpp | |
| parent | 8475cfb7ac19f8698d2b96a0367f4735a016c375 (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.cpp | 330 |
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()); } |
