#include "AudioEngine.h" #include "engine/ffmpeg/FFmpegDecoder.h" #include "engine/EqProcessor.h" #include "engine/SpectrumAnalyzer.h" #include #include #include #include #include #include #include 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 engine::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::ffmpeg::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(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(buffer.samples.data()); qsizetype bytes = static_cast(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(); }