diff --git a/plugins/channelrx/filesink/filesinkgui.cpp b/plugins/channelrx/filesink/filesinkgui.cpp index 08e114564..ee4d6f492 100644 --- a/plugins/channelrx/filesink/filesinkgui.cpp +++ b/plugins/channelrx/filesink/filesinkgui.cpp @@ -500,7 +500,7 @@ void FileSinkGUI::on_showFileDialog_clicked(bool checked) this, tr("Save record file"), m_settings.m_fileRecordName, - tr("SDR I/Q Files (*.sdriq)") + tr("SDR I/Q Files (*.sdriq *.wav)") ); fileDialog.setOptions(QFileDialog::DontUseNativeDialog); diff --git a/plugins/channelrx/filesink/filesinksink.cpp b/plugins/channelrx/filesink/filesinksink.cpp index a2ac74fe3..bc7faa087 100644 --- a/plugins/channelrx/filesink/filesinksink.cpp +++ b/plugins/channelrx/filesink/filesinksink.cpp @@ -22,6 +22,8 @@ #include "filesinkmessages.h" #include "filesinksink.h" +#include "dsp/filerecord.h" +#include "dsp/wavfilerecord.h" FileSinkSink::FileSinkSink() : m_nbCaptures(0), @@ -34,8 +36,11 @@ FileSinkSink::FileSinkSink() : m_squelchOpen(false), m_postSquelchCounter(0), m_msCount(0), - m_byteCount(0) -{} + m_byteCount(0), + m_bytesPerSample(sizeof(Sample)) +{ + m_fileSink = new FileRecord(); +} FileSinkSink::~FileSinkSink() {} @@ -46,16 +51,16 @@ void FileSinkSink::startRecording() { // set the length of pre record time qint64 mSShift = (m_preRecordFill * 1000) / m_sinkSampleRate; - m_fileSink.setMsShift(-mSShift); + m_fileSink->setMsShift(-mSShift); // notify capture start - if (!m_fileSink.startRecording()) + if (!m_fileSink->startRecording()) { // qWarning already output in startRecording, just need to send to GUI if (m_msgQueueToGUI) { FileSinkMessages::MsgReportRecordFileError *msg - = FileSinkMessages::MsgReportRecordFileError::create(QString("Failed to open %1").arg(m_fileSink.getCurrentFileName())); + = FileSinkMessages::MsgReportRecordFileError::create(QString("Failed to open %1").arg(m_fileSink->getCurrentFileName())); m_msgQueueToGUI->push(msg); } return; @@ -66,7 +71,7 @@ void FileSinkSink::startRecording() if (m_msgQueueToGUI) { FileSinkMessages::MsgReportRecordFileName *msg1 - = FileSinkMessages::MsgReportRecordFileName::create(m_fileSink.getCurrentFileName()); + = FileSinkMessages::MsgReportRecordFileName::create(m_fileSink->getCurrentFileName()); m_msgQueueToGUI->push(msg1); FileSinkMessages::MsgReportRecording *msg2 = FileSinkMessages::MsgReportRecording::create(true); m_msgQueueToGUI->push(msg2); @@ -77,13 +82,13 @@ void FileSinkSink::startRecording() m_preRecordBuffer.readBegin(m_preRecordFill, &p1Begin, &p1End, &p2Begin, &p2End); if (p1Begin != p1End) { - m_fileSink.feed(p1Begin, p1End, false); + m_fileSink->feed(p1Begin, p1End, false); } if (p2Begin != p2End) { - m_fileSink.feed(p2Begin, p2End, false); + m_fileSink->feed(p2Begin, p2End, false); } - m_byteCount += m_preRecordFill * sizeof(Sample); + m_byteCount += m_preRecordFill * m_bytesPerSample; if (m_sinkSampleRate > 0) { m_msCount += (m_preRecordFill * 1000) / m_sinkSampleRate; @@ -97,13 +102,13 @@ void FileSinkSink::stopRecording() { m_preRecordBuffer.reset(); - if (!m_fileSink.stopRecording()) + if (!m_fileSink->stopRecording()) { // qWarning already output stopRecording, just need to send to GUI if (m_msgQueueToGUI) { FileSinkMessages::MsgReportRecordFileError *msg - = FileSinkMessages::MsgReportRecordFileError::create(QString("Error while writing to %1").arg(m_fileSink.getCurrentFileName())); + = FileSinkMessages::MsgReportRecordFileError::create(QString("Error while writing to %1").arg(m_fileSink->getCurrentFileName())); m_msgQueueToGUI->push(msg); } } @@ -151,18 +156,18 @@ void FileSinkSink::feed(const SampleVector::const_iterator& begin, const SampleV if (m_squelchOpen) { - m_fileSink.feed(beginw, endw, true); + m_fileSink->feed(beginw, endw, true); } else { if (nbToWrite < m_postSquelchCounter) { - m_fileSink.feed(beginw, endw, true); + m_fileSink->feed(beginw, endw, true); m_postSquelchCounter -= nbToWrite; } else { - m_fileSink.feed(beginw, endw + m_postSquelchCounter, true); + m_fileSink->feed(beginw, endw + m_postSquelchCounter, true); nbToWrite = m_postSquelchCounter; m_postSquelchCounter = 0; @@ -170,7 +175,7 @@ void FileSinkSink::feed(const SampleVector::const_iterator& begin, const SampleV } } - m_byteCount += nbToWrite * sizeof(Sample); + m_byteCount += nbToWrite * m_bytesPerSample; if (m_sinkSampleRate > 0) { m_msCount += (nbToWrite * 1000) / m_sinkSampleRate; @@ -178,9 +183,9 @@ void FileSinkSink::feed(const SampleVector::const_iterator& begin, const SampleV } else if (m_record) { - m_fileSink.feed(beginw, endw, true); + m_fileSink->feed(beginw, endw, true); int nbSamples = endw - beginw; - m_byteCount += nbSamples * sizeof(Sample); + m_byteCount += nbSamples * m_bytesPerSample; if (m_sinkSampleRate > 0) { m_msCount += (nbSamples * 1000) / m_sinkSampleRate; @@ -240,7 +245,7 @@ void FileSinkSink::applyChannelSettings( { DSPSignalNotification *notif = new DSPSignalNotification(sinkSampleRate, centerFrequency); DSPSignalNotification *notifToSpectrum = new DSPSignalNotification(*notif); - m_fileSink.getInputMessageQueue()->push(notif); + m_fileSink->getInputMessageQueue()->push(notif); m_spectrumSink->getInputMessageQueue()->push(notifToSpectrum); if (m_msgQueueToGUI) @@ -277,7 +282,7 @@ void FileSinkSink::applySettings(const FileSinkSettings& settings, bool force) if (dotBreakout.size() > 1) { QString extension = dotBreakout.last(); - if (extension != "sdriq") { + if ((extension != "sdriq") && (extension != "wav")) { dotBreakout.last() = "sdriq"; } } @@ -291,11 +296,18 @@ void FileSinkSink::applySettings(const FileSinkSettings& settings, bool force) QString fileBase; FileRecordInterface::RecordType recordType = FileRecordInterface::guessTypeFromFileName(fileRecordName, fileBase); - if (recordType == FileRecordInterface::RecordTypeSdrIQ) + if ((recordType == FileRecordInterface::RecordTypeSdrIQ) || (recordType == FileRecordInterface::RecordTypeWav)) { - m_fileSink.setFileName(fileBase); + delete m_fileSink; + if (recordType == FileRecordInterface::RecordTypeSdrIQ) { + m_fileSink = new FileRecord(m_sinkSampleRate, m_centerFrequency); + } else { + m_fileSink = new WavFileRecord(m_sinkSampleRate, m_centerFrequency); + } + m_fileSink->setFileName(fileBase); m_msCount = 0; m_byteCount = 0; + m_bytesPerSample = m_fileSink->getBytesPerSample(); m_nbCaptures = 0; m_recordEnabled = true; } diff --git a/plugins/channelrx/filesink/filesinksink.h b/plugins/channelrx/filesink/filesinksink.h index caca68ff4..2097057ee 100644 --- a/plugins/channelrx/filesink/filesinksink.h +++ b/plugins/channelrx/filesink/filesinksink.h @@ -19,7 +19,7 @@ #define INCLUDE_FILESINKSINK_H_ #include "dsp/channelsamplesink.h" -#include "dsp/filerecord.h" +#include "dsp/filerecordinterface.h" #include "dsp/decimatorc.h" #include "dsp/samplesimplefifo.h" #include "dsp/ncof.h" @@ -36,7 +36,7 @@ public: virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); - FileRecord *getFileSink() { return &m_fileSink; } + FileRecordInterface *getFileSink() { return m_fileSink; } void setSpectrumSink(SpectrumVis* spectrumSink) { m_spectrumSink = spectrumSink; } void startRecording(); void stopRecording(); @@ -66,7 +66,7 @@ private: DecimatorC m_decimator; SampleVector m_sampleBuffer; FileSinkSettings m_settings; - FileRecord m_fileSink; + FileRecordInterface *m_fileSink; unsigned int m_nbCaptures; SampleSimpleFifo m_preRecordBuffer; unsigned int m_preRecordFill; @@ -81,6 +81,7 @@ private: int m_deviceUId; uint64_t m_msCount; uint64_t m_byteCount; + int m_bytesPerSample; }; #endif // INCLUDE_FILESINKSINK_H_ diff --git a/plugins/channelrx/filesink/readme.md b/plugins/channelrx/filesink/readme.md index a81c179c0..e1c4b4e25 100644 --- a/plugins/channelrx/filesink/readme.md +++ b/plugins/channelrx/filesink/readme.md @@ -2,11 +2,11 @@

