202 lines
5.5 KiB
C++
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();
|
|
}
|