// src/AudioEngine.cpp #include "AudioEngine.h" #include #include #include #include #include #include #include #include #include AudioEngine::AudioEngine(QObject* parent) : QObject(parent) { m_processors.push_back(new Processor(m_frameSize, m_sampleRate)); m_processors.push_back(new Processor(m_frameSize, m_sampleRate)); m_processTimer = new QTimer(this); m_processTimer->setInterval(16); connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer); } AudioEngine::~AudioEngine() { stop(); for(auto p : m_processors) delete p; if (m_fileSource) delete m_fileSource; } void AudioEngine::setNumBins(int n) { for(auto p : m_processors) p->setNumBins(n); } void AudioEngine::loadTrack(const QString& filePath) { stop(); m_pcmData.clear(); m_buffer.close(); if (m_fileSource) { m_fileSource->close(); delete m_fileSource; m_fileSource = nullptr; } if (m_decoder) delete m_decoder; m_decoder = new QAudioDecoder(this); QAudioFormat format; format.setSampleRate(44100); format.setChannelCount(2); format.setSampleFormat(QAudioFormat::Int16); m_decoder->setAudioFormat(format); connect(m_decoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onBufferReady); connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished); connect(m_decoder, QOverload::of(&QAudioDecoder::error), this, &AudioEngine::onError); qDebug() << "AudioEngine: Attempting to load" << filePath; #ifdef Q_OS_ANDROID m_fileSource = new QFile(filePath); if (m_fileSource->open(QIODevice::ReadOnly)) { m_decoder->setSourceDevice(m_fileSource); } else { delete m_fileSource; m_fileSource = nullptr; if (filePath.startsWith("content://")) { m_decoder->setSource(QUrl(filePath)); } else { m_decoder->setSource(QUrl::fromLocalFile(filePath)); } } #else m_decoder->setSource(QUrl::fromLocalFile(filePath)); #endif m_decoder->start(); } void AudioEngine::onError(QAudioDecoder::Error error) { qWarning() << "AudioEngine: Decoder Error:" << error << m_decoder->errorString(); emit trackLoaded(false); } void AudioEngine::onBufferReady() { QAudioBuffer buffer = m_decoder->read(); if (!buffer.isValid()) return; const int frames = static_cast(buffer.frameCount()); const int channels = buffer.format().channelCount(); auto sampleType = buffer.format().sampleFormat(); // We store everything as Stereo Int16 to ensure compatibility with all sinks if (sampleType == QAudioFormat::Int16) { const int16_t* src = buffer.constData(); if (!src) return; for (int i = 0; i < frames; ++i) { int16_t left = 0; int16_t right = 0; if (channels == 1) { left = src[i]; right = left; } else if (channels >= 2) { left = src[i * channels]; right = src[i * channels + 1]; } m_pcmData.append(reinterpret_cast(&left), sizeof(int16_t)); m_pcmData.append(reinterpret_cast(&right), sizeof(int16_t)); } } else if (sampleType == QAudioFormat::Float) { const float* src = buffer.constData(); if (!src) return; auto toInt16 = [](float x) -> int16_t { return static_cast(std::clamp(x, -1.0f, 1.0f) * 32767.0f); }; for (int i = 0; i < frames; ++i) { float l = 0.0f; float r = 0.0f; if (channels == 1) { l = src[i]; r = l; } else if (channels >= 2) { l = src[i * channels]; r = src[i * channels + 1]; } int16_t left = toInt16(l); int16_t right = toInt16(r); m_pcmData.append(reinterpret_cast(&left), sizeof(int16_t)); m_pcmData.append(reinterpret_cast(&right), sizeof(int16_t)); } } } void AudioEngine::onFinished() { if (m_pcmData.isEmpty()) { emit trackLoaded(false); return; } m_buffer.setData(m_pcmData); if (!m_buffer.open(QIODevice::ReadOnly)) { emit trackLoaded(false); return; } emit trackLoaded(true); } void AudioEngine::play() { if (!m_buffer.isOpen() || m_pcmData.isEmpty()) return; if (m_sink) { m_sink->resume(); m_processTimer->start(); return; } QAudioFormat format; format.setSampleRate(44100); format.setChannelCount(2); format.setSampleFormat(QAudioFormat::Int16); // Universal format QAudioDevice device = QMediaDevices::defaultAudioOutput(); if (device.isNull()) { qWarning() << "AudioEngine: No audio output device found."; return; } if (!device.isFormatSupported(format)) { qWarning() << "AudioEngine: Int16 format not supported, using preferred format."; format = device.preferredFormat(); } m_sink = new QAudioSink(device, format, this); connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){ if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) { if (m_buffer.bytesAvailable() == 0) { m_processTimer->stop(); emit playbackFinished(); } } }); m_sink->start(&m_buffer); m_processTimer->start(); } void AudioEngine::pause() { if (m_sink) m_sink->suspend(); m_processTimer->stop(); } void AudioEngine::stop() { m_processTimer->stop(); if (m_sink) { m_sink->stop(); delete m_sink; m_sink = nullptr; } } void AudioEngine::seek(float position) { if (m_pcmData.isEmpty()) return; qint64 pos = position * m_pcmData.size(); // Align to 4 bytes (2 channels * 2 bytes per sample) pos -= pos % 4; if (m_buffer.isOpen()) m_buffer.seek(pos); } void AudioEngine::setDspParams(int frameSize, int hopSize) { m_frameSize = frameSize; m_hopSize = hopSize; for(auto p : m_processors) p->setFrameSize(frameSize); } void AudioEngine::onProcessTimer() { if (!m_buffer.isOpen()) return; qint64 currentPos = m_buffer.pos(); emit positionChanged((float)currentPos / m_pcmData.size()); // Convert Int16 back to Float for DSP const int16_t* samples = reinterpret_cast(m_pcmData.constData()); qint64 sampleIdx = currentPos / sizeof(int16_t); qint64 totalSamples = m_pcmData.size() / sizeof(int16_t); if (sampleIdx + m_frameSize * 2 >= totalSamples) return; std::vector ch0(m_frameSize), ch1(m_frameSize); for (int i = 0; i < m_frameSize; ++i) { ch0[i] = samples[sampleIdx + i*2] / 32768.0f; ch1[i] = samples[sampleIdx + i*2 + 1] / 32768.0f; } m_processors[0]->pushData(ch0); m_processors[1]->pushData(ch1); std::vector results; for (auto p : m_processors) { auto spec = p->getSpectrum(); results.push_back({spec.freqs, spec.db}); } emit spectrumReady(results); }