WSJT-X/Audio/soundout.cpp
Bill Somerville 542ffe8311
Improve audio device handling and error recovery
where possible  audio devices that  disappear are not  forgotten until
the user selects another device, this should allow temporarily missing
devices or forgetting  to switch on devices before  starting WSJT-X to
be  handled more  cleanly. If  all else  fails, visiting  the Settings
dialog and  clicking OK should  get things  going again. Note  that we
still  do not  have  a  reliable way  of  detecting  failed audio  out
devices, in that  case selecting another device and  then returning to
the original should work.

Enumerating  audio devices  is expensive  and on  Linux may  take many
seconds per  device. To avoid  lengthy blocking behaviour until  it is
absolutely necessary,  audio devices are  not enumerated until  one of
the "Settings->Audio" device drop-down lists is opened. Elsewhere when
devices  must be  discovered  the  enumeration stops  as  soon as  the
configured device is  discovered. A status bar message  is posted when
audio devices are being enumerated as a reminder that the UI may block
while this is happening.

The message box warning about  unaccounted-for input audio samples now
only triggers when  >5 seconds of audio appears to  be missing or over
provided. Hopefully this will make the warning less annoying for those
that are  using audio sources  with high and/or variable  latencies. A
status  bar message  is still  posted for  any amount  of audio  input
samples  unaccounted for  >1/5 second,  this message  appearing a  lot
should be considered as notification that  there is a problem with the
audio sub-system, system load is  too high, or time synchronization is
stepping the PC clock rather  than adjusting the frequency to maintain
monotonic clock ticks.
2020-09-20 18:20:16 +01:00

215 lines
5.1 KiB
C++

#include "soundout.h"
#include <QDateTime>
#include <QAudioDeviceInfo>
#include <QAudioOutput>
#include <QSysInfo>
#include <qmath.h>
#include <QDebug>
#include "Audio/AudioDevice.hpp"
#include "moc_soundout.cpp"
bool SoundOutput::checkStream () const
{
bool result {false};
Q_ASSERT_X (m_stream, "SoundOutput", "programming error");
if (m_stream) {
switch (m_stream->error ())
{
case QAudio::OpenError:
Q_EMIT error (tr ("An error opening the audio output device has occurred."));
break;
case QAudio::IOError:
Q_EMIT error (tr ("An error occurred during write to the audio output device."));
break;
case QAudio::UnderrunError:
Q_EMIT error (tr ("Audio data not being fed to the audio output device fast enough."));
break;
case QAudio::FatalError:
Q_EMIT error (tr ("Non-recoverable error, audio output device not usable at this time."));
break;
case QAudio::NoError:
result = true;
break;
}
}
return result;
}
void SoundOutput::setFormat (QAudioDeviceInfo const& device, unsigned channels, int frames_buffered)
{
Q_ASSERT (0 < channels && channels < 3);
m_device = device;
m_channels = channels;
m_framesBuffered = frames_buffered;
}
void SoundOutput::restart (AudioDevice * source)
{
if (!m_device.isNull ())
{
QAudioFormat format (m_device.preferredFormat ());
// qDebug () << "Preferred audio output format:" << format;
format.setChannelCount (m_channels);
format.setCodec ("audio/pcm");
format.setSampleRate (48000);
format.setSampleType (QAudioFormat::SignedInt);
format.setSampleSize (16);
format.setByteOrder (QAudioFormat::Endian (QSysInfo::ByteOrder));
if (!format.isValid ())
{
Q_EMIT error (tr ("Requested output audio format is not valid."));
}
else if (!m_device.isFormatSupported (format))
{
Q_EMIT error (tr ("Requested output audio format is not supported on device."));
}
else
{
// qDebug () << "Selected audio output format:" << format;
m_stream.reset (new QAudioOutput (m_device, format));
checkStream ();
m_stream->setVolume (m_volume);
m_stream->setNotifyInterval(1000);
error_ = false;
connect (m_stream.data(), &QAudioOutput::stateChanged, this, &SoundOutput::handleStateChanged);
connect (m_stream.data(), &QAudioOutput::notify, [this] () {checkStream ();});
// qDebug() << "A" << m_volume << m_stream->notifyInterval();
}
}
if (!m_stream)
{
if (!error_)
{
error_ = true; // only signal error once
Q_EMIT error (tr ("No audio output device configured."));
}
return;
}
else
{
error_ = false;
}
// we have to set this before every start on the stream because the
// Windows implementation seems to forget the buffer size after a
// stop.
//qDebug () << "SoundOut default buffer size (bytes):" << m_stream->bufferSize () << "period size:" << m_stream->periodSize ();
if (m_framesBuffered)
{
#if defined (Q_OS_WIN)
m_stream->setBufferSize (m_stream->format().bytesForFrames (m_framesBuffered));
#endif
}
m_stream->setCategory ("production");
m_source = source;
m_stream->start (source);
// qDebug () << "SoundOut selected buffer size (bytes):" << m_stream->bufferSize () << "period size:" << m_stream->periodSize ();
}
void SoundOutput::suspend ()
{
if (m_stream && QAudio::ActiveState == m_stream->state ())
{
m_stream->suspend ();
checkStream ();
}
}
void SoundOutput::resume ()
{
if (m_stream && QAudio::SuspendedState == m_stream->state ())
{
m_stream->resume ();
checkStream ();
}
}
void SoundOutput::reset ()
{
if (m_stream)
{
m_stream->reset ();
checkStream ();
}
}
void SoundOutput::stop ()
{
if (m_stream)
{
m_stream->reset ();
m_stream->stop ();
}
m_stream.reset ();
}
qreal SoundOutput::attenuation () const
{
return -(20. * qLn (m_volume) / qLn (10.));
}
void SoundOutput::setAttenuation (qreal a)
{
Q_ASSERT (0. <= a && a <= 999.);
m_volume = qPow(10.0, -a/20.0);
// qDebug () << "SoundOut: attn = " << a << ", vol = " << m_volume;
if (m_stream)
{
m_stream->setVolume (m_volume);
}
}
void SoundOutput::resetAttenuation ()
{
m_volume = 1.;
if (m_stream)
{
m_stream->setVolume (m_volume);
}
}
void SoundOutput::handleStateChanged (QAudio::State newState)
{
switch (newState)
{
case QAudio::IdleState:
Q_EMIT status (tr ("Idle"));
break;
case QAudio::ActiveState:
Q_EMIT status (tr ("Sending"));
break;
case QAudio::SuspendedState:
Q_EMIT status (tr ("Suspended"));
break;
#if QT_VERSION >= QT_VERSION_CHECK (5, 10, 0)
case QAudio::InterruptedState:
Q_EMIT status (tr ("Interrupted"));
break;
#endif
case QAudio::StoppedState:
if (!checkStream ())
{
Q_EMIT status (tr ("Error"));
}
else
{
Q_EMIT status (tr ("Stopped"));
}
break;
}
}