js8call/JS8_Audio/NotificationSoundOutput.cpp
rruchte c8f0632b3b Addresses issue #260 Multiple notification audio connections left open
Add new NotificationSoundOutput class and wire it up
2026-04-28 12:14:37 -05:00

176 lines
4.2 KiB
C++

/**
* @file NotificationSoundOutput.cpp
* @brief Self-contained audio output for notification sounds.
*
* Designed for fire-and-forget playback of buffered audio data.
* Manages its own buffer lifecycle and releases the audio device
* automatically when playback completes.
*/
#include "NotificationSoundOutput.h"
#include <QAudioSink>
#include <QBuffer>
NotificationSoundOutput::NotificationSoundOutput(QObject *parent)
: QObject(parent)
{
}
NotificationSoundOutput::~NotificationSoundOutput()
{
if (m_sink) {
disconnect(m_sink.get(), nullptr, this, nullptr);
m_sink->stop();
}
}
/**
* @brief Sets the audio device and buffer size.
* @param device The QAudioDevice to use.
* @param msBuffer The buffer size in milliseconds.
*/
void NotificationSoundOutput::setDevice(QAudioDevice const &device,
unsigned const msBuffer)
{
m_device = device;
m_msBuffer = msBuffer;
}
/**
* @brief Sets the attenuation in dB.
* @param a The attenuation value (0 = full volume).
*/
void NotificationSoundOutput::setAttenuation(qreal const a)
{
Q_ASSERT(0.0 <= a && a <= 999.0);
m_volume = qPow(10.0, -a / 20.0);
if (m_sink)
m_sink->setVolume(m_volume);
}
/**
* @brief Plays audio from the provided data and format.
* @param data The raw PCM audio data.
* @param format The QAudioFormat describing the data.
*
* Any currently playing notification is stopped and replaced.
*/
void NotificationSoundOutput::play(QByteArray const &data,
QAudioFormat const &format)
{
if (m_sink) {
disconnect(m_sink.get(), nullptr, this, nullptr);
m_sink->stop();
}
if (m_buffer) {
m_buffer->close();
m_buffer.reset();
}
if (m_device.isNull()) {
Q_EMIT error(tr("No audio output device configured."));
return;
}
if (!format.isValid()) {
Q_EMIT error(tr("Requested audio format is not valid."));
return;
}
if (!m_device.isFormatSupported(format)) {
Q_EMIT error(tr("Requested audio format is not supported on device."));
return;
}
if (data.isEmpty()) {
Q_EMIT error(tr("Audio data is empty."));
return;
}
m_buffer.reset(new QBuffer);
m_buffer->setData(data);
if (!m_buffer->open(QIODevice::ReadOnly)) {
m_buffer.reset();
Q_EMIT error(tr("Unable to open audio buffer."));
return;
}
// Only create a new sink if we don't have one, or if the
// format has changed since the last playback.
if (!m_sink || m_currentFormat != format) {
m_sink.reset(new QAudioSink(m_device, format));
m_sink->setVolume(m_volume);
m_currentFormat = format;
if (m_msBuffer > 0) {
m_sink->setBufferSize(
m_sink->format().bytesForDuration(m_msBuffer));
}
}
connect(m_sink.get(), &QAudioSink::stateChanged,
this, &NotificationSoundOutput::handleStateChanged);
m_sink->start(m_buffer.get());
if (m_sink->error() != QAudio::NoError) {
Q_EMIT error(tr("Failed to start audio output."));
m_sink->stop();
if (m_buffer) {
m_buffer->close();
m_buffer.reset();
}
}
}
/**
* @brief Stops playback and releases all resources.
*/
void NotificationSoundOutput::stop()
{
release();
}
// Private
void NotificationSoundOutput::release()
{
if (m_sink) {
disconnect(m_sink.get(), nullptr, this, nullptr);
m_sink->stop();
}
if (m_buffer) {
m_buffer->close();
m_buffer.reset();
}
}
/**
* @brief Handles state changes from the QAudioSink.
* @param newState The new audio state.
*/
void NotificationSoundOutput::handleStateChanged(QAudio::State const newState)
{
switch (newState) {
case QAudio::ActiveState:
Q_EMIT status(tr("Active"));
break;
case QAudio::SuspendedState:
Q_EMIT status(tr("Suspended"));
break;
case QAudio::IdleState:
case QAudio::StoppedState:
if (m_buffer) {
m_buffer->close();
m_buffer.reset();
}
Q_EMIT status(tr("Idle"));
break;
}
}