From 6d99ddf9b0ef3dec43a129b8421be3c85660d632 Mon Sep 17 00:00:00 2001 From: Klaus Basan Date: Wed, 5 Feb 2014 20:42:59 +0000 Subject: [PATCH] Added sub project for sound refs #107 Added sound generator, which can play a series of tones --- client.pro | 5 + src/blacksound/blacksound.pro | 27 +++++ src/blacksound/soundgenerator.cpp | 180 ++++++++++++++++++++++++++++++ src/blacksound/soundgenerator.h | 149 +++++++++++++++++++++++++ 4 files changed, 361 insertions(+) create mode 100644 src/blacksound/blacksound.pro create mode 100644 src/blacksound/soundgenerator.cpp create mode 100644 src/blacksound/soundgenerator.h diff --git a/client.pro b/client.pro index dd9885053..dc99aa315 100644 --- a/client.pro +++ b/client.pro @@ -7,6 +7,7 @@ include (externals.pri) WITH_BLACKMISC = ON WITH_BLACKCORE = ON WITH_BLACKGUI = ON +WITH_BLACKSOUND = ON WITH_SAMPLES = ON WITH_UNITTESTS = ON @@ -28,6 +29,10 @@ equals(WITH_BLACKGUI, ON) { SUBDIRS += src/blackgui } +equals(WITH_BLACKSOUND, ON) { + SUBDIRS += src/blacksound +} + equals(WITH_DRIVER_FSX, ON) { SUBDIRS += src/driver/fsx/driver_fsx.pro } diff --git a/src/blacksound/blacksound.pro b/src/blacksound/blacksound.pro new file mode 100644 index 000000000..4ac0b0a9c --- /dev/null +++ b/src/blacksound/blacksound.pro @@ -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 += diff --git a/src/blacksound/soundgenerator.cpp b/src/blacksound/soundgenerator.cpp new file mode 100644 index 000000000..bc9d51b08 --- /dev/null +++ b/src/blacksound/soundgenerator.cpp @@ -0,0 +1,180 @@ +#include "soundgenerator.h" +#include +#include +#include +#include + +namespace BlackSound +{ + CSoundGenerator::CSoundGenerator(const QAudioFormat &format, const QList &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 &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 &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(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((1.0 + x) / 2 * 255); + *reinterpret_cast(bufferPointer) = value; + } + else if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::SignedInt) + { + const qint8 value = static_cast(x * 127); + *reinterpret_cast(bufferPointer) = value; + } + else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::UnSignedInt) + { + quint16 value = static_cast((1.0 + x) / 2 * 65535); + if (format.byteOrder() == QAudioFormat::LittleEndian) + qToLittleEndian(value, bufferPointer); + else + qToBigEndian(value, bufferPointer); + } + else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::SignedInt) + { + qint16 value = static_cast(x * 32767); + if (format.byteOrder() == QAudioFormat::LittleEndian) + qToLittleEndian(value, bufferPointer); + else + qToBigEndian(value, bufferPointer); + } + + bufferPointer += bytesPerSample; + lengthPerTone -= bytesPerSample; + } + ++sampleIndexPerTone; + } + } + } + + qint64 CSoundGenerator::calculateDurationMs(const QList &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 &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 diff --git a/src/blacksound/soundgenerator.h b/src/blacksound/soundgenerator.h new file mode 100644 index 000000000..756cf53d5 --- /dev/null +++ b/src/blacksound/soundgenerator.h @@ -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 +#include +#include + +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 &tones, bool singlePlay, QObject *parent); + + + /*! + * \brief Constructor + * \param tones list of Tones + * \param singlePlay play once? + * \param parent + */ + CSoundGenerator(const QList &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 &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 &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 &tones); + + }; +} //namespace +#endif // guard