mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-04-14 08:45:36 +08:00
Added sub project for sound refs #107
Added sound generator, which can play a series of tones
This commit is contained in:
committed by
Mathew Sutcliffe
parent
d360fe5ce2
commit
6d99ddf9b0
@@ -7,6 +7,7 @@ include (externals.pri)
|
|||||||
WITH_BLACKMISC = ON
|
WITH_BLACKMISC = ON
|
||||||
WITH_BLACKCORE = ON
|
WITH_BLACKCORE = ON
|
||||||
WITH_BLACKGUI = ON
|
WITH_BLACKGUI = ON
|
||||||
|
WITH_BLACKSOUND = ON
|
||||||
WITH_SAMPLES = ON
|
WITH_SAMPLES = ON
|
||||||
WITH_UNITTESTS = ON
|
WITH_UNITTESTS = ON
|
||||||
|
|
||||||
@@ -28,6 +29,10 @@ equals(WITH_BLACKGUI, ON) {
|
|||||||
SUBDIRS += src/blackgui
|
SUBDIRS += src/blackgui
|
||||||
}
|
}
|
||||||
|
|
||||||
|
equals(WITH_BLACKSOUND, ON) {
|
||||||
|
SUBDIRS += src/blacksound
|
||||||
|
}
|
||||||
|
|
||||||
equals(WITH_DRIVER_FSX, ON) {
|
equals(WITH_DRIVER_FSX, ON) {
|
||||||
SUBDIRS += src/driver/fsx/driver_fsx.pro
|
SUBDIRS += src/driver/fsx/driver_fsx.pro
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/blacksound/blacksound.pro
Normal file
27
src/blacksound/blacksound.pro
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# quick is required for metadata registration
|
||||||
|
|
||||||
|
QT += network dbus gui multimedia
|
||||||
|
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
||||||
|
|
||||||
|
|
||||||
|
TARGET = blacksound
|
||||||
|
TEMPLATE = lib
|
||||||
|
CONFIG += staticlib c++11
|
||||||
|
|
||||||
|
INCLUDEPATH += ..
|
||||||
|
DEPENDPATH += . ..
|
||||||
|
|
||||||
|
# PRECOMPILED_HEADER = stdpch.h
|
||||||
|
precompile_header:!isEmpty(PRECOMPILED_HEADER) {
|
||||||
|
DEFINES += USING_PCH
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINES += LOG_IN_FILE
|
||||||
|
|
||||||
|
win32:!win32-g++*: PRE_TARGETDEPS += ../../lib/blackmisc.lib
|
||||||
|
else: PRE_TARGETDEPS += ../../lib/libblackmisc.a
|
||||||
|
|
||||||
|
HEADERS += *.h
|
||||||
|
SOURCES += *.cpp
|
||||||
|
DESTDIR = ../../lib
|
||||||
|
OTHER_FILES +=
|
||||||
180
src/blacksound/soundgenerator.cpp
Normal file
180
src/blacksound/soundgenerator.cpp
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#include "soundgenerator.h"
|
||||||
|
#include <math.h>
|
||||||
|
#include <qmath.h>
|
||||||
|
#include <qendian.h>
|
||||||
|
#include <QAudioOutput>
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
Q_ASSERT(tones.size() > 0);
|
||||||
|
this->generateData(CSoundGenerator::defaultAudioFormat(), tones);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSoundGenerator::~CSoundGenerator()
|
||||||
|
{
|
||||||
|
this->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSoundGenerator::start()
|
||||||
|
{
|
||||||
|
this->open(QIODevice::ReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSoundGenerator::stop()
|
||||||
|
{
|
||||||
|
this->close();
|
||||||
|
this->m_position = 0;
|
||||||
|
emit this->stopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSoundGenerator::generateData(const QAudioFormat &format, const QList<Tone> &tones)
|
||||||
|
{
|
||||||
|
Q_ASSERT(tones.size() > 0);
|
||||||
|
|
||||||
|
const int bytesPerSample = format.sampleSize() / 8;
|
||||||
|
const int bytesForAllChannels = format.channelCount() * bytesPerSample;
|
||||||
|
|
||||||
|
qint64 totalLength = 0;
|
||||||
|
foreach(Tone t, tones)
|
||||||
|
{
|
||||||
|
totalLength += format.sampleRate() * bytesForAllChannels * t.m_durationMs / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(totalLength % bytesForAllChannels == 0);
|
||||||
|
Q_UNUSED(bytesForAllChannels) // suppress warning in release builds
|
||||||
|
|
||||||
|
m_buffer.resize(totalLength);
|
||||||
|
unsigned char *bufferPointer = reinterpret_cast<unsigned char *>(m_buffer.data());
|
||||||
|
|
||||||
|
foreach(Tone t, tones)
|
||||||
|
{
|
||||||
|
qint64 lengthPerTone = format.sampleRate() * bytesForAllChannels * t.m_durationMs / 1000;
|
||||||
|
int sampleIndexPerTone = 0;
|
||||||
|
|
||||||
|
while (lengthPerTone)
|
||||||
|
{
|
||||||
|
const qreal x = qSin(2 * M_PI * t.m_frequencyHz * qreal(sampleIndexPerTone % format.sampleRate()) / format.sampleRate());
|
||||||
|
for (int i = 0; i < format.channelCount(); ++i)
|
||||||
|
{
|
||||||
|
if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::UnSignedInt)
|
||||||
|
{
|
||||||
|
const quint8 value = static_cast<quint8>((1.0 + x) / 2 * 255);
|
||||||
|
*reinterpret_cast<quint8 *>(bufferPointer) = value;
|
||||||
|
}
|
||||||
|
else if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::SignedInt)
|
||||||
|
{
|
||||||
|
const qint8 value = static_cast<qint8>(x * 127);
|
||||||
|
*reinterpret_cast<quint8 *>(bufferPointer) = value;
|
||||||
|
}
|
||||||
|
else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::UnSignedInt)
|
||||||
|
{
|
||||||
|
quint16 value = static_cast<quint16>((1.0 + x) / 2 * 65535);
|
||||||
|
if (format.byteOrder() == QAudioFormat::LittleEndian)
|
||||||
|
qToLittleEndian<quint16>(value, bufferPointer);
|
||||||
|
else
|
||||||
|
qToBigEndian<quint16>(value, bufferPointer);
|
||||||
|
}
|
||||||
|
else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::SignedInt)
|
||||||
|
{
|
||||||
|
qint16 value = static_cast<qint16>(x * 32767);
|
||||||
|
if (format.byteOrder() == QAudioFormat::LittleEndian)
|
||||||
|
qToLittleEndian<qint16>(value, bufferPointer);
|
||||||
|
else
|
||||||
|
qToBigEndian<qint16>(value, bufferPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferPointer += bytesPerSample;
|
||||||
|
lengthPerTone -= bytesPerSample;
|
||||||
|
}
|
||||||
|
++sampleIndexPerTone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 CSoundGenerator::calculateDurationMs(const QList<CSoundGenerator::Tone> &tones)
|
||||||
|
{
|
||||||
|
if (tones.isEmpty()) return 0;
|
||||||
|
qint64 d = 0;
|
||||||
|
foreach(Tone t, tones)
|
||||||
|
{
|
||||||
|
d += t.m_durationMs;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 CSoundGenerator::readData(char *data, qint64 len)
|
||||||
|
{
|
||||||
|
if (this->m_endReached) return 0;
|
||||||
|
if (!this->isOpen()) return 0;
|
||||||
|
qint64 total = 0;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
this->m_endReached = true;
|
||||||
|
this->stop();
|
||||||
|
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);
|
||||||
|
format.setCodec("audio/pcm");
|
||||||
|
format.setByteOrder(QAudioFormat::LittleEndian);
|
||||||
|
format.setSampleType(QAudioFormat::SignedInt);
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// top and clean uo when done
|
||||||
|
connect(generator, &CSoundGenerator::stopped, audioOutput, &QAudioOutput::stop);
|
||||||
|
connect(generator, &CSoundGenerator::stopped, audioOutput, &QAudioOutput::deleteLater);
|
||||||
|
|
||||||
|
qreal vol = volume / 100.0;
|
||||||
|
audioOutput->setVolume(vol);
|
||||||
|
generator->start();
|
||||||
|
audioOutput->start(generator);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
149
src/blacksound/soundgenerator.h
Normal file
149
src/blacksound/soundgenerator.h
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/* Copyright (C) 2013 VATSIM Community / contributors
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
#ifndef BLACKSOUND_SOUNDGENERATOR_H
|
||||||
|
#define BLACKSOUND_SOUNDGENERATOR_H
|
||||||
|
|
||||||
|
#include <QIODevice>
|
||||||
|
#include <QAudioFormat>
|
||||||
|
#include <QAudioDeviceInfo>
|
||||||
|
|
||||||
|
namespace BlackSound
|
||||||
|
{
|
||||||
|
|
||||||
|
class CSoundGenerator : public QIODevice
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
/*!
|
||||||
|
* \brief Tone to be played
|
||||||
|
*/
|
||||||
|
struct Tone
|
||||||
|
{
|
||||||
|
int m_frequencyHz;
|
||||||
|
qint64 m_durationMs;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Play frequency f for t milliseconds
|
||||||
|
*/
|
||||||
|
Tone(int frequencyHz, qint64 durationMs) : m_frequencyHz(frequencyHz), m_durationMs(durationMs) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Constructor
|
||||||
|
* \param format
|
||||||
|
* \param tones list of Tones
|
||||||
|
* \param singlePlay play once?
|
||||||
|
* \param parent
|
||||||
|
*/
|
||||||
|
CSoundGenerator(const QAudioFormat &format, const QList<Tone> &tones, bool singlePlay, QObject *parent);
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Constructor
|
||||||
|
* \param tones list of Tones
|
||||||
|
* \param singlePlay play once?
|
||||||
|
* \param parent
|
||||||
|
*/
|
||||||
|
CSoundGenerator(const QList<Tone> &tones, bool singlePlay, QObject *parent);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Destructor
|
||||||
|
*/
|
||||||
|
~CSoundGenerator();
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Open device
|
||||||
|
*/
|
||||||
|
void start();
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Close device, buffer stays intact
|
||||||
|
*/
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \copydoc QIODevice::readData()
|
||||||
|
*/
|
||||||
|
qint64 readData(char *data, qint64 maxlen);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \copydoc QIODevice::writeData()
|
||||||
|
* \remarks NOT(!) used here
|
||||||
|
*/
|
||||||
|
qint64 writeData(const char *data, qint64 len);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \copydoc QIODevice::bytesAvailable()
|
||||||
|
*/
|
||||||
|
qint64 bytesAvailable() const;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \copydoc QIODevice::seek()
|
||||||
|
*/
|
||||||
|
virtual bool seek(qint64 pos)
|
||||||
|
{
|
||||||
|
return this->m_endReached ? false : QIODevice::seek(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \copydoc QIODevice::atEnd()
|
||||||
|
*/
|
||||||
|
virtual bool atEnd() const
|
||||||
|
{
|
||||||
|
return this->m_endReached ? true : QIODevice::atEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief One cycle of tones takes t milliseconds
|
||||||
|
*/
|
||||||
|
qint64 oneCycleDurationMs() const
|
||||||
|
{
|
||||||
|
return this->m_oneCycleDurationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Default audio format fo play these sounds
|
||||||
|
* \return
|
||||||
|
*/
|
||||||
|
static QAudioFormat defaultAudioFormat();
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Play signal of tones once
|
||||||
|
* \param volume 0-100
|
||||||
|
* \param tones list of tones
|
||||||
|
* \param device device to be used
|
||||||
|
*/
|
||||||
|
static void playSignal(qint32 volume, const QList<Tone> &tones, QAudioDeviceInfo device = QAudioDeviceInfo::defaultOutputDevice());
|
||||||
|
|
||||||
|
signals:
|
||||||
|
/*!
|
||||||
|
* \brief Device was closed
|
||||||
|
* \remarks With singleShot the signal indicates that sound sequence has finished
|
||||||
|
*/
|
||||||
|
void stopped();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/*!
|
||||||
|
* \brief Generate tone data in internal buffer
|
||||||
|
*/
|
||||||
|
void generateData(const QAudioFormat &format, const QList<Tone> &tones);
|
||||||
|
|
||||||
|
private:
|
||||||
|
qint64 m_position; /*!< position in buffer */
|
||||||
|
bool m_singlePlay; /*!< end data provisioning after playing all tones */
|
||||||
|
bool m_endReached; /*!< indicates end in combination with single play */
|
||||||
|
qint64 m_oneCycleDurationMs; /*!< how long is one cycle of tones */
|
||||||
|
QByteArray m_buffer;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Duration of these tones
|
||||||
|
*/
|
||||||
|
static qint64 calculateDurationMs(const QList<Tone> &tones);
|
||||||
|
|
||||||
|
};
|
||||||
|
} //namespace
|
||||||
|
#endif // guard
|
||||||
Reference in New Issue
Block a user