mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-03-24 07:55:35 +08:00
Sound generator, a class playing simple notification sounds (1/2 frequency tones).
These tones are generated "in memory", so no sound files ("wav") are needed.
New lib blacksound for utils around audio
This commit is contained in:
committed by
Mathew Sutcliffe
parent
e877c5c368
commit
f9225814f9
@@ -3,7 +3,6 @@
|
||||
QT += network dbus gui multimedia
|
||||
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
||||
|
||||
|
||||
TARGET = blacksound
|
||||
TEMPLATE = lib
|
||||
CONFIG += staticlib c++11
|
||||
|
||||
@@ -3,54 +3,73 @@
|
||||
#include <qmath.h>
|
||||
#include <qendian.h>
|
||||
#include <QAudioOutput>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
using namespace BlackMisc::Aviation;
|
||||
using namespace BlackMisc::PhysicalQuantities;
|
||||
using namespace BlackMisc::Voice;
|
||||
|
||||
namespace BlackSound
|
||||
{
|
||||
CSoundGenerator::CSoundGenerator(const QAudioFormat &format, const QList<Tone> &tones, bool singlePlay, QObject *parent)
|
||||
: QIODevice(parent), m_position(0), m_singlePlay(singlePlay), m_endReached(false), m_oneCycleDurationMs(calculateDurationMs(tones))
|
||||
CSoundGenerator::CSoundGenerator(const QAudioDeviceInfo &device, const QAudioFormat &format, const QList<Tone> &tones, 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(tones.size() > 0);
|
||||
this->generateData(format, tones);
|
||||
}
|
||||
|
||||
CSoundGenerator::CSoundGenerator(const QList<Tone> &tones, bool singlePlay, QObject *parent)
|
||||
: QIODevice(parent), m_position(0), m_singlePlay(singlePlay), m_endReached(false), m_oneCycleDurationMs(calculateDurationMs(tones))
|
||||
CSoundGenerator::CSoundGenerator(const QList<Tone> &tones, 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(tones.size() > 0);
|
||||
this->generateData(CSoundGenerator::defaultAudioFormat(), tones);
|
||||
}
|
||||
|
||||
CSoundGenerator::~CSoundGenerator()
|
||||
{
|
||||
this->close();
|
||||
this->stop(true);
|
||||
}
|
||||
|
||||
void CSoundGenerator::start()
|
||||
void CSoundGenerator::start(int volume)
|
||||
{
|
||||
if (volume < 1) return;
|
||||
if (this->m_buffer.isEmpty()) this->generateData();
|
||||
this->open(QIODevice::ReadOnly);
|
||||
this->m_audioOutput->setVolume(qreal(0.01 * volume));
|
||||
this->m_audioOutput->start(this); // pull
|
||||
}
|
||||
|
||||
void CSoundGenerator::stop()
|
||||
void CSoundGenerator::stop(bool destructor)
|
||||
{
|
||||
this->close();
|
||||
this->m_audioOutput->setVolume(0);
|
||||
this->m_audioOutput->stop();
|
||||
if (this->isOpen())
|
||||
{
|
||||
// 1. isOpen avoids redundant signals
|
||||
// 2. OK in destructor, see http://stackoverflow.com/a/14024955/356726
|
||||
emit this->stopped();
|
||||
this->close(); // close IO Device
|
||||
}
|
||||
this->m_position = 0;
|
||||
emit this->stopped();
|
||||
if (destructor) return;
|
||||
|
||||
// trigger own termination
|
||||
if (this->m_playMode == SingleWithAutomaticDeletion) this->deleteLater();
|
||||
}
|
||||
|
||||
void CSoundGenerator::generateData(const QAudioFormat &format, const QList<Tone> &tones)
|
||||
void CSoundGenerator::generateData()
|
||||
{
|
||||
Q_ASSERT(tones.size() > 0);
|
||||
|
||||
const int bytesPerSample = format.sampleSize() / 8;
|
||||
const int bytesForAllChannels = format.channelCount() * bytesPerSample;
|
||||
Q_ASSERT(this->m_tones.size() > 0);
|
||||
const int bytesPerSample = this->m_audioFormat.sampleSize() / 8;
|
||||
const int bytesForAllChannels = this->m_audioFormat.channelCount() * bytesPerSample;
|
||||
|
||||
qint64 totalLength = 0;
|
||||
foreach(Tone t, tones)
|
||||
foreach(Tone t, this->m_tones)
|
||||
{
|
||||
totalLength += format.sampleRate() * bytesForAllChannels * t.m_durationMs / 1000;
|
||||
totalLength += this->m_audioFormat.sampleRate() * bytesForAllChannels * t.m_durationMs / 1000;
|
||||
}
|
||||
|
||||
Q_ASSERT(totalLength % bytesForAllChannels == 0);
|
||||
@@ -59,16 +78,16 @@ namespace BlackSound
|
||||
m_buffer.resize(totalLength);
|
||||
unsigned char *bufferPointer = reinterpret_cast<unsigned char *>(m_buffer.data());
|
||||
|
||||
foreach(Tone t, tones)
|
||||
foreach(Tone t, this->m_tones)
|
||||
{
|
||||
qint64 lengthPerTone = format.sampleRate() * bytesForAllChannels * t.m_durationMs / 1000;
|
||||
qint64 lengthPerTone = this->m_audioFormat.sampleRate() * bytesForAllChannels * t.m_durationMs / 1000;
|
||||
int sampleIndexPerTone = 0;
|
||||
|
||||
while (lengthPerTone)
|
||||
{
|
||||
// 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 % format.sampleRate()) / format.sampleRate();
|
||||
const double pseudoTime = double(sampleIndexPerTone % this->m_audioFormat.sampleRate()) / this->m_audioFormat.sampleRate();
|
||||
double amplitude = 0; // silence
|
||||
if (t.m_frequencyHz > 10)
|
||||
{
|
||||
@@ -81,30 +100,30 @@ namespace BlackSound
|
||||
// the combination of two frequencies actually would have 2*amplitude,
|
||||
// but I have to normalize with amplitude -1 -> +1
|
||||
|
||||
for (int i = 0; i < format.channelCount(); ++i)
|
||||
for (int i = 0; i < this->m_audioFormat.channelCount(); ++i)
|
||||
{
|
||||
if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::UnSignedInt)
|
||||
if (this->m_audioFormat.sampleSize() == 8 && this->m_audioFormat.sampleType() == QAudioFormat::UnSignedInt)
|
||||
{
|
||||
const quint8 value = static_cast<quint8>((1.0 + amplitude) / 2 * 255);
|
||||
*reinterpret_cast<quint8 *>(bufferPointer) = value;
|
||||
}
|
||||
else if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::SignedInt)
|
||||
else if (this->m_audioFormat.sampleSize() == 8 && this->m_audioFormat.sampleType() == QAudioFormat::SignedInt)
|
||||
{
|
||||
const qint8 value = static_cast<qint8>(amplitude * 127);
|
||||
*reinterpret_cast<quint8 *>(bufferPointer) = value;
|
||||
}
|
||||
else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::UnSignedInt)
|
||||
else if (this->m_audioFormat.sampleSize() == 16 && this->m_audioFormat.sampleType() == QAudioFormat::UnSignedInt)
|
||||
{
|
||||
quint16 value = static_cast<quint16>((1.0 + amplitude) / 2 * 65535);
|
||||
if (format.byteOrder() == QAudioFormat::LittleEndian)
|
||||
if (this->m_audioFormat.byteOrder() == QAudioFormat::LittleEndian)
|
||||
qToLittleEndian<quint16>(value, bufferPointer);
|
||||
else
|
||||
qToBigEndian<quint16>(value, bufferPointer);
|
||||
}
|
||||
else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::SignedInt)
|
||||
else if (this->m_audioFormat.sampleSize() == 16 && this->m_audioFormat.sampleType() == QAudioFormat::SignedInt)
|
||||
{
|
||||
qint16 value = static_cast<qint16>(amplitude * 32767);
|
||||
if (format.byteOrder() == QAudioFormat::LittleEndian)
|
||||
if (this->m_audioFormat.byteOrder() == QAudioFormat::LittleEndian)
|
||||
qToLittleEndian<qint16>(value, bufferPointer);
|
||||
else
|
||||
qToBigEndian<qint16>(value, bufferPointer);
|
||||
@@ -131,19 +150,24 @@ namespace BlackSound
|
||||
|
||||
qint64 CSoundGenerator::readData(char *data, qint64 len)
|
||||
{
|
||||
if (this->m_endReached) return 0;
|
||||
if (len < 1) return 0;
|
||||
if (this->m_endReached)
|
||||
{
|
||||
this->stop(); // all data read, we can stop output
|
||||
return 0;
|
||||
}
|
||||
if (!this->isOpen()) return 0;
|
||||
qint64 total = 0;
|
||||
qint64 total = 0; // toal is used for the overflow when starting new wave again
|
||||
while (len - total > 0)
|
||||
{
|
||||
const qint64 chunk = qMin((m_buffer.size() - m_position), len - total);
|
||||
memcpy(data + total, m_buffer.constData() + m_position, chunk);
|
||||
this->m_position = (m_position + chunk) % m_buffer.size();
|
||||
total += chunk;
|
||||
if (m_singlePlay && m_position == 0)
|
||||
const qint64 chunkSize = qMin((m_buffer.size() - m_position), (len - total));
|
||||
memcpy(data + total, m_buffer.constData() + m_position, chunkSize);
|
||||
this->m_position = (m_position + chunkSize) % m_buffer.size();
|
||||
total += chunkSize;
|
||||
if (m_position == 0 &&
|
||||
(m_playMode == Single || m_playMode == SingleWithAutomaticDeletion))
|
||||
{
|
||||
this->m_endReached = true;
|
||||
this->stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -175,23 +199,50 @@ namespace BlackSound
|
||||
return format;
|
||||
}
|
||||
|
||||
/*
|
||||
* BlackMisc to Qt audio device
|
||||
*/
|
||||
QAudioDeviceInfo CSoundGenerator::findClosestOutputDevice(const BlackMisc::Voice::CAudioDevice &audioDevice)
|
||||
{
|
||||
Q_ASSERT(audioDevice.getType() == CAudioDevice::OutputDevice);
|
||||
const QString lookFor = audioDevice.getName().toLower();
|
||||
QAudioDeviceInfo qtDevice = QAudioDeviceInfo::defaultOutputDevice();
|
||||
if (lookFor.startsWith("default")) return qtDevice;
|
||||
int score = 0;
|
||||
foreach(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;
|
||||
|
||||
}
|
||||
|
||||
void CSoundGenerator::playSignal(qint32 volume, const QList<CSoundGenerator::Tone> &tones, QAudioDeviceInfo device)
|
||||
{
|
||||
if (tones.isEmpty()) return; // that was easy
|
||||
if (volume < 1) return;
|
||||
qint64 timeOut = calculateDurationMs(tones);
|
||||
if (timeOut < 10) return; // unable to hear
|
||||
QAudioOutput *audioOutput = new QAudioOutput(device, CSoundGenerator::defaultAudioFormat());
|
||||
CSoundGenerator *generator = new CSoundGenerator(tones, true, audioOutput);
|
||||
CSoundGenerator *generator = new CSoundGenerator(device, CSoundGenerator::defaultAudioFormat(), tones, CSoundGenerator::SingleWithAutomaticDeletion);
|
||||
if (generator->singleCyleDurationMs() < 10) return; // unable to hear
|
||||
|
||||
// top and clean uo when done
|
||||
connect(generator, &CSoundGenerator::stopped, audioOutput, &QAudioOutput::stop);
|
||||
connect(generator, &CSoundGenerator::stopped, audioOutput, &QAudioOutput::deleteLater);
|
||||
|
||||
double vol = volume / 100.0;
|
||||
audioOutput->setVolume(vol);
|
||||
generator->start();
|
||||
audioOutput->start(generator);
|
||||
generator->start(volume);
|
||||
}
|
||||
|
||||
void CSoundGenerator::playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, QAudioDeviceInfo device)
|
||||
@@ -208,4 +259,9 @@ namespace BlackSound
|
||||
CSoundGenerator::playSignal(volume, tones, device);
|
||||
}
|
||||
|
||||
void CSoundGenerator::playSelcal(qint32 volume, const CSelcal &selcal, const CAudioDevice &audioDevice)
|
||||
{
|
||||
CSoundGenerator::playSelcal(volume, selcal, CSoundGenerator::findClosestOutputDevice(audioDevice));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -7,26 +7,43 @@
|
||||
#define BLACKSOUND_SOUNDGENERATOR_H
|
||||
|
||||
#include "blackmisc/avselcal.h"
|
||||
#include "blackmisc/vaudiodevice.h"
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QAudioFormat>
|
||||
#include <QAudioOutput>
|
||||
#include <QAudioDeviceInfo>
|
||||
|
||||
namespace BlackSound
|
||||
{
|
||||
|
||||
/*!
|
||||
* \brief Paying simple sounds
|
||||
*/
|
||||
class CSoundGenerator : public QIODevice
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
/*!
|
||||
* \brief How to play
|
||||
*/
|
||||
enum PlayMode
|
||||
{
|
||||
Single,
|
||||
SingleWithAutomaticDeletion,
|
||||
EndlessLoop
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Tone to be played
|
||||
*/
|
||||
struct Tone
|
||||
{
|
||||
int m_frequencyHz;
|
||||
int m_secondaryFrequencyHz;
|
||||
qint64 m_durationMs;
|
||||
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 */
|
||||
|
||||
/*!
|
||||
* \brief Play frequency f for t milliseconds
|
||||
@@ -37,41 +54,42 @@ namespace BlackSound
|
||||
* \brief Play 2 frequencies f for t milliseconds
|
||||
*/
|
||||
Tone(int frequencyHz, int secondaryFrequencyHz, qint64 durationMs) : m_frequencyHz(frequencyHz), m_secondaryFrequencyHz(secondaryFrequencyHz), m_durationMs(durationMs) {}
|
||||
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Constructor
|
||||
* \param format
|
||||
* \param device device
|
||||
* \param format audio format
|
||||
* \param tones list of Tones
|
||||
* \param singlePlay play once?
|
||||
* \param mode play once?
|
||||
* \param parent
|
||||
* \see PlayMode
|
||||
*/
|
||||
CSoundGenerator(const QAudioFormat &format, const QList<Tone> &tones, bool singlePlay, QObject *parent);
|
||||
|
||||
CSoundGenerator(const QAudioDeviceInfo &device, const QAudioFormat &format, const QList<Tone> &tones, PlayMode mode, QObject *parent = nullptr);
|
||||
|
||||
/*!
|
||||
* \brief Constructor
|
||||
* \param tones list of Tones
|
||||
* \param singlePlay play once?
|
||||
* \param mode play once?
|
||||
* \param parent
|
||||
* \see PlayMode
|
||||
*/
|
||||
CSoundGenerator(const QList<Tone> &tones, bool singlePlay, QObject *parent);
|
||||
CSoundGenerator(const QList<Tone> &tones, PlayMode mode, QObject *parent = nullptr);
|
||||
|
||||
/*!
|
||||
* Destructor
|
||||
*/
|
||||
~CSoundGenerator();
|
||||
|
||||
/*!
|
||||
* \brief Open device
|
||||
*/
|
||||
void start();
|
||||
|
||||
/*!
|
||||
* \brief Close device, buffer stays intact
|
||||
*/
|
||||
void stop();
|
||||
void stop(bool destructor = false);
|
||||
|
||||
/*!
|
||||
* \brief sDuration of one cycle
|
||||
*/
|
||||
qint64 singleCyleDurationMs() const { return calculateDurationMs(this->m_tones); }
|
||||
|
||||
/*!
|
||||
* \copydoc QIODevice::readData()
|
||||
@@ -111,6 +129,13 @@ namespace BlackSound
|
||||
*/
|
||||
static QAudioFormat defaultAudioFormat();
|
||||
|
||||
/*!
|
||||
* \brief Find the closest Qt device to this audio device
|
||||
* \param audioDevice output audio device
|
||||
* \return
|
||||
*/
|
||||
static QAudioDeviceInfo findClosestOutputDevice(const BlackMisc::Voice::CAudioDevice &audioDevice);
|
||||
|
||||
/*!
|
||||
* \brief Play signal of tones once
|
||||
* \param volume 0-100
|
||||
@@ -128,18 +153,14 @@ namespace BlackSound
|
||||
*/
|
||||
static void playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, QAudioDeviceInfo device = QAudioDeviceInfo::defaultOutputDevice());
|
||||
|
||||
signals:
|
||||
/*!
|
||||
* \brief Device was closed
|
||||
* \remarks With singleShot the signal indicates that sound sequence has finished
|
||||
* \brief Play SELCAL tone
|
||||
* \param volume 0-100
|
||||
* \param selcal
|
||||
* \param audioDevice device to be used
|
||||
* \see BlackMisc::Aviation::CSelcal
|
||||
*/
|
||||
void stopped();
|
||||
|
||||
private:
|
||||
/*!
|
||||
* \brief Generate tone data in internal buffer
|
||||
*/
|
||||
void generateData(const QAudioFormat &format, const QList<Tone> &tones);
|
||||
static void playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, const BlackMisc::Voice::CAudioDevice &audioDevice);
|
||||
|
||||
/*!
|
||||
* \brief One cycle of tones takes t milliseconds
|
||||
@@ -149,12 +170,36 @@ namespace BlackSound
|
||||
return this->m_oneCycleDurationMs;
|
||||
}
|
||||
|
||||
signals:
|
||||
/*!
|
||||
* \brief Device was closed
|
||||
* \remarks With singleShot the signal indicates that sound sequence has finished
|
||||
*/
|
||||
void stopped();
|
||||
|
||||
public slots:
|
||||
/*!
|
||||
* \brief Play sound, open device
|
||||
* \param volume 0..100
|
||||
*/
|
||||
void start(int volume);
|
||||
|
||||
private:
|
||||
/*!
|
||||
* \brief Generate tone data in internal buffer
|
||||
*/
|
||||
void generateData();
|
||||
|
||||
private:
|
||||
QList<Tone> m_tones; /*! tones to be played */
|
||||
qint64 m_position; /*!< position in buffer */
|
||||
bool m_singlePlay; /*!< end data provisioning after playing all tones */
|
||||
bool 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;
|
||||
QByteArray m_buffer; /*!< generated buffer for data */
|
||||
QAudioDeviceInfo m_device; /*!< audio device */
|
||||
QAudioFormat m_audioFormat; /*!< used format */
|
||||
QScopedPointer<QAudioOutput> m_audioOutput;
|
||||
|
||||
/*!
|
||||
* \brief Duration of these tones
|
||||
@@ -163,4 +208,6 @@ namespace BlackSound
|
||||
|
||||
};
|
||||
} //namespace
|
||||
|
||||
|
||||
#endif // guard
|
||||
|
||||
Reference in New Issue
Block a user