Files
SubWave/src/engine/AudioEngine.cpp
T

202 lines
5.5 KiB
C++

#include "AudioEngine.h"
#include "engine/FFmpegDecoder.h"
#include "engine/EqProcessor.h"
#include "engine/SpectrumAnalyzer.h"
#include <cmath>
#include <numbers>
#include <algorithm>
#include <QThread>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
#include <QSemaphore>
#include <QMutexLocker>
AudioEngine::AudioEngine(QObject *parent) : QObject(parent) {
initEq();
}
AudioEngine::~AudioEngine() { stop(); }
AudioEngine::State AudioEngine::state() const { return m_state.load(); }
qint64 AudioEngine::positionFrames() const { return m_posFrames.load(); }
qint64 AudioEngine::totalFrames() const { return m_totalFrames.load(); }
int AudioEngine::sampleRate() const { return m_sampleRate.load(); }
void AudioEngine::setVolume(float v) {
m_volume = std::clamp(v, 0.f, 1.f);
if (m_sink) m_sink->setVolume(m_volume);
}
void AudioEngine::play(const Track &track) {
stop();
m_stopReq = false;
m_pauseReq = false;
m_seekPending = false;
m_track = track;
m_thread = QThread::create([this]{ playbackLoop(); });
m_thread->setObjectName("subwave-audio");
m_thread->start(QThread::HighestPriority);
}
void AudioEngine::togglePause() {
if (m_state == State::Playing) {
m_pauseReq = true;
m_state = State::Paused;
} else if (m_state == State::Paused) {
m_pauseReq = false;
m_state = State::Playing;
m_pauseCv.wakeAll();
}
}
void AudioEngine::stop() {
m_stopReq = true;
m_pauseReq = false;
m_pauseCv.wakeAll();
if (m_thread) {
m_thread->quit();
m_thread->wait(3000);
delete m_thread;
m_thread = nullptr;
}
m_state = State::Stopped;
m_posFrames = 0;
}
void AudioEngine::seekFraction(double fraction) {
m_seekFraction = std::clamp(fraction, 0.0, 1.0);
m_seekPending = true;
}
void AudioEngine::setEqBand(int band, float db) {
if (band < 0 || band >= EQ_BANDS) return;
m_eq.setBand(band, db);
}
float AudioEngine::eqBand(int band) const {
return m_eq.band(band);
}
void AudioEngine::initEq() {
for (int b = 0; b < EQ_BANDS; ++b)
m_eq.setBand(b, 0.f);
}
void AudioEngine::playbackLoop() {
m_state = State::Playing;
engine::FFmpegDecoder decoder;
if (!decoder.open(m_track.filePath)) {
qWarning() << "[AudioEngine] Cannot open:" << m_track.filePath;
m_state = State::Stopped;
return;
}
if (decoder.sampleRate() <= 0 || decoder.channels() <= 0) {
qWarning() << "[AudioEngine] Invalid stream format:" << decoder.sampleRate() << "Hz" << decoder.channels() << "channels";
m_state = State::Stopped;
return;
}
m_sampleRate = decoder.sampleRate();
m_eq.setSampleRate(m_sampleRate);
m_eq.reset();
if (m_track.durationSecs == 0 && decoder.durationSeconds() > 0) {
m_track.durationSecs = decoder.durationSeconds();
}
emit durationKnown(m_track.durationSecs);
m_totalFrames = decoder.totalFrames();
QAudioFormat af;
af.setSampleRate(m_sampleRate);
af.setChannelCount(decoder.channels());
af.setSampleFormat(QAudioFormat::Float);
QSemaphore sinkReady(0);
QMetaObject::invokeMethod(this, [this, &af, &sinkReady] {
QAudioDevice dev = QMediaDevices::defaultAudioOutput();
m_sink = new QAudioSink(dev, af);
m_sink->setVolume(m_volume);
m_sinkIO = m_sink->start();
sinkReady.release();
}, Qt::QueuedConnection);
sinkReady.acquire();
if (!m_sinkIO) {
qWarning() << "[AudioEngine] QAudioSink failed to start";
m_state = State::Stopped;
return;
}
engine::SpectrumAnalyzer spectrumAnalyzer;
connect(&spectrumAnalyzer, &engine::SpectrumAnalyzer::fftReady,
this, &AudioEngine::fftReady, Qt::QueuedConnection);
auto cleanup = [&] {
QAudioSink *sinkToDelete = m_sink;
m_sink = nullptr;
m_sinkIO = nullptr;
if (sinkToDelete) {
QMetaObject::invokeMethod(this, [sinkToDelete] {
sinkToDelete->stop();
delete sinkToDelete;
}, Qt::QueuedConnection);
}
m_state = State::Stopped;
};
m_posFrames = 0;
engine::AudioBuffer buffer;
while (!m_stopReq) {
{
QMutexLocker lock(&m_pauseMtx);
while (m_pauseReq && !m_stopReq)
m_pauseCv.wait(&m_pauseMtx, 50);
}
if (m_stopReq) break;
if (m_seekPending) {
m_seekPending = false;
double frac = m_seekFraction.load();
decoder.seek(frac);
if (m_totalFrames > 0)
m_posFrames = static_cast<qint64>(frac * m_totalFrames.load());
m_eq.reset();
continue;
}
if (!decoder.decodeNext(buffer))
break;
if (buffer.empty())
continue;
m_eq.process(buffer);
spectrumAnalyzer.feed(buffer);
const char *src = reinterpret_cast<const char *>(buffer.samples.data());
qsizetype bytes = static_cast<qsizetype>(buffer.sampleCount()) * sizeof(float);
qsizetype written = 0;
while (written < bytes && !m_stopReq) {
qsizetype n = m_sinkIO->write(src + written, bytes - written);
if (n <= 0)
QThread::msleep(1);
else
written += n;
}
m_posFrames += buffer.frameCount();
emit positionChanged(m_posFrames.load());
}
bool natural = !m_stopReq;
cleanup();
if (natural)
emit trackEnded();
}