Introduction

-Use this plugin to record its channel IQ data in [sdriq](../../samplesource/fileinput/readme.md#introduction) format. The baseband sample rate can be decimated by a factor of two and its center shifted to accomodate different requirements than recording the full baseband. More than one such plugin can be used in the same baseband to record different parts of the baseband spectrum. Of course in this case file output collision should be avoided. +Use this plugin to record its channel IQ data in [sdriq](../../samplesource/fileinput/readme.md#introduction) or signed 16-bit PCM `.wav` format. The baseband sample rate can be decimated by a factor of two and its center shifted to accomodate different requirements than recording the full baseband. More than one such plugin can be used in the same baseband to record different parts of the baseband spectrum. Of course in this case file output collision should be avoided. Such files can be read in SDRangel using the [File input plugin](../../samplesource/fileinput/readme.md). -Each recording is written in a new file with the starting timestamp before the `.sdriq` extension in `yyyy-MM-ddTHH_mm_ss_zzz` format. It keeps the first dot limted groups of the filename before the `.sdriq` extension if there are two such groups or before the two last groups if there are more than two groups. Examples: +Each recording is written in a new file with the starting timestamp before the `.sdriq` extension in `yyyy-MM-ddTHH_mm_ss_zzz` format. It keeps the first dot limted groups of the filename before the `.sdriq` or `.wav` extension if there are two such groups or before the two last groups if there are more than two groups. Examples: - Given file name: `test.sdriq` then a recording file will be like: `test.2020-08-05T21_39_07_974.sdriq` - Given file name: `test.2020-08-05T20_36_15_974.sdriq` then a recording file will be like (with timestamp updated): `test.2020-08-05T21_41_21_173.sdriq` @@ -14,7 +14,7 @@ Each recording is written in a new file with the starting timestamp before the ` - Given file name: `record.test.first.sdriq` then a recording file will be like: `reocrd.test.2020-08-05T21_39_52_974.sdriq` If a filename is given without `.sdriq` extension then the `.sdriq` extension is appended automatically before the above algorithm is applied. -If a filename is given with an extension different of `.sdriq` then the extension is replaced by `.sdriq` automatically before the above algorithm is applied. +If a filename is given with an extension different of `.sdriq` or `.wav` then the extension is replaced by `.sdriq` automatically before the above algorithm is applied.

Interface

diff --git a/plugins/samplesource/fileinput/fileinput.cpp b/plugins/samplesource/fileinput/fileinput.cpp index fc05306a0..84233661b 100644 --- a/plugins/samplesource/fileinput/fileinput.cpp +++ b/plugins/samplesource/fileinput/fileinput.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "SWGDeviceSettings.h" #include "SWGFileInputSettings.h" @@ -32,6 +33,7 @@ #include "dsp/dspdevicesourceengine.h" #include "dsp/dspengine.h" #include "dsp/filerecord.h" +#include "dsp/wavfilerecord.h" #include "device/deviceapi.h" #include "fileinput.h" @@ -99,7 +101,74 @@ void FileInput::openFileStream() #endif quint64 fileSize = m_ifstream.tellg(); - if (fileSize > sizeof(FileRecord::Header)) + if (m_settings.m_fileName.endsWith(".wav")) + { + WavFileRecord::Header header; + m_ifstream.seekg(0, std::ios_base::beg); + bool headerOK = WavFileRecord::readHeader(m_ifstream, header); + m_sampleRate = header.m_sampleRate; + if (header.m_auxiHeader.m_size > 0) + { + // Some WAV files written by SDR tools have auxi header + m_centerFrequency = header.m_auxi.m_centerFreq; + m_startingTimeStamp = QDateTime(QDate( + header.m_auxi.m_startTime.m_year, + header.m_auxi.m_startTime.m_month, + header.m_auxi.m_startTime.m_day + ), QTime( + header.m_auxi.m_startTime.m_hour, + header.m_auxi.m_startTime.m_minute, + header.m_auxi.m_startTime.m_second, + header.m_auxi.m_startTime.m_milliseconds + )).toMSecsSinceEpoch() / 1000; + } + else + { + // Attempt to extract time and frequency from filename + QRegExp dateTimeRE("([12][0-9][0-9][0-9]).?([01][0-9]).?([0-3][0-9]).?([0-2][0-9]).?([0-5][0-9]).?([0-5][0-9])"); + if (dateTimeRE.indexIn(m_settings.m_fileName) != -1) + { + m_startingTimeStamp = QDateTime(QDate( + dateTimeRE.capturedTexts()[1].toInt(), + dateTimeRE.capturedTexts()[2].toInt(), + dateTimeRE.capturedTexts()[3].toInt() + ), QTime( + dateTimeRE.capturedTexts()[4].toInt(), + dateTimeRE.capturedTexts()[5].toInt(), + dateTimeRE.capturedTexts()[6].toInt() + )).toMSecsSinceEpoch() / 1000; + } + // Attempt to extract centre frequency from filename + QRegExp freqkRE("(([0-9]+)kHz)"); + QRegExp freqRE("(([0-9]+)Hz)"); + if (freqkRE.indexIn(m_settings.m_fileName)) + { + m_centerFrequency = freqkRE.capturedTexts()[2].toLongLong() * 1000LL; + } + else if (freqRE.indexIn(m_settings.m_fileName)) + { + m_centerFrequency = freqRE.capturedTexts()[2].toLongLong(); + } + } + m_sampleSize = header.m_bitsPerSample; + + if (headerOK && (m_sampleRate > 0) && (m_sampleSize > 0)) + { + m_recordLengthMuSec = ((fileSize - m_ifstream.tellg()) * 1000000UL) / ((m_sampleSize == 24 ? 8 : 4) * m_sampleRate); + } + else + { + qCritical("FileInput::openFileStream: invalid .wav file"); + m_recordLengthMuSec = 0; + } + + if (getMessageQueueToGUI()) + { + MsgReportHeaderCRC *report = MsgReportHeaderCRC::create(headerOK); + getMessageQueueToGUI()->push(report); + } + } + else if (fileSize > sizeof(FileRecord::Header)) { FileRecord::Header header; m_ifstream.seekg(0,std::ios_base::beg); diff --git a/plugins/samplesource/fileinput/fileinputgui.cpp b/plugins/samplesource/fileinput/fileinputgui.cpp index 466de16c8..84bbc8968 100644 --- a/plugins/samplesource/fileinput/fileinputgui.cpp +++ b/plugins/samplesource/fileinput/fileinputgui.cpp @@ -302,7 +302,7 @@ void FileInputGUI::on_showFileDialog_clicked(bool checked) { (void) checked; QString fileName = QFileDialog::getOpenFileName(this, - tr("Open I/Q record file"), ".", tr("SDR I/Q Files (*.sdriq)"), 0, QFileDialog::DontUseNativeDialog); + tr("Open I/Q record file"), ".", tr("SDR I/Q Files (*.sdriq *.wav)"), 0, QFileDialog::DontUseNativeDialog); if (fileName != "") { diff --git a/plugins/samplesource/fileinput/readme.md b/plugins/samplesource/fileinput/readme.md index ac674f36b..447d7de8e 100644 --- a/plugins/samplesource/fileinput/readme.md +++ b/plugins/samplesource/fileinput/readme.md @@ -2,7 +2,9 @@

Introduction

-This plugin reads a file of I/Q samples that have been previously saved with the file record button of other sampling source devices. The file starts with a 32 byte header of all unsigned integer of various sizes containing meta data: +This plugin reads a file of I/Q samples that have been previously saved with the file record button of other sampling source devices. The plugin supports SDRangel's own .sdriq file format as well as signed 16-bit PCM, 2 channel .wav files (including support for optional auxi headers, containing centre frequency). + +An .sdriq file starts with a 32 byte header of all unsigned integer of various sizes containing meta data: @@ -66,7 +68,7 @@ This is the center frequency of reception in kHz when the record was taken and w

4: Open file

-Opens a file dialog to select the input file. It expects a default extension of `.sdriq`. This button is disabled when the stream is running. You need to pause (button 11) to make it active and thus be able to select another file. +Opens a file dialog to select the input file. It expects an extension of `.sdriq` or `.wav`. This button is disabled when the stream is running. You need to pause (button 11) to make it active and thus be able to select another file.

5: File path

diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 20b88a5e7..bc0dcf2be 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -149,6 +149,7 @@ set(sdrbase_SOURCES dsp/devicesamplemimo.cpp dsp/devicesamplestatic.cpp dsp/spectrumvis.cpp + dsp/wavfilerecord.cpp device/deviceapi.cpp device/deviceenumerator.cpp @@ -341,6 +342,7 @@ set(sdrbase_HEADERS dsp/devicesamplemimo.h dsp/devicesamplestatic.h dsp/spectrumvis.h + dsp/wavfilerecord.h device/deviceapi.h device/deviceenumerator.h diff --git a/sdrbase/dsp/filerecord.cpp b/sdrbase/dsp/filerecord.cpp index 3e149ea8d..899eed818 100644 --- a/sdrbase/dsp/filerecord.cpp +++ b/sdrbase/dsp/filerecord.cpp @@ -27,11 +27,11 @@ #include "filerecord.h" -FileRecord::FileRecord() : +FileRecord::FileRecord(quint32 sampleRate, quint64 centerFrequency) : FileRecordInterface(), m_fileBase("test"), - m_sampleRate(0), - m_centerFrequency(0), + m_sampleRate(sampleRate), + m_centerFrequency(centerFrequency), m_recordOn(false), m_recordStart(false), m_byteCount(0), diff --git a/sdrbase/dsp/filerecord.h b/sdrbase/dsp/filerecord.h index 91571c7cb..145cd9a45 100644 --- a/sdrbase/dsp/filerecord.h +++ b/sdrbase/dsp/filerecord.h @@ -44,12 +44,12 @@ public: }; #pragma pack(pop) - FileRecord(); + FileRecord(quint32 sampleRate=0, quint64 centerFrequency=0); FileRecord(const QString& fileBase); virtual ~FileRecord(); quint64 getByteCount() const { return m_byteCount; } - void setMsShift(int shift) { m_msShift = shift; } + void setMsShift(qint64 shift) { m_msShift = shift; } const QString& getCurrentFileName() { return m_curentFileName; } void genUniqueFileName(uint deviceUID, int istream = -1); @@ -76,7 +76,7 @@ private: std::ofstream m_sampleFile; QString m_curentFileName; quint64 m_byteCount; - int m_msShift; + qint64 m_msShift; QMutex m_mutex; void writeHeader(); diff --git a/sdrbase/dsp/filerecordinterface.cpp b/sdrbase/dsp/filerecordinterface.cpp index ed46dc25c..64c8221d3 100644 --- a/sdrbase/dsp/filerecordinterface.cpp +++ b/sdrbase/dsp/filerecordinterface.cpp @@ -61,6 +61,11 @@ FileRecordInterface::RecordType FileRecordInterface::guessTypeFromFileName(const fileBase = dotBreakout.join(QLatin1Char('.')); return RecordTypeSigMF; } + else if (extension == "wav") + { + fileBase = dotBreakout.join(QLatin1Char('.')); + return RecordTypeWav; + } else { fileBase = fileName; diff --git a/sdrbase/dsp/filerecordinterface.h b/sdrbase/dsp/filerecordinterface.h index b66e8694f..5894d048e 100644 --- a/sdrbase/dsp/filerecordinterface.h +++ b/sdrbase/dsp/filerecordinterface.h @@ -35,7 +35,8 @@ public: { RecordTypeUndefined = 0, RecordTypeSdrIQ, - RecordTypeSigMF + RecordTypeSigMF, + RecordTypeWav }; FileRecordInterface(); @@ -51,10 +52,14 @@ public: MessageQueue *getMessageQueueToGUI() { return m_guiMessageQueue; } virtual void setFileName(const QString &filename) = 0; + virtual const QString& getCurrentFileName() = 0; virtual bool startRecording() = 0; virtual bool stopRecording() = 0; virtual bool isRecording() const = 0; + virtual void setMsShift(qint64 msShift) = 0; + virtual int getBytesPerSample() { return sizeof(Sample); }; + static QString genUniqueFileName(unsigned int deviceUID, int istream = -1); static RecordType guessTypeFromFileName(const QString& fileName, QString& fileBase); diff --git a/sdrbase/dsp/sigmffilerecord.h b/sdrbase/dsp/sigmffilerecord.h index 0648c5e50..fcdd260c3 100644 --- a/sdrbase/dsp/sigmffilerecord.h +++ b/sdrbase/dsp/sigmffilerecord.h @@ -39,18 +39,19 @@ public: SigMFFileRecord(const QString& filename, const QString& hardwareId); virtual ~SigMFFileRecord(); - virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positiveOnly); - virtual void start(); - virtual void stop(); - virtual bool handleMessage(const Message& message); + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positiveOnly) override; + virtual void start() override; + virtual void stop() override; + virtual bool handleMessage(const Message& message) override; - virtual void setFileName(const QString& filename); - virtual bool startRecording(); - virtual bool stopRecording(); - virtual bool isRecording() const { return m_recordOn; } + virtual void setFileName(const QString& filename) override; + virtual const QString& getCurrentFileName() override { return m_fileName; } + virtual bool startRecording() override; + virtual bool stopRecording() override; + virtual bool isRecording() const override { return m_recordOn; } void setHardwareId(const QString& hardwareId) { m_hardwareId = hardwareId; } - void setMsShift(qint64 msShift) { m_msShift = msShift; } + void setMsShift(qint64 msShift) override { m_msShift = msShift; } unsigned int getNbCaptures() const; uint64_t getInitialMsCount() const { return m_initialMsCount; } uint64_t getInitialBytesCount() const { return m_initialBytesCount; } diff --git a/sdrbase/dsp/wavfilerecord.cpp b/sdrbase/dsp/wavfilerecord.cpp new file mode 100644 index 000000000..89bb7e114 --- /dev/null +++ b/sdrbase/dsp/wavfilerecord.cpp @@ -0,0 +1,342 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include +#include + +#include "dsp/dspcommands.h" +#include "util/simpleserializer.h" +#include "util/message.h" + +#include "wavfilerecord.h" + +WavFileRecord::WavFileRecord(quint32 sampleRate=0, quint64 centerFrequency=0) : + FileRecordInterface(), + m_fileBase("test"), + m_sampleRate(sampleRate), + m_centerFrequency(centerFrequency), + m_recordOn(false), + m_recordStart(false), + m_byteCount(0), + m_msShift(0) +{ + setObjectName("WavFileRecord"); +} + +WavFileRecord::WavFileRecord(const QString& fileBase) : + FileRecordInterface(), + m_fileBase(fileBase), + m_sampleRate(0), + m_centerFrequency(0), + m_recordOn(false), + m_recordStart(false), + m_byteCount(0) +{ + setObjectName("WavFileRecord"); +} + +WavFileRecord::~WavFileRecord() +{ + stopRecording(); +} + +void WavFileRecord::setFileName(const QString& fileBase) +{ + if (!m_recordOn) + { + m_fileBase = fileBase; + } +} + +void WavFileRecord::genUniqueFileName(uint deviceUID, int istream) +{ + if (istream < 0) { + setFileName(QString("rec%1_%2.wav").arg(deviceUID).arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH_mm_ss_zzz"))); + } else { + setFileName(QString("rec%1_%2_%3.wav").arg(deviceUID).arg(istream).arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH_mm_ss_zzz"))); + } +} + +void WavFileRecord::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positiveOnly) +{ + (void) positiveOnly; + + if(!m_recordOn) + return; + + if (begin < end) // if there is something to put out + { + if (m_recordStart) + { + writeHeader(); + m_recordStart = false; + } + + if (SDR_RX_SAMP_SZ == 16) + { + m_sampleFile.write(reinterpret_cast(&*(begin)), (end - begin)*sizeof(Sample)); + m_byteCount += end - begin; + } + else + { + for (SampleVector::const_iterator it = begin; it != end; ++it) + { + // Convert from 24-bit to 16-bit + int16_t samples[2]; + samples[0] = it->real() >> 8; + samples[1] = it->imag() >> 8; + m_sampleFile.write(reinterpret_cast(&samples), 4); + m_byteCount += 4; + } + } + } +} + +void WavFileRecord::start() +{ +} + +void WavFileRecord::stop() +{ + stopRecording(); +} + +bool WavFileRecord::startRecording() +{ + if (m_recordOn) { + stopRecording(); + } + + if (!m_sampleFile.is_open()) + { + qDebug() << "WavFileRecord::startRecording"; + m_curentFileName = QString("%1.%2.wav").arg(m_fileBase).arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH_mm_ss_zzz")); + m_sampleFile.open(m_curentFileName.toStdString().c_str(), std::ios::binary); + if (!m_sampleFile.is_open()) + { + qWarning() << "WavFileRecord::startRecording: failed to open file: " << m_curentFileName; + return false; + } + m_recordOn = true; + m_recordStart = true; + m_byteCount = 0; + } + return true; +} + +bool WavFileRecord::stopRecording() +{ + if (m_sampleFile.is_open()) + { + qDebug() << "WavFileRecord::stopRecording"; + // Fix up chunk sizes + long fileSize = m_sampleFile.tellp(); + m_sampleFile.seekp(offsetof(Header, m_riffHeader.m_size)); + qint32 size = (fileSize - 8); + m_sampleFile.write((char *)&size, 4); + m_sampleFile.seekp(offsetof(Header, m_dataHeader.m_size)); + size = fileSize - sizeof(Header); + m_sampleFile.write((char *)&size, 4); + m_sampleFile.close(); + m_recordOn = false; + m_recordStart = false; + if (m_sampleFile.bad()) + { + qWarning() << "WavFileRecord::stopRecording: an error occurred while writing to " << m_curentFileName; + return false; + } + } + return true; +} + +bool WavFileRecord::handleMessage(const Message& message) +{ + if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + + int sampleRate = notif.getSampleRate(); + if ((sampleRate != (int)m_sampleRate) && m_recordOn) { + qDebug() << "WavFileRecord::handleMessage: sample rate has changed. Creating a new .wav file"; + stopRecording(); + m_recordOn = true; + } + + m_sampleRate = sampleRate; + m_centerFrequency = notif.getCenterFrequency(); + qDebug() << "WavFileRecord::handleMessage: DSPSignalNotification: m_inputSampleRate: " << m_sampleRate + << " m_centerFrequency: " << m_centerFrequency; + + if (m_recordOn) { + startRecording(); + } + + return true; + } + else + { + return false; + } +} + +void WavFileRecord::writeHeader() +{ + Header header; + header.m_riffHeader.m_id[0] = 'R'; + header.m_riffHeader.m_id[1] = 'I'; + header.m_riffHeader.m_id[2] = 'F'; + header.m_riffHeader.m_id[3] = 'F'; + header.m_riffHeader.m_size = 0; // Needs to be fixed on close + header.m_type[0] = 'W'; + header.m_type[1] = 'A'; + header.m_type[2] = 'V'; + header.m_type[3] = 'E'; + header.m_fmtHeader.m_id[0] = 'f'; + header.m_fmtHeader.m_id[1] = 'm'; + header.m_fmtHeader.m_id[2] = 't'; + header.m_fmtHeader.m_id[3] = ' '; + header.m_fmtHeader.m_size = 16; + header.m_audioFormat = 1; // Linear PCM + header.m_numChannels = 2; // I/Q + header.m_sampleRate = m_sampleRate; + // We always use 16-bits regardless of SDR_RX_SAMP_SZ + header.m_byteRate = m_sampleRate * 2 * 16 / 8; + header.m_blockAlign = 2 * 16 / 8; + header.m_bitsPerSample = 16; + + header.m_auxiHeader.m_id[0] = 'a'; + header.m_auxiHeader.m_id[1] = 'u'; + header.m_auxiHeader.m_id[2] = 'x'; + header.m_auxiHeader.m_id[3] = 'i'; + header.m_auxiHeader.m_size = sizeof(Auxi); + QDateTime now = QDateTime::currentDateTime(); + header.m_auxi.m_startTime.m_year = now.date().year(); + header.m_auxi.m_startTime.m_month = now.date().month(); + header.m_auxi.m_startTime.m_dayOfWeek = now.date().dayOfWeek(); + header.m_auxi.m_startTime.m_day = now.date().day(); + header.m_auxi.m_startTime.m_hour = now.time().hour(); + header.m_auxi.m_startTime.m_minute = now.time().minute(); + header.m_auxi.m_startTime.m_second = now.time().second(); + header.m_auxi.m_startTime.m_milliseconds = now.time().msec(); + header.m_auxi.m_stopTime.m_year = 0; // Needs to be fixed on close + header.m_auxi.m_stopTime.m_month = 0; + header.m_auxi.m_stopTime.m_dayOfWeek = 0; + header.m_auxi.m_stopTime.m_day = 0; + header.m_auxi.m_stopTime.m_hour = 0; + header.m_auxi.m_stopTime.m_minute = 0; + header.m_auxi.m_stopTime.m_second = 0; + header.m_auxi.m_stopTime.m_milliseconds = 0; + header.m_auxi.m_centerFreq = m_centerFrequency; + header.m_auxi.m_adFrequency = m_sampleRate; + header.m_auxi.m_ifFrequency = 0; + header.m_auxi.m_bandwidth = 0; + header.m_auxi.m_iqOffset = 0; + header.m_auxi.m_unused2 = 0; + header.m_auxi.m_unused3 = 0; + header.m_auxi.m_unused4 = 0; + header.m_auxi.m_unused5 = 0; + memset(&header.m_auxi.m_nextFilename[0], 0, 96); + + header.m_dataHeader.m_size = sizeof(Auxi); + header.m_dataHeader.m_id[0] = 'd'; + header.m_dataHeader.m_id[1] = 'a'; + header.m_dataHeader.m_id[2] = 't'; + header.m_dataHeader.m_id[3] = 'a'; + header.m_dataHeader.m_size = 0; // Needs to be fixed on close + + writeHeader(m_sampleFile, header); +} + +bool WavFileRecord::readHeader(std::ifstream& sampleFile, Header& header) +{ + memset(&header, 0, sizeof(Header)); + + sampleFile.read((char *) &header, 8+4+8+16); + if (!sampleFile) + { + qDebug() << "WavFileRecord::readHeader: End of file without reading header"; + return false; + } + + if (strncmp(header.m_riffHeader.m_id, "RIFF", 4)) + { + qDebug() << "WavFileRecord::readHeader: No RIFF header"; + return false; + } + if (strncmp(header.m_type, "WAVE", 4)) + { + qDebug() << "WavFileRecord::readHeader: No WAVE header"; + return false; + } + if (strncmp(header.m_fmtHeader.m_id, "fmt ", 4)) + { + qDebug() << "WavFileRecord::readHeader: No fmt header"; + return false; + } + if (header.m_audioFormat != 1) + { + qDebug() << "WavFileRecord::readHeader: Audio format is not PCM"; + return false; + } + if (header.m_numChannels != 2) + { + qDebug() << "WavFileRecord::readHeader: Number of channels is not 2"; + return false; + } + // FileInputWorker can't handle other bits sizes + if (header.m_bitsPerSample != 16) + { + qDebug() << "WavFileRecord::readHeader: Number of bits per sample is not 16"; + return false; + } + + Chunk chunkHeader; + bool gotData = false; + while (!gotData) + { + sampleFile.read((char *) &chunkHeader, 8); + if (!sampleFile) + { + qDebug() << "WavFileRecord::readHeader: End of file without reading data header"; + return false; + } + + if (!strncmp(chunkHeader.m_id, "auxi", 4)) + { + memcpy(&header.m_auxiHeader, &chunkHeader, sizeof(Chunk)); + sampleFile.read((char *) &header.m_auxi, sizeof(Auxi)); + if (!sampleFile) + return false; + } + else if (!strncmp(chunkHeader.m_id, "data", 4)) + { + memcpy(&header.m_dataHeader, &chunkHeader, sizeof(Chunk)); + gotData = true; + } + } + + return true; +} + +void WavFileRecord::writeHeader(std::ofstream& sampleFile, Header& header) +{ + sampleFile.write((const char *) &header, sizeof(Header)); +} diff --git a/sdrbase/dsp/wavfilerecord.h b/sdrbase/dsp/wavfilerecord.h new file mode 100644 index 000000000..ccb5615ab --- /dev/null +++ b/sdrbase/dsp/wavfilerecord.h @@ -0,0 +1,124 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// File recorder in .wav format // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_WAV_FILERECORD_H +#define INCLUDE_WAV_FILERECORD_H + +#include +#include +#include +#include + +#include + +#include "dsp/filerecordinterface.h" +#include "export.h" + +class Message; + +class SDRBASE_API WavFileRecord : public FileRecordInterface { +public: + +#pragma pack(push, 1) + struct Chunk + { + char m_id[4]; // "RIFF", "fmt ", "auxi", "data" + quint32 m_size; + }; + struct SystemTime { + quint16 m_year; + quint16 m_month; + quint16 m_dayOfWeek; + quint16 m_day; + quint16 m_hour; + quint16 m_minute; + quint16 m_second; + quint16 m_milliseconds; + }; + struct Auxi { + SystemTime m_startTime; + SystemTime m_stopTime; + quint32 m_centerFreq; + quint32 m_adFrequency; + quint32 m_ifFrequency; + quint32 m_bandwidth; + quint32 m_iqOffset; + quint32 m_unused2; + quint32 m_unused3; + quint32 m_unused4; + quint32 m_unused5; + char m_nextFilename[96]; + }; + struct Header + { + Chunk m_riffHeader; + char m_type[4]; // "WAVE" + Chunk m_fmtHeader; + quint16 m_audioFormat; + quint16 m_numChannels; + quint32 m_sampleRate; + quint32 m_byteRate; + quint16 m_blockAlign; + quint16 m_bitsPerSample; + Chunk m_auxiHeader; + Auxi m_auxi; + Chunk m_dataHeader; + }; +#pragma pack(pop) + + WavFileRecord(quint32 sampleRate, quint64 centerFrequency); + WavFileRecord(const QString& fileBase); + virtual ~WavFileRecord(); + + quint64 getByteCount() const { return m_byteCount; } + void setMsShift(qint64 shift) override { m_msShift = shift; } + virtual int getBytesPerSample() override { return 4; }; + const QString& getCurrentFileName() override { return m_curentFileName; } + + void genUniqueFileName(uint deviceUID, int istream = -1); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positiveOnly) override; + virtual void start() override; + virtual void stop() override; + virtual bool handleMessage(const Message& message) override; + + virtual void setFileName(const QString& fileBase) override; + virtual bool startRecording() override; + virtual bool stopRecording() override; + virtual bool isRecording() const override { return m_recordOn; } + + static bool readHeader(std::ifstream& samplefile, Header& header); + static void writeHeader(std::ofstream& samplefile, Header& header); + +private: + QString m_fileBase; + quint32 m_sampleRate; + quint64 m_centerFrequency; + bool m_recordOn; + bool m_recordStart; + std::ofstream m_sampleFile; + QString m_curentFileName; + quint64 m_byteCount; + qint64 m_msShift; + + void writeHeader(); +}; + +#endif // INCLUDE_WAV_FILERECORD_H