mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-03-31 12:55:33 +08:00
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:
committed by
Mat Sutcliffe
parent
8656131eb1
commit
a2e3700739
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user