mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
177 lines
4.2 KiB
C++
177 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;
|
||
|
|
}
|
||
|
|
}
|