Replace QAudioDeviceInfo with CAudioDeviceInfo where possible

QAudioDeviceInfo is a low level technical class, which shouldn't be used in higher level code. Remove it from all APIs
where possible and just create it in order to interface with QAudio
This commit is contained in:
Roland Rossgotterer
2019-10-02 14:50:37 +02:00
committed by Mat Sutcliffe
parent 8656131eb1
commit a2e3700739
18 changed files with 141 additions and 863 deletions

View File

@@ -10,12 +10,13 @@
#include <QTimer>
using namespace BlackMisc;
using namespace BlackMisc::Audio;
using namespace BlackMisc::Aviation;
using namespace BlackMisc::PhysicalQuantities;
namespace BlackSound
{
CSelcalPlayer::CSelcalPlayer(const QAudioDeviceInfo &device, QObject *parent)
CSelcalPlayer::CSelcalPlayer(const CAudioDeviceInfo &device, QObject *parent)
: QObject(parent),
m_threadedPlayer(this, "CSelcalPlayer", device)
{

View File

@@ -14,11 +14,10 @@
#include "blacksound/threadedtonepairplayer.h"
#include "blacksound/tonepair.h"
#include "blacksoundexport.h"
#include "blackmisc/audio/audiodeviceinfo.h"
#include "blackmisc/aviation/selcal.h"
#include "blackmisc/worker.h"
#include <QAudioDeviceInfo>
namespace BlackSound
{
//! SELCAL player
@@ -28,7 +27,7 @@ namespace BlackSound
public:
//! Constructor
CSelcalPlayer(const QAudioDeviceInfo &device = QAudioDeviceInfo::defaultOutputDevice(), QObject *parent = nullptr);
CSelcalPlayer(const BlackMisc::Audio::CAudioDeviceInfo &device, QObject *parent = nullptr);
//! Destructor
virtual ~CSelcalPlayer() override;

View File

@@ -1,477 +0,0 @@
/* Copyright (C) 2013
* swift project Community / Contributors
*
* This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level
* directory of this distribution. No part of swift project, including this file, may be copied, modified, propagated,
* or distributed except according to the terms contained in the LICENSE file.
*/
#include "blacksound/soundgenerator.h"
#include "blackmisc/directoryutils.h"
#include "blackmisc/filedeleter.h"
#include "blackmisc/worker.h"
#include <QtCore/qendian.h>
#include <math.h>
#include <qmath.h>
#include <qendian.h>
#include <QMultimedia>
#include <QAudioOutput>
#include <QTimer>
#include <QFile>
#include <QSound>
using namespace BlackMisc;
using namespace BlackMisc::Aviation;
using namespace BlackMisc::PhysicalQuantities;
using namespace BlackMisc::Audio;
namespace BlackSound
{
QDateTime CSoundGenerator::s_selcalStarted = QDateTime::currentDateTimeUtc();
CSoundGenerator::CSoundGenerator(const QAudioDeviceInfo &device, const QAudioFormat &format, const QList<Tone> &tones, CNotificationSounds::PlayMode mode, QObject *parent)
: QIODevice(parent),
m_tones(tones), m_position(0), m_playMode(mode), m_endReached(false), m_oneCycleDurationMs(calculateDurationMs(tones)),
m_device(device), m_audioFormat(format), m_audioOutput(new QAudioOutput(format))
{
Q_ASSERT_X(tones.size() > 0, Q_FUNC_INFO, "No tones");
}
CSoundGenerator::CSoundGenerator(const QList<Tone> &tones, CNotificationSounds::PlayMode mode, QObject *parent)
: QIODevice(parent),
m_tones(tones), m_position(0), m_playMode(mode), m_endReached(false), m_oneCycleDurationMs(calculateDurationMs(tones)),
m_device(QAudioDeviceInfo::defaultOutputDevice()), m_audioFormat(CSoundGenerator::defaultAudioFormat()),
m_audioOutput(new QAudioOutput(CSoundGenerator::defaultAudioFormat()))
{
Q_ASSERT_X(tones.size() > 0, Q_FUNC_INFO, "No tones");
}
CSoundGenerator::~CSoundGenerator()
{
this->stop(true);
if (m_ownThread) { m_ownThread->deleteLater(); }
}
void CSoundGenerator::start(int volume, bool pull)
{
if (m_buffer.isEmpty()) this->generateData();
this->open(QIODevice::ReadOnly);
m_audioOutput->setVolume(qreal(0.01 * volume));
if (pull)
{
// For an output device, the QAudioOutput class will pull data from the QIODevice
// (using QIODevice::read()) when more audio data is required.
m_audioOutput->start(this); // pull
}
else
{
// In push mode, the audio device provides a QIODevice instance that can be
// written or read to as needed. Typically this results in simpler code but more buffering, which may affect latency.
if (!m_pushTimer)
{
m_pushTimer = new QTimer(this);
bool ok = connect(m_pushTimer, &QTimer::timeout, this, &CSoundGenerator::pushTimerExpired);
Q_ASSERT(ok);
Q_UNUSED(ok) // suppress Clang warning in release build
m_pushTimer->start(20);
}
m_pushModeIODevice = m_audioOutput->start(); // push, IO device not owned
}
}
void CSoundGenerator::stop(bool destructor)
{
// m_audioOutput->setVolume(0); // Bug or feature, killing the applicaions volume?
if (this->isOpen())
{
// 1. isOpen avoids redundant signals
// 2. OK in destructor, see http://stackoverflow.com/a/14024955/356726
this->close(); // close IO Device
m_audioOutput->stop();
if (m_pushTimer) { m_pushTimer->stop(); }
emit this->stopped();
}
m_position = 0;
if (destructor) return;
// trigger own termination
if (m_playMode == CNotificationSounds::SingleWithAutomaticDeletion)
{
emit this->stopping();
if (!m_ownThread) this->deleteLater(); // with own thread, thread signal will call deleteLater
}
}
void CSoundGenerator::pushTimerExpired()
{
if (m_pushModeIODevice && !m_endReached && m_audioOutput->state() != QAudio::StoppedState)
{
int chunks = m_audioOutput->bytesFree() / m_audioOutput->periodSize();
while (chunks)
{
// periodSize-> Returns the period size in bytes.
//! \todo looks wrong: read() will memcpy from m_buffer.constData() to m_buffer.data()
const qint64 len = this->read(m_buffer.data(), m_audioOutput->periodSize());
if (len >= 0)
{
m_pushModeIODevice->write(m_buffer.constData(), len);
}
if (len != m_audioOutput->periodSize())
{
break; // not a complete period, so buffer is completely read
}
--chunks;
}
}
else
{
if (m_pushTimer)
{
m_pushTimer->stop();
m_pushTimer->disconnect(this);
}
if (m_playMode == CNotificationSounds::SingleWithAutomaticDeletion)
{
this->stop();
}
}
}
void CSoundGenerator::generateData()
{
Q_ASSERT(m_tones.size() > 0);
const int bytesPerSample = m_audioFormat.sampleSize() / 8;
const int bytesForAllChannels = m_audioFormat.channelCount() * bytesPerSample;
qint64 totalLength = 0;
for (const Tone &t : m_tones)
{
totalLength += m_audioFormat.sampleRate() * bytesForAllChannels * t.m_durationMs / 1000;
}
Q_ASSERT(totalLength % bytesForAllChannels == 0);
Q_UNUSED(bytesForAllChannels) // suppress warning in release builds
m_buffer.resize(static_cast<int>(totalLength)); // potentially dangerous cast, but I see no use case where the int range on any of our platforms is exceeded
unsigned char *bufferPointer = reinterpret_cast<unsigned char *>(m_buffer.data()); // clazy:exclude=detaching-member
for (const Tone &t : m_tones)
{
qint64 bytesPerTone = m_audioFormat.sampleRate() * bytesForAllChannels * t.m_durationMs / 1000;
qint64 last0AmplitudeSample = bytesPerTone; // last sample when amplitude was 0
int sampleIndexPerTone = 0;
while (bytesPerTone)
{
// http://hyperphysics.phy-astr.gsu.edu/hbase/audio/sumdif.html
// http://math.stackexchange.com/questions/164369/how-do-you-calculate-the-frequency-perceived-by-humans-of-two-sinusoidal-waves-a
const double pseudoTime = double(sampleIndexPerTone % m_audioFormat.sampleRate()) / m_audioFormat.sampleRate();
double amplitude = 0.0; // amplitude -1 -> +1 , 0 is silence
if (t.m_frequencyHz > 10)
{
// the combination of two frequencies actually would have 2*amplitude,
// but I have to normalize with amplitude -1 -> +1
amplitude = t.m_secondaryFrequencyHz == 0 ?
qSin(2 * M_PI * t.m_frequencyHz * pseudoTime) :
qSin(M_PI * (t.m_frequencyHz + t.m_secondaryFrequencyHz) * pseudoTime) *
qCos(M_PI * (t.m_frequencyHz - t.m_secondaryFrequencyHz) * pseudoTime);
}
// avoid overflow
Q_ASSERT(amplitude <= 1.0 && amplitude >= -1.0);
if (amplitude < -1.0)
{
amplitude = -1.0;
}
else if (amplitude > 1.0)
{
amplitude = 1.0;
}
else if (qAbs(amplitude) < double(1.0 / 65535))
{
amplitude = 0;
last0AmplitudeSample = bytesPerTone;
}
// generate this for all channels, usually 1 channel
for (int i = 0; i < m_audioFormat.channelCount(); ++i)
{
this->writeAmplitudeToBuffer(amplitude, bufferPointer);
bufferPointer += bytesPerSample;
bytesPerTone -= bytesPerSample;
}
++sampleIndexPerTone;
}
// fixes the range from the last 0 pass through
if (last0AmplitudeSample > 0)
{
bufferPointer -= last0AmplitudeSample;
while (last0AmplitudeSample)
{
double amplitude = 0.0; // amplitude -1 -> +1 , 0 is silence
// generate this for all channels, usually 1 channel
for (int i = 0; i < m_audioFormat.channelCount(); ++i)
{
this->writeAmplitudeToBuffer(amplitude, bufferPointer);
bufferPointer += bytesPerSample;
last0AmplitudeSample -= bytesPerSample;
}
}
}
}
}
void CSoundGenerator::writeAmplitudeToBuffer(const double amplitude, unsigned char *bufferPointer)
{
if (m_audioFormat.sampleSize() == 8 && m_audioFormat.sampleType() == QAudioFormat::UnSignedInt)
{
const quint8 value = static_cast<quint8>((1.0 + amplitude) / 2 * 255);
*reinterpret_cast<quint8 *>(bufferPointer) = value;
}
else if (m_audioFormat.sampleSize() == 8 && m_audioFormat.sampleType() == QAudioFormat::SignedInt)
{
const qint8 value = static_cast<qint8>(amplitude * 127);
*reinterpret_cast<qint8 *>(bufferPointer) = value;
}
else if (m_audioFormat.sampleSize() == 16 && m_audioFormat.sampleType() == QAudioFormat::UnSignedInt)
{
quint16 value = static_cast<quint16>((1.0 + amplitude) / 2 * 65535);
if (m_audioFormat.byteOrder() == QAudioFormat::LittleEndian)
{
qToLittleEndian<quint16>(value, bufferPointer);
}
else
{
qToBigEndian<quint16>(value, bufferPointer);
}
}
else if (m_audioFormat.sampleSize() == 16 && m_audioFormat.sampleType() == QAudioFormat::SignedInt)
{
qint16 value = static_cast<qint16>(amplitude * 32767);
if (m_audioFormat.byteOrder() == QAudioFormat::LittleEndian)
{
qToLittleEndian<qint16>(value, bufferPointer);
}
else
{
qToBigEndian<qint16>(value, bufferPointer);
}
}
}
bool CSoundGenerator::saveToWavFile(const QString &fileName) const
{
QFile file(fileName);
bool success = file.open(QIODevice::WriteOnly);
if (!success) return false;
CombinedHeader header;
constexpr auto headerLength = sizeof(CombinedHeader);
memset(&header, 0, headerLength);
// RIFF header
if (m_audioFormat.byteOrder() == QAudioFormat::LittleEndian)
{
memcpy(&header.riff.descriptor.id[0], "RIFF", 4);
}
else
{
memcpy(&header.riff.descriptor.id[0], "RIFX", 4);
}
qToLittleEndian<quint32>(quint32(static_cast<uint>(m_buffer.size()) + headerLength - 8),
reinterpret_cast<unsigned char *>(&header.riff.descriptor.size));
memcpy(&header.riff.type[0], "WAVE", 4);
// WAVE header
memcpy(&header.wave.descriptor.id[0], "fmt ", 4);
qToLittleEndian<quint32>(quint32(16), reinterpret_cast<unsigned char *>(&header.wave.descriptor.size));
qToLittleEndian<quint16>(quint16(1), reinterpret_cast<unsigned char *>(&header.wave.audioFormat));
qToLittleEndian<quint16>(quint16(m_audioFormat.channelCount()), reinterpret_cast<unsigned char *>(&header.wave.numChannels));
qToLittleEndian<quint32>(quint32(m_audioFormat.sampleRate()), reinterpret_cast<unsigned char *>(&header.wave.sampleRate));
qToLittleEndian<quint32>(quint32(m_audioFormat.sampleRate() * m_audioFormat.channelCount() * m_audioFormat.sampleSize() / 8), reinterpret_cast<unsigned char *>(&header.wave.byteRate));
qToLittleEndian<quint16>(quint16(m_audioFormat.channelCount() * m_audioFormat.sampleSize() / 8), reinterpret_cast<unsigned char *>(&header.wave.blockAlign));
qToLittleEndian<quint16>(quint16(m_audioFormat.sampleSize()), reinterpret_cast<unsigned char *>(&header.wave.bitsPerSample));
// DATA header
memcpy(&header.data.descriptor.id[0], "data", 4);
qToLittleEndian<quint32>(quint32(m_buffer.size()), reinterpret_cast<unsigned char *>(&header.data.descriptor.size));
success = file.write(reinterpret_cast<const char *>(&header), headerLength) == headerLength;
success = success && file.write(m_buffer) == m_buffer.size();
file.close();
return success;
}
qint64 CSoundGenerator::calculateDurationMs(const QList<CSoundGenerator::Tone> &tones)
{
if (tones.isEmpty()) { return 0; }
qint64 d = 0;
for (const Tone &t : tones)
{
d += t.m_durationMs;
}
return d;
}
qint64 CSoundGenerator::readData(char *data, qint64 len)
{
if (len < 1) { return 0; }
if (m_endReached)
{
this->stop(); // all data read, we can stop output
return 0;
}
if (!this->isOpen()) return 0;
qint64 total = 0; // toal is used for the overflow when starting new wave again
while (len - total > 0)
{
const qint64 chunkSize = qMin((m_buffer.size() - m_position), (len - total));
memcpy(data + total, m_buffer.constData() + m_position, static_cast<size_t>(chunkSize));
m_position = (m_position + chunkSize) % m_buffer.size();
total += chunkSize;
if (m_position == 0 &&
(m_playMode == CNotificationSounds::Single || m_playMode == CNotificationSounds::SingleWithAutomaticDeletion))
{
m_endReached = true;
break;
}
}
return total;
}
qint64 CSoundGenerator::writeData(const char *data, qint64 len)
{
Q_UNUSED(data)
Q_UNUSED(len)
return 0;
}
qint64 CSoundGenerator::bytesAvailable() const
{
return m_buffer.size() + QIODevice::bytesAvailable();
}
QAudioFormat CSoundGenerator::defaultAudioFormat()
{
QAudioFormat format;
format.setSampleRate(44100);
format.setChannelCount(1);
format.setSampleSize(16); // 8 or 16 works
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::SignedInt);
return format;
}
QAudioDeviceInfo CSoundGenerator::findClosestOutputDevice(const CAudioDeviceInfo &audioDevice)
{
Q_ASSERT(audioDevice.getType() == CAudioDeviceInfo::OutputDevice);
const QString lookFor = audioDevice.getName().toLower();
QAudioDeviceInfo qtDevice = QAudioDeviceInfo::defaultOutputDevice();
if (lookFor.startsWith("default")) { return qtDevice; }
int score = 0;
for (const QAudioDeviceInfo &qd : QAudioDeviceInfo::availableDevices(QAudio::AudioOutput))
{
const QString cn = qd.deviceName().toLower();
if (lookFor == cn) { return qd; } // exact match
if (cn.length() < lookFor.length())
{
if (lookFor.contains(cn) && cn.length() > score)
{
qtDevice = qd;
score = cn.length();
}
}
else
{
if (cn.contains(lookFor) && lookFor.length() > score)
{
qtDevice = qd;
score = lookFor.length();
}
}
}
return qtDevice;
}
CSoundGenerator *CSoundGenerator::playSignal(int volume, const QList<CSoundGenerator::Tone> &tones, const QAudioDeviceInfo &device)
{
CSoundGenerator *generator = new CSoundGenerator(device, CSoundGenerator::defaultAudioFormat(), tones, CNotificationSounds::SingleWithAutomaticDeletion);
if (tones.isEmpty()) { return generator; } // that was easy
if (volume < 1) { return generator; }
if (generator->singleCyleDurationMs() < 10) { return generator; } // unable to hear
// play, and maybe clean up when done
generator->start(volume);
return generator;
}
void CSoundGenerator::playSignalInBackground(int volume, const QList<CSoundGenerator::Tone> &tones, const QAudioDeviceInfo &device)
{
CSoundGenerator *generator = new CSoundGenerator(device, CSoundGenerator::defaultAudioFormat(), tones, CNotificationSounds::SingleWithAutomaticDeletion);
if (tones.isEmpty()) { return; } // that was easy
if (volume < 1) { return; }
if (generator->singleCyleDurationMs() < 10) { return; } // unable to hear
CWorker *worker = CWorker::fromTask(QCoreApplication::instance(), "CSoundGenerator::playSignalInBackground", [generator, volume]()
{
generator->start(volume, false);
});
worker->then([generator]()
{
generator->deleteLater();
});
}
void CSoundGenerator::playSelcal(int volume, const CSelcal &selcal, const QAudioDeviceInfo &device)
{
QList<CSoundGenerator::Tone> tones;
if (selcal.isValid())
{
QList<CFrequency> frequencies = selcal.getFrequencies();
Q_ASSERT(frequencies.size() == 4);
const CTime oneSec(1000.0, CTimeUnit::ms());
Tone t1(frequencies.at(0), frequencies.at(1), oneSec);
Tone t2(CFrequency(), oneSec / 5.0);
Tone t3(frequencies.at(2), frequencies.at(3), oneSec);
tones << t1 << t2 << t3;
}
CSoundGenerator::playSignalInBackground(volume, tones, device);
}
void CSoundGenerator::playSelcal(int volume, const CSelcal &selcal, const CAudioDeviceInfo &audioDevice)
{
if (CSoundGenerator::s_selcalStarted.msecsTo(QDateTime::currentDateTimeUtc()) < 2500) return; // simple check not to play 2 SELCAL at the same time
CSoundGenerator::s_selcalStarted = QDateTime::currentDateTimeUtc();
CSoundGenerator::playSelcal(volume, selcal, CSoundGenerator::findClosestOutputDevice(audioDevice));
}
void CSoundGenerator::playFile(int volume, const QString &file, bool removeFileAfterPlaying)
{
if (!QFile::exists(file)) { return; }
Q_UNUSED(volume)
QSound::play(file);
// I cannot delete the file here, only after it has been played
if (removeFileAfterPlaying) { new CTimedFileDeleter(file, 1000 * 60, QCoreApplication::instance()); }
}
void CSoundGenerator::printAllQtSoundDevices(QTextStream &out)
{
out << "output:" << endl;
for (const QAudioDeviceInfo &qd : QAudioDeviceInfo::availableDevices(QAudio::AudioOutput))
{
out << qd.deviceName() << endl;
}
out << "input:" << endl;
for (const QAudioDeviceInfo &qd : QAudioDeviceInfo::availableDevices(QAudio::AudioInput))
{
out << qd.deviceName() << endl;
}
}
} // namespace

View File

@@ -1,254 +0,0 @@
/* Copyright (C) 2013
* swift project Community / Contributors
*
* This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level
* directory of this distribution. No part of swift project, including this file, may be copied, modified, propagated,
* or distributed except according to the terms contained in the LICENSE file.
*/
//! \file
#ifndef BLACKSOUND_SOUNDGENERATOR_H
#define BLACKSOUND_SOUNDGENERATOR_H
#include "blacksoundexport.h"
#include "blackmisc/aviation/selcal.h"
#include "blackmisc/audio/audiodeviceinfo.h"
#include "blackmisc/pq/time.h"
#include "blackmisc/pq/frequency.h"
#include "blackmisc/audio/notificationsounds.h"
#include <QIODevice>
#include <QThread>
#include <QDateTime>
#include <QAudioFormat>
#include <QAudioOutput>
#include <QAudioDeviceInfo>
#include <QMediaPlayer>
namespace BlackSound
{
//! Playing simple sounds
class BLACKSOUND_EXPORT CSoundGenerator : public QIODevice
{
Q_OBJECT
public:
//! Tone to be played
struct Tone
{
friend class CSoundGenerator;
public:
//! Play frequency f for t milliseconds
Tone(const BlackMisc::PhysicalQuantities::CFrequency &frequency, const BlackMisc::PhysicalQuantities::CTime &duration) :
m_frequencyHz(static_cast<int>(frequency.valueRounded(BlackMisc::PhysicalQuantities::CFrequencyUnit::Hz()))),
m_secondaryFrequencyHz(0),
m_durationMs(static_cast<qint64>(duration.valueRounded(BlackMisc::PhysicalQuantities::CTimeUnit::ms()))) {}
//! Play 2 frequencies f for t milliseconds
Tone(const BlackMisc::PhysicalQuantities::CFrequency &frequency, const BlackMisc::PhysicalQuantities::CFrequency &secondaryFrequency, const BlackMisc::PhysicalQuantities::CTime &duration) :
m_frequencyHz(static_cast<int>(frequency.valueRounded(BlackMisc::PhysicalQuantities::CFrequencyUnit::Hz()))),
m_secondaryFrequencyHz(static_cast<int>(secondaryFrequency.valueRounded(BlackMisc::PhysicalQuantities::CFrequencyUnit::Hz()))),
m_durationMs(static_cast<qint64>(duration.valueRounded(BlackMisc::PhysicalQuantities::CTimeUnit::ms()))) {}
private:
int m_frequencyHz; //!< first tone's frequency, use 0 for silence
int m_secondaryFrequencyHz; //!< second tone's frequency, or 0
qint64 m_durationMs; //!< How long to play (duration)
};
//! Constructor
//! \param device device
//! \param format audio format
//! \param tones list of Tones
//! \param mode play once?
//! \param parent
//! \see PlayMode
CSoundGenerator(const QAudioDeviceInfo &device, const QAudioFormat &format, const QList<Tone> &tones, BlackMisc::Audio::CNotificationSounds::PlayMode mode, QObject *parent = nullptr);
//! Constructor
//! \param tones list of Tones
//! \param mode play once?
//! \param parent
//! \see PlayMode
CSoundGenerator(const QList<Tone> &tones, BlackMisc::Audio::CNotificationSounds::PlayMode mode, QObject *parent = nullptr);
//! Destructor
virtual ~CSoundGenerator() override;
//! Set volume
//! \param volume 0..100
void setVolume(int volume)
{
m_audioOutput->setVolume(qreal(volume / 100.0));
}
//! Close device, buffer stays intact
void stop(bool destructor = false);
//! Duration of one cycle
qint64 singleCyleDurationMs() const { return calculateDurationMs(m_tones); }
//! \copydoc QIODevice::readData()
virtual qint64 readData(char *data, qint64 maxlen) override;
//! \copydoc QIODevice::writeData()
//! \remarks NOT(!) used here
virtual qint64 writeData(const char *data, qint64 len) override;
//! \copydoc QIODevice::bytesAvailable()
virtual qint64 bytesAvailable() const override;
//! \copydoc QIODevice::seek()
virtual bool seek(qint64 pos) override
{
return m_endReached ? false : QIODevice::seek(pos);
}
//! \copydoc QIODevice::atEnd()
virtual bool atEnd() const override
{
return m_endReached ? true : QIODevice::atEnd();
}
//! Default audio format fo play these sounds
static QAudioFormat defaultAudioFormat();
//! Find the closest Qt device to this audio device
//! \param audioDevice output audio device
//! \return
static QAudioDeviceInfo findClosestOutputDevice(const BlackMisc::Audio::CAudioDeviceInfo &audioDevice);
//! Play signal of tones once
//! \param volume 0-100
//! \param tones list of tones
//! \param device device to be used
//! \return generator used, important with SingleWithAutomaticDeletion automatically deleted
static CSoundGenerator *playSignal(int volume, const QList<Tone> &tones, const QAudioDeviceInfo &device = QAudioDeviceInfo::defaultOutputDevice());
//! Play signal of tones once
//! \param volume 0-100
//! \param tones list of tones
//! \param device device to be used
static void playSignalInBackground(int volume, const QList<CSoundGenerator::Tone> &tones, const QAudioDeviceInfo &device);
//! Play SELCAL tone
//! \param volume 0-100
//! \param selcal
//! \param device device to be used
//! \see BlackMisc::Aviation::CSelcal
static void playSelcal(int volume, const BlackMisc::Aviation::CSelcal &selcal, const QAudioDeviceInfo &device = QAudioDeviceInfo::defaultOutputDevice());
//! Play SELCAL tone
//! \param volume 0-100
//! \param selcal
//! \param audioDevice device to be used
//! \see BlackMisc::Aviation::CSelcal
static void playSelcal(int volume, const BlackMisc::Aviation::CSelcal &selcal, const BlackMisc::Audio::CAudioDeviceInfo &audioDevice);
//! One cycle of tones takes t milliseconds
BlackMisc::PhysicalQuantities::CTime oneCycleDurationMs() const
{
return BlackMisc::PhysicalQuantities::CTime(m_oneCycleDurationMs, BlackMisc::PhysicalQuantities::CTimeUnit::ms());
}
//! Play given file
//! \param volume 0-100
//! \param file
//! \param removeFileAfterPlaying delete the file, after it has been played
static void playFile(int volume, const QString &file, bool removeFileAfterPlaying);
//! For debugging purposes
static void printAllQtSoundDevices(QTextStream &qtout);
signals:
//! Device was closed
//! \remarks With singleShot the signal indicates that sound sequence has finished
void stopped();
//! Generator is stopping
void stopping();
public slots:
//! Play sound, open device
//! \param volume 0..100
//! \param pull pull/push, if false push mode
void start(int volume, bool pull = true);
private slots:
//! Push mode, timer expired
void pushTimerExpired();
private:
//! Generate tone data in internal buffer
void generateData();
private:
QList<Tone> m_tones; //!< tones to be played
qint64 m_position; //!< position in buffer
BlackMisc::Audio::CNotificationSounds::PlayMode m_playMode; //!< end data provisioning after playing all tones, play endless loop
bool m_endReached; //!< indicates end in combination with single play
qint64 m_oneCycleDurationMs; //!< how long is one cycle of tones
QByteArray m_buffer; //!< generated buffer for data
QAudioDeviceInfo m_device; //!< audio device
QAudioFormat m_audioFormat; //!< used format
QScopedPointer<QAudioOutput> m_audioOutput;
QTimer *m_pushTimer = nullptr; //!< Push mode timer
QIODevice *m_pushModeIODevice = nullptr; //!< IO device when used in push mode
QThread *m_ownThread = nullptr;
static QDateTime s_selcalStarted;
//! Header for saving .wav files
struct chunk
{
char id[4];
quint32 size;
};
//! Header for saving .wav files
struct RiffHeader
{
chunk descriptor; //!< "RIFF"
char type[4]; //!< "WAVE"
};
//! Header for saving .wav files
struct WaveHeader
{
chunk descriptor;
quint16 audioFormat;
quint16 numChannels;
quint32 sampleRate;
quint32 byteRate;
quint16 blockAlign;
quint16 bitsPerSample;
};
//! Header for saving .wav files
struct DataHeader
{
chunk descriptor;
};
//! Header for saving .wav files
struct CombinedHeader
{
RiffHeader riff;
WaveHeader wave;
DataHeader data;
};
//! Duration of these tones
static qint64 calculateDurationMs(const QList<Tone> &tones);
//! save buffer to wav file
bool saveToWavFile(const QString &fileName) const;
//! Write amplitude to buffer
//! \param amplitude value -1 .. 1
//! \param bufferPointer current buffer pointer
void writeAmplitudeToBuffer(const double amplitude, unsigned char *bufferPointer);
};
} //namespace
#endif // guard

View File

@@ -12,10 +12,11 @@
#include <QTimer>
using namespace BlackMisc;
using namespace BlackMisc::Audio;
namespace BlackSound
{
CThreadedTonePairPlayer::CThreadedTonePairPlayer(QObject *owner, const QString &name, const QAudioDeviceInfo &device)
CThreadedTonePairPlayer::CThreadedTonePairPlayer(QObject *owner, const QString &name, const CAudioDeviceInfo &device)
: CContinuousWorker(owner, name),
m_deviceInfo(device)
{ }
@@ -35,7 +36,26 @@ namespace BlackSound
void CThreadedTonePairPlayer::initialize()
{
CLogMessage(this).info(u"CThreadedTonePairPlayer for device '%1'") << m_deviceInfo.deviceName();
CLogMessage(this).info(u"CThreadedTonePairPlayer for device '%1'") << m_deviceInfo.getName();
QAudioDeviceInfo selectedDevice;
if (m_deviceInfo.isDefault())
{
selectedDevice = QAudioDeviceInfo::defaultOutputDevice();
}
else
{
// TODO: Add smart algorithm to find the device with exactly supports the audio format below
const QList<QAudioDeviceInfo> outputDevices = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput);
for (const QAudioDeviceInfo &d : outputDevices)
{
if (d.deviceName() == m_deviceInfo.getName())
{
selectedDevice = d;
}
}
}
QAudioFormat format;
format.setSampleRate(44100);
format.setChannelCount(1);
@@ -43,12 +63,12 @@ namespace BlackSound
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::SignedInt);
if (!m_deviceInfo.isFormatSupported(format))
if (!selectedDevice.isFormatSupported(format))
{
format = m_deviceInfo.nearestFormat(format);
format = selectedDevice.nearestFormat(format);
}
m_audioFormat = format;
m_audioOutput = new QAudioOutput(m_deviceInfo, m_audioFormat, this);
m_audioOutput = new QAudioOutput(selectedDevice, m_audioFormat, this);
connect(m_audioOutput, &QAudioOutput::stateChanged, this, &CThreadedTonePairPlayer::handleStateChanged);
}

View File

@@ -13,9 +13,9 @@
#include "blacksoundexport.h"
#include "blacksound/tonepair.h"
#include "blackmisc/audio/audiodeviceinfo.h"
#include "blackmisc/worker.h"
#include <QAudioDeviceInfo>
#include <QAudioOutput>
#include <QBuffer>
#include <QMap>
@@ -34,7 +34,7 @@ namespace BlackSound
public:
//! Constructor
CThreadedTonePairPlayer(QObject *owner, const QString &name, const QAudioDeviceInfo &device = QAudioDeviceInfo::defaultOutputDevice());
CThreadedTonePairPlayer(QObject *owner, const QString &name, const BlackMisc::Audio::CAudioDeviceInfo &device);
//! Destructor
virtual ~CThreadedTonePairPlayer() override;
@@ -61,7 +61,7 @@ namespace BlackSound
//! \li sample type == signed int
void writeAmplitudeToBuffer(double amplitude, unsigned char *bufferPointer);
QAudioDeviceInfo m_deviceInfo;
BlackMisc::Audio::CAudioDeviceInfo m_deviceInfo;
QAudioOutput *m_audioOutput = nullptr;
QByteArray m_bufferData;
QBuffer m_buffer;