js8call/JS8_Audio/SoundInput.cpp
rruchte 806050226a Adds SoundInput::canOpenAsInput to test if an QAudioDevice instance is actually a viable input
Update Configuration::impl::find_audio_device to test if device is actually in input by using Adds SoundInput::canOpenAsInput, rather than trusting the mode listed in the device.

On Linux, QT can erroneously list QAudioDevice instance as having the mode QAudioDevice::Output when they are actually inputs. This results in the "Requested output audio format is not valid" error on startup, and requires the user to open the configuration and select the audio device in the input list before we can start it.
2026-02-26 07:45:28 -06:00

196 lines
4.9 KiB
C++

/**
* @file soundin.cpp
* @brief Implementation of SoundInput class
*/
#include "SoundInput.h"
#include "JS8_Main/DriftingDateTime.h"
#include <QAudioFormat>
#include <QLoggingCategory>
#include <QSysInfo>
#include "moc_SoundInput.cpp"
Q_DECLARE_LOGGING_CATEGORY(soundin_js8)
/**
* @brief Checks for audio errors and emits appropriate error messages.
*
* @return true
* @return false
*/
bool SoundInput::audioError() const {
bool result(true);
Q_ASSERT_X(m_stream, "SoundInput", "programming error");
if (m_stream) {
switch (m_stream->error()) {
case QAudio::OpenError:
Q_EMIT error(
tr("An error opening the audio input device has occurred."));
break;
case QAudio::IOError:
Q_EMIT error(tr(
"An error occurred during read from the audio input device."));
break;
case QAudio::UnderrunError:
Q_EMIT error(tr("Audio data not being fed to the audio input "
"device fast enough."));
break;
case QAudio::FatalError:
Q_EMIT error(tr("Non-recoverable error, audio input device not "
"usable at this time."));
break;
case QAudio::NoError:
result = false;
break;
}
}
return result;
}
/**
* @brief Starts audio input from the specified device.
*
* @param device The QAudioDevice to use for input.
* @param framesPerBuffer The number of frames per buffer.
* @param sink The AudioDevice sink to write audio data to.
* @param channel The audio channel configuration (Mono or Stereo).
*/
void SoundInput::start(QAudioDevice const &device, int framesPerBuffer,
AudioDevice *sink, AudioDevice::Channel channel) {
Q_ASSERT(sink);
stop();
m_sink = sink;
QAudioFormat format(device.preferredFormat());
// qCDebug (soundin_js8) << "Preferred audio input format:" << format;
format.setSampleFormat(QAudioFormat::Int16);
format.setChannelCount(AudioDevice::Mono == channel ? 1 : 2);
format.setSampleRate(48000);
if (!format.isValid()) {
Q_EMIT error(tr("Requested input audio format is not valid."));
return;
}
if (!device.isFormatSupported(format)) {
// qCDebug (soundin_js8) << "Nearest supported audio format:" <<
// device.nearestFormat (format);
Q_EMIT error(
tr("Requested input audio format is not supported on device."));
return;
}
// qCDebug (soundin_js8) << "Selected audio input format:" << format;
m_stream.reset(new QAudioSource{device, format});
if (audioError()) {
return;
}
connect(m_stream.data(), &QAudioSource::stateChanged, this,
&SoundInput::handleStateChanged);
m_stream->setBufferSize(m_stream->format().bytesForFrames(framesPerBuffer));
if (sink->initialize(QIODevice::WriteOnly, channel)) {
m_stream->start(sink);
audioError();
} else {
Q_EMIT error(tr("Failed to initialize audio sink device"));
}
}
/**
* @brief Suspends audio input.
*/
void SoundInput::suspend() {
if (m_stream) {
m_stream->suspend();
audioError();
}
}
/**
* @brief Resumes audio input.
*/
void SoundInput::resume() {
// qCDebug(soundin_js8) << "Resume" <<
// fmod(0.001*DriftingDateTime::currentMSecsSinceEpoch(),6.0);
if (m_sink) {
m_sink->reset();
}
if (m_stream) {
m_stream->resume();
audioError();
}
}
/**
* @brief Handles state changes of the audio input.
* @param newState The new state of the audio input.
*/
void SoundInput::handleStateChanged(QAudio::State newState) const {
// qCDebug (soundin_js8) << "SoundInput::handleStateChanged: newState:" <<
// newState;
switch (newState) {
case QAudio::IdleState:
Q_EMIT status(tr("Idle"));
break;
case QAudio::ActiveState:
Q_EMIT status(tr("Receiving"));
break;
case QAudio::SuspendedState:
Q_EMIT status(tr("Suspended"));
break;
case QAudio::StoppedState:
if (audioError()) {
Q_EMIT status(tr("Error"));
} else {
Q_EMIT status(tr("Stopped"));
}
break;
}
}
/**
* @brief Stops audio input.
*/
void SoundInput::stop() {
if (m_stream) {
m_stream->stop();
}
m_stream.reset();
if (m_sink) {
m_sink->close();
}
}
/**
* @brief Tests whether device can be opened as an input
*/
bool SoundInput::canOpenAsInput(const QAudioDevice &dev) {
QAudioFormat fmt = dev.preferredFormat();
auto src = std::make_unique<QAudioSource>(dev, fmt);
src->start(); // opens device
src->stop();
return src->error() == QAudio::NoError;
}
/**
* @brief Destructs the SoundInput object.
*/
SoundInput::~SoundInput() { stop(); }
Q_LOGGING_CATEGORY(soundin_js8, "soundin.js8", QtWarningMsg)