1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-23 10:05:46 -05:00

Add signed 16-bit PCM 2 channel .wav file support to File Input and File Record plugins

This commit is contained in:
Jon Beniston 2021-05-21 10:06:10 +01:00
parent cd3434c871
commit 80fe6cb096
15 changed files with 611 additions and 48 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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_

View File

@ -2,11 +2,11 @@
<h2>Introduction</h2>
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.
<h2>Interface</h2>

View File

@ -21,6 +21,7 @@
#include <QDebug>
#include <QNetworkReply>
#include <QBuffer>
#include <QRegExp>
#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);

View File

@ -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 != "")
{

View File

@ -2,7 +2,9 @@
<h2>Introduction</h2>
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:
<table>
<tr>
@ -66,7 +68,7 @@ This is the center frequency of reception in kHz when the record was taken and w
<h3>4: Open file</h3>
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.
<h3>5: File path</h3>

View File

@ -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

View File

@ -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),

View File

@ -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();

View File

@ -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;

View File

@ -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);

View File

@ -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; }

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <cstddef>
#include <cstring>
#include <QDebug>
#include <QDateTime>
#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<const char*>(&*(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<const char*>(&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 != 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));
}

124
sdrbase/dsp/wavfilerecord.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_WAV_FILERECORD_H
#define INCLUDE_WAV_FILERECORD_H
#include <string>
#include <iostream>
#include <fstream>
#include <ctime>
#include <QDateTime>
#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