nosignal-shell/plugin/src/Caelestia/audioprovider.cpp
2025-09-07 18:21:31 +10:00

217 lines
6 KiB
C++

#include "audioprovider.hpp"
#include "service.hpp"
#include <QAudioSource>
#include <QDebug>
#include <QIODevice>
#include <QMediaDevices>
#include <QMutexLocker>
#include <QObject>
#include <QThread>
#include <QVector>
#include <algorithm>
#include <cstddef>
#include <cstdint>
namespace caelestia {
AudioCollector::AudioCollector(AudioProvider* provider, QObject* parent)
: QObject(parent)
, m_source(nullptr)
, m_device(nullptr)
, m_provider(provider)
, m_sampleRate(provider->sampleRate())
, m_chunkSize(provider->chunkSize())
, m_chunk(m_chunkSize)
, m_chunkOffset(0) {}
AudioCollector::~AudioCollector() {
m_source->stop();
}
void AudioCollector::init() {
QAudioFormat format;
format.setSampleRate(m_sampleRate);
format.setChannelCount(1);
format.setSampleFormat(QAudioFormat::Int16);
m_source = new QAudioSource(QMediaDevices::defaultAudioInput(), format, this);
connect(m_source, &QAudioSource::stateChanged, this, &AudioCollector::handleStateChanged);
};
void AudioCollector::start() {
if (!m_source) {
return;
}
m_device = m_source->start();
connect(m_device, &QIODevice::readyRead, this, &AudioCollector::loadChunk);
}
void AudioCollector::stop() {
if (m_source) {
m_source->stop();
m_device = nullptr;
}
}
void AudioCollector::loadChunk() {
const QByteArray data = m_device->readAll();
const int16_t* samples = reinterpret_cast<const int16_t*>(data.constData());
const size_t count = static_cast<size_t>(data.size()) / sizeof(int16_t);
size_t i = 0;
while (i < count) {
const int spaceLeft = m_chunkSize - m_chunkOffset;
const auto toCopy = std::min<size_t>(static_cast<size_t>(spaceLeft), count - i);
std::transform(samples + i, samples + i + toCopy, m_chunk.begin() + m_chunkOffset, [](int16_t sample) {
return sample / 32768.0;
});
m_chunkOffset += toCopy;
i += toCopy;
if (m_chunkOffset == m_chunkSize) {
m_provider->loadChunk(m_chunk);
m_chunkOffset = 0;
}
}
}
void AudioCollector::handleStateChanged(QtAudio::State state) const {
if (state == QtAudio::StoppedState && m_source->error() != QtAudio::NoError) {
switch (m_source->error()) {
case QtAudio::OpenError:
qWarning() << "AudioWorker: failed to open audio device";
break;
case QtAudio::IOError:
qWarning() << "AudioWorker: an error occurred during read/write of audio device";
break;
case QtAudio::UnderrunError:
qWarning() << "AudioWorker: audio data is not being fed to audio device fast enough";
break;
case QtAudio::FatalError:
qCritical() << "AudioWorker: fatal error in audio device";
break;
default:
break;
}
}
}
AudioProcessor::AudioProcessor(AudioProvider* provider, QObject* parent)
: QObject(parent)
, m_sampleRate(provider->sampleRate())
, m_chunkSize(provider->chunkSize())
, m_provider(provider) {}
AudioProcessor::~AudioProcessor() {
if (m_timer) {
m_timer->stop();
}
}
void AudioProcessor::init() {
m_timer = new QTimer(this);
m_timer->setInterval(static_cast<int>(m_chunkSize * 1000.0 / m_sampleRate));
connect(m_timer, &QTimer::timeout, this, &AudioProcessor::handleTimeout);
}
void AudioProcessor::start() {
if (m_timer) {
m_timer->start();
}
}
void AudioProcessor::stop() {
if (m_timer) {
m_timer->stop();
}
}
void AudioProcessor::handleTimeout() {
const QVector<double> chunk = m_provider->nextChunk();
if (!chunk.isEmpty()) {
processChunk(chunk);
}
}
AudioProvider::AudioProvider(int sampleRate, int chunkSize, QObject* parent)
: Service(parent)
, m_sampleRate(sampleRate)
, m_chunkSize(chunkSize)
, m_collector(new AudioCollector(this))
, m_processor(nullptr)
, m_collectorThread(new QThread(this))
, m_processorThread(nullptr) {
m_collector->moveToThread(m_collectorThread);
connect(m_collectorThread, &QThread::started, m_collector, &AudioCollector::init);
connect(m_collectorThread, &QThread::finished, m_collector, &AudioCollector::deleteLater);
connect(m_collectorThread, &QThread::finished, m_collectorThread, &QThread::deleteLater);
m_collectorThread->start();
}
AudioProvider::~AudioProvider() {
m_collectorThread->quit();
if (m_processorThread) {
m_processorThread->quit();
m_processorThread->wait();
}
m_collectorThread->wait();
}
int AudioProvider::sampleRate() const {
return m_sampleRate;
}
int AudioProvider::chunkSize() const {
return m_chunkSize;
}
QVector<double> AudioProvider::nextChunk() {
QMutexLocker lock(&m_mutex);
if (m_chunks.isEmpty()) {
return {};
}
return m_chunks.dequeue();
}
void AudioProvider::loadChunk(const QVector<double>& chunk) {
QMutexLocker lock(&m_mutex);
m_chunks.enqueue(chunk);
}
void AudioProvider::init() {
if (!m_processor) {
qWarning() << "AudioProvider::init: attempted to init with no processor set";
return;
}
m_processorThread = new QThread(this);
m_processor->moveToThread(m_processorThread);
connect(m_processorThread, &QThread::started, m_processor, &AudioProcessor::init);
connect(m_processorThread, &QThread::finished, m_processor, &AudioProcessor::deleteLater);
connect(m_processorThread, &QThread::finished, m_processorThread, &QThread::deleteLater);
m_processorThread->start();
}
void AudioProvider::start() {
QMetaObject::invokeMethod(m_collector, "start", Qt::QueuedConnection);
if (m_processor) {
QMetaObject::invokeMethod(m_processor, "start", Qt::QueuedConnection);
}
}
void AudioProvider::stop() {
QMetaObject::invokeMethod(m_collector, "stop", Qt::QueuedConnection);
if (m_processor) {
QMetaObject::invokeMethod(m_processor, "stop", Qt::QueuedConnection);
}
}
} // namespace caelestia