diff --git a/CMakeLists.txt b/CMakeLists.txt index acb187c..f0684a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -253,6 +253,8 @@ set (wsjtx_CXXSRCS wsprnet.cpp WSPRBandHopping.cpp TransmitTextEdit.cpp + WaveUtils.cpp + WaveFile.cpp AudioDecoder.cpp NotificationAudio.cpp ) diff --git a/NotificationAudio.cpp b/NotificationAudio.cpp index 23f67ea..82e4014 100644 --- a/NotificationAudio.cpp +++ b/NotificationAudio.cpp @@ -1,11 +1,13 @@ #include "NotificationAudio.h" +#include "WaveFile.h" NotificationAudio::NotificationAudio(QObject *parent): QObject(parent) { m_stream = new SoundOutput(); - m_decoder = new AudioDecoder(this); + //m_decoder = new AudioDecoder(this); + m_file = new WaveFile(this); } NotificationAudio::~NotificationAudio(){ @@ -13,16 +15,31 @@ NotificationAudio::~NotificationAudio(){ } void NotificationAudio::setDevice(const QAudioDeviceInfo &device, unsigned channels, unsigned msBuffer){ + m_device = device; + m_channels = channels; + m_msBuffer = msBuffer; m_stream->setFormat(device, channels, msBuffer); - m_decoder->init(m_stream->format()); + //m_decoder->init(m_stream->format()); } void NotificationAudio::play(const QString &filePath){ - m_decoder->start(filePath); - m_stream->restart(m_decoder); + //m_decoder->start(filePath); + //m_stream->restart(m_decoder); + if(m_file->isOpen()){ + m_file->close(); + } + if(m_file->open(filePath)){ + m_file->seek(0); + m_stream->setDeviceFormat(m_device, m_file->fileFormat(), m_channels, m_msBuffer); + m_stream->restart(m_file); + } } void NotificationAudio::stop(){ - m_decoder->stop(); + //m_decoder->stop(); m_stream->stop(); + + if(m_file->isOpen()){ + m_file->close(); + } } diff --git a/NotificationAudio.h b/NotificationAudio.h index dc37023..755f9c5 100644 --- a/NotificationAudio.h +++ b/NotificationAudio.h @@ -11,8 +11,10 @@ #include "AudioDevice.hpp" #include "AudioDecoder.h" +#include "WaveFile.h" #include "soundout.h" + class NotificationAudio : public QObject { @@ -30,6 +32,10 @@ public slots: private: QPointer m_stream; QPointer m_decoder; + QPointer m_file; + QAudioDeviceInfo m_device; + unsigned m_channels; + unsigned m_msBuffer; }; #endif // NOTIFICATIONAUDIO_H diff --git a/WaveFile.cpp b/WaveFile.cpp new file mode 100644 index 0000000..d028255 --- /dev/null +++ b/WaveFile.cpp @@ -0,0 +1,150 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include "WaveFile.h" + +struct chunk +{ + char id[4]; + quint32 size; +}; + +struct RIFFHeader +{ + chunk descriptor; // "RIFF" + char type[4]; // "WAVE" +}; + +struct WAVEHeader +{ + chunk descriptor; + quint16 audioFormat; + quint16 numChannels; + quint32 sampleRate; + quint32 byteRate; + quint16 blockAlign; + quint16 bitsPerSample; +}; + +struct DATAHeader +{ + chunk descriptor; +}; + +struct CombinedHeader +{ + RIFFHeader riff; + WAVEHeader wave; +}; + +WaveFile::WaveFile(QObject *parent) + : QFile(parent) + , m_headerLength(0) +{ + +} + +bool WaveFile::open(const QString &fileName) +{ + close(); + setFileName(fileName); + return QFile::open(QIODevice::ReadOnly) && readHeader(); +} + +const QAudioFormat &WaveFile::fileFormat() const +{ + return m_fileFormat; +} + +qint64 WaveFile::headerLength() const +{ +return m_headerLength; +} + +bool WaveFile::readHeader() +{ + seek(0); + CombinedHeader header; + bool result = read(reinterpret_cast(&header), sizeof(CombinedHeader)) == sizeof(CombinedHeader); + if (result) { + if ((memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0 + || memcmp(&header.riff.descriptor.id, "RIFX", 4) == 0) + && memcmp(&header.riff.type, "WAVE", 4) == 0 + && memcmp(&header.wave.descriptor.id, "fmt ", 4) == 0 + && (header.wave.audioFormat == 1 || header.wave.audioFormat == 0)) { + + // Read off remaining header information + DATAHeader dataHeader; + + if (qFromLittleEndian(header.wave.descriptor.size) > sizeof(WAVEHeader)) { + // Extended data available + quint16 extraFormatBytes; + if (peek((char*)&extraFormatBytes, sizeof(quint16)) != sizeof(quint16)) + return false; + const qint64 throwAwayBytes = sizeof(quint16) + qFromLittleEndian(extraFormatBytes); + if (read(throwAwayBytes).size() != throwAwayBytes) + return false; + } + + if (read((char*)&dataHeader, sizeof(DATAHeader)) != sizeof(DATAHeader)) + return false; + + // Establish format + if (memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0) + m_fileFormat.setByteOrder(QAudioFormat::LittleEndian); + else + m_fileFormat.setByteOrder(QAudioFormat::BigEndian); + + int bps = qFromLittleEndian(header.wave.bitsPerSample); + m_fileFormat.setChannelCount(qFromLittleEndian(header.wave.numChannels)); + m_fileFormat.setCodec("audio/pcm"); + m_fileFormat.setSampleRate(qFromLittleEndian(header.wave.sampleRate)); + m_fileFormat.setSampleSize(qFromLittleEndian(header.wave.bitsPerSample)); + m_fileFormat.setSampleType(bps == 8 ? QAudioFormat::UnSignedInt : QAudioFormat::SignedInt); + } else { + result = false; + } + } + m_headerLength = pos(); + return result; +} diff --git a/WaveFile.h b/WaveFile.h new file mode 100644 index 0000000..85e80a5 --- /dev/null +++ b/WaveFile.h @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WAVEFILE_H +#define WAVEFILE_H + +#include +#include +#include + +class WaveFile : public QFile +{ +public: + WaveFile(QObject *parent = 0); + + using QFile::open; + bool open(const QString &fileName); + const QAudioFormat &fileFormat() const; + qint64 headerLength() const; + +private: + bool readHeader(); + +private: + QAudioFormat m_fileFormat; + qint64 m_headerLength; +}; + +#endif // WAVEFILE_H diff --git a/WaveUtils.cpp b/WaveUtils.cpp new file mode 100644 index 0000000..49f31b3 --- /dev/null +++ b/WaveUtils.cpp @@ -0,0 +1,138 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include "WaveUtils.h" + +qint64 audioDuration(const QAudioFormat &format, qint64 bytes) +{ + return (bytes * 1000000) / + (format.sampleRate() * format.channelCount() * (format.sampleSize() / 8)); +} + +qint64 audioLength(const QAudioFormat &format, qint64 microSeconds) +{ + qint64 result = (format.sampleRate() * format.channelCount() * (format.sampleSize() / 8)) + * microSeconds / 1000000; + result -= result % (format.channelCount() * format.sampleSize()); + return result; +} + +qreal nyquistFrequency(const QAudioFormat &format) +{ + return format.sampleRate() / 2; +} + +QString formatToString(const QAudioFormat &format) +{ + QString result; + + if (QAudioFormat() != format) { + if (format.codec() == "audio/pcm") { + Q_ASSERT(format.sampleType() == QAudioFormat::SignedInt); + + const QString formatEndian = (format.byteOrder() == QAudioFormat::LittleEndian) + ? QString("LE") : QString("BE"); + + QString formatType; + switch (format.sampleType()) { + case QAudioFormat::SignedInt: + formatType = "signed"; + break; + case QAudioFormat::UnSignedInt: + formatType = "unsigned"; + break; + case QAudioFormat::Float: + formatType = "float"; + break; + case QAudioFormat::Unknown: + formatType = "unknown"; + break; + } + + QString formatChannels = QString("%1 channels").arg(format.channelCount()); + switch (format.channelCount()) { + case 1: + formatChannels = "mono"; + break; + case 2: + formatChannels = "stereo"; + break; + } + + result = QString("%1 Hz %2 bit %3 %4 %5") + .arg(format.sampleRate()) + .arg(format.sampleSize()) + .arg(formatType) + .arg(formatEndian) + .arg(formatChannels); + } else { + result = format.codec(); + } + } + + return result; +} + +bool isPCM(const QAudioFormat &format) +{ + return (format.codec() == "audio/pcm"); +} + +bool isPCMS16LE(const QAudioFormat &format) +{ + return isPCM(format) && + format.sampleType() == QAudioFormat::SignedInt && + format.sampleSize() == 16 && + format.byteOrder() == QAudioFormat::LittleEndian; +} + +const qint16 PCMS16MaxValue = 32767; +const quint16 PCMS16MaxAmplitude = 32768; // because minimum is -32768 + +qreal pcmToReal(qint16 pcm) +{ + return qreal(pcm) / PCMS16MaxAmplitude; +} + +qint16 realToPcm(qreal real) +{ + return real * PCMS16MaxValue; +} diff --git a/WaveUtils.h b/WaveUtils.h new file mode 100644 index 0000000..46a1f25 --- /dev/null +++ b/WaveUtils.h @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WAVEUTILS_H +#define WAVEUTILS_H + +#include +#include + +QT_FORWARD_DECLARE_CLASS(QAudioFormat) + +//----------------------------------------------------------------------------- +// Miscellaneous utility functions +//----------------------------------------------------------------------------- + +qint64 audioDuration(const QAudioFormat &format, qint64 bytes); +qint64 audioLength(const QAudioFormat &format, qint64 microSeconds); + +QString formatToString(const QAudioFormat &format); + +qreal nyquistFrequency(const QAudioFormat &format); + +// Scale PCM value to [-1.0, 1.0] +qreal pcmToReal(qint16 pcm); + +// Scale real value in [-1.0, 1.0] to PCM +qint16 realToPcm(qreal real); + +// Check whether the audio format is PCM +bool isPCM(const QAudioFormat &format); + +// Check whether the audio format is signed, little-endian, 16-bit PCM +bool isPCMS16LE(const QAudioFormat &format); + +// Compile-time calculation of powers of two + +template class PowerOfTwo +{ public: static const int Result = PowerOfTwo::Result * 2; }; + +template<> class PowerOfTwo<0> +{ public: static const int Result = 1; }; + +//----------------------------------------------------------------------------- +// Debug output +//----------------------------------------------------------------------------- + +class NullDebug +{ +public: + template + NullDebug& operator<<(const T&) { return *this; } +}; + +inline NullDebug nullDebug() { return NullDebug(); } + +#ifdef LOG_ENGINE +# define ENGINE_DEBUG qDebug() +#else +# define ENGINE_DEBUG nullDebug() +#endif + +#ifdef LOG_SPECTRUMANALYSER +# define SPECTRUMANALYSER_DEBUG qDebug() +#else +# define SPECTRUMANALYSER_DEBUG nullDebug() +#endif + +#ifdef LOG_WAVEFORM +# define WAVEFORM_DEBUG qDebug() +#else +# define WAVEFORM_DEBUG nullDebug() +#endif + +#endif // WAVEUTILS_H diff --git a/js8call.pro b/js8call.pro index 26f2320..403b65a 100644 --- a/js8call.pro +++ b/js8call.pro @@ -87,7 +87,9 @@ SOURCES += \ TransmitTextEdit.cpp \ NotificationAudio.cpp \ CallsignValidator.cpp \ - AudioDecoder.cpp + AudioDecoder.cpp \ + WaveFile.cpp \ + WaveUtils.cpp HEADERS += qt_helpers.hpp \ pimpl_h.hpp pimpl_impl.hpp \ @@ -125,7 +127,9 @@ HEADERS += qt_helpers.hpp \ logbook/n3fjp.h \ TransmitTextEdit.h \ NotificationAudio.h \ - AudioDecoder.h + AudioDecoder.h \ + WaveFile.h \ + WaveUtils.h INCLUDEPATH += qmake_only diff --git a/soundout.cpp b/soundout.cpp index a91026a..368dbd9 100644 --- a/soundout.cpp +++ b/soundout.cpp @@ -47,20 +47,24 @@ bool SoundOutput::audioError () const return result; } -void SoundOutput::setFormat (QAudioDeviceInfo const& device, unsigned channels, unsigned msBuffered) +void SoundOutput::setFormat (QAudioDeviceInfo const &device, unsigned channels, unsigned msBuffered){ + QAudioFormat format (device.preferredFormat ()); + format.setChannelCount (channels); + format.setCodec ("audio/pcm"); + format.setSampleRate (48000); + format.setSampleType (QAudioFormat::SignedInt); + format.setSampleSize (16); + format.setByteOrder (QAudioFormat::Endian (QSysInfo::ByteOrder)); + + setDeviceFormat(device, format, channels, msBuffered); +} + +void SoundOutput::setDeviceFormat (QAudioDeviceInfo const &device, QAudioFormat const &format, unsigned channels, unsigned msBuffered) { Q_ASSERT (0 < channels && channels < 3); m_msBuffered = msBuffered; - QAudioFormat format (device.preferredFormat ()); -// qDebug () << "Preferred audio output format:" << format; - format.setChannelCount (channels); - format.setCodec ("audio/pcm"); - format.setSampleRate (48000); - format.setSampleType (QAudioFormat::SignedInt); - format.setSampleSize (16); - format.setByteOrder (QAudioFormat::Endian (QSysInfo::ByteOrder)); if (!format.isValid ()) { Q_EMIT error (tr ("Requested output audio format is not valid.")); diff --git a/soundout.h b/soundout.h index 70a0759..e92640f 100644 --- a/soundout.h +++ b/soundout.h @@ -28,6 +28,7 @@ public: public Q_SLOTS: void setFormat (QAudioDeviceInfo const& device, unsigned channels, unsigned msBuffered = 0u); + void setDeviceFormat (QAudioDeviceInfo const& device, QAudioFormat const&format, unsigned channels, unsigned msBuffered = 0u); void restart (QIODevice *); void suspend (); void resume ();