1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-25 09:18:54 -05:00

DemodAnalyzer: implemented record audio. Part of #1330

This commit is contained in:
f4exb 2022-11-13 19:53:22 +01:00
parent 93a238503c
commit 2c02a9bcf1
15 changed files with 391 additions and 16 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

View File

@ -220,6 +220,10 @@ bool DemodAnalyzer::handleMessage(const Message& cmd)
m_sampleRate = report.getSampleRate();
m_scopeVis.setLiveRate(m_sampleRate);
if (m_running) {
m_worker->applySampleRate(m_sampleRate);
}
DSPSignalNotification *msg = new DSPSignalNotification(0, m_sampleRate);
m_spectrumVis.getInputMessageQueue()->push(msg);

View File

@ -16,6 +16,7 @@
///////////////////////////////////////////////////////////////////////////////////
#include <QMessageBox>
#include <QFileDialog>
#include "feature/featureuiset.h"
#include "dsp/spectrumvis.h"
@ -206,6 +207,11 @@ void DemodAnalyzerGUI::displaySettings()
setTitle(m_settings.m_title);
blockApplySettings(true);
ui->log2Decim->setCurrentIndex(m_settings.m_log2Decim);
ui->record->setChecked(m_settings.m_recordToFile);
ui->fileNameText->setText(m_settings.m_fileRecordName);
ui->showFileDialog->setEnabled(!m_settings.m_recordToFile);
ui->recordSilenceTime->setValue(m_settings.m_recordSilenceTime);
ui->recordSilenceText->setText(tr("%1").arg(m_settings.m_recordSilenceTime / 10.0, 0, 'f', 1));
getRollupContents()->restoreState(m_rollupState);
blockApplySettings(false);
}
@ -327,6 +333,47 @@ void DemodAnalyzerGUI::on_log2Decim_currentIndexChanged(int index)
applySettings();
}
void DemodAnalyzerGUI::on_record_toggled(bool checked)
{
ui->showFileDialog->setEnabled(!checked);
m_settings.m_recordToFile = checked;
applySettings();
}
void DemodAnalyzerGUI::on_showFileDialog_clicked(bool checked)
{
(void) checked;
QFileDialog fileDialog(
this,
tr("Save record file"),
m_settings.m_fileRecordName,
tr("WAV Files (*.wav)")
);
fileDialog.setOptions(QFileDialog::DontUseNativeDialog);
fileDialog.setFileMode(QFileDialog::AnyFile);
QStringList fileNames;
if (fileDialog.exec())
{
fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
m_settings.m_fileRecordName = fileNames.at(0);
ui->fileNameText->setText(m_settings.m_fileRecordName);
applySettings();
}
}
}
void DemodAnalyzerGUI::on_recordSilenceTime_valueChanged(int value)
{
m_settings.m_recordSilenceTime = value;
ui->recordSilenceText->setText(tr("%1").arg(value / 10.0, 0, 'f', 1));
applySettings();
}
void DemodAnalyzerGUI::tick()
{
m_channelPowerAvg(m_demodAnalyzer->getMagSqAvg());
@ -379,4 +426,7 @@ void DemodAnalyzerGUI::makeUIConnections()
QObject::connect(ui->channels, qOverload<int>(&QComboBox::currentIndexChanged), this, &DemodAnalyzerGUI::on_channels_currentIndexChanged);
QObject::connect(ui->channelApply, &QPushButton::clicked, this, &DemodAnalyzerGUI::on_channelApply_clicked);
QObject::connect(ui->log2Decim, qOverload<int>(&QComboBox::currentIndexChanged), this, &DemodAnalyzerGUI::on_log2Decim_currentIndexChanged);
QObject::connect(ui->record, &ButtonSwitch::toggled, this, &DemodAnalyzerGUI::on_record_toggled);
QObject::connect(ui->showFileDialog, &QPushButton::clicked, this, &DemodAnalyzerGUI::on_showFileDialog_clicked);
QObject::connect(ui->recordSilenceTime, &QSlider::valueChanged, this, &DemodAnalyzerGUI::on_recordSilenceTime_valueChanged);
}

View File

@ -93,6 +93,9 @@ private slots:
void on_channels_currentIndexChanged(int index);
void on_channelApply_clicked();
void on_log2Decim_currentIndexChanged(int index);
void on_record_toggled(bool checked);
void on_showFileDialog_clicked(bool checked);
void on_recordSilenceTime_valueChanged(int value);
void updateStatus();
void tick();
};

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>739</width>
<height>778</height>
<height>785</height>
</rect>
</property>
<property name="sizePolicy">
@ -37,7 +37,7 @@
<x>0</x>
<y>10</y>
<width>718</width>
<height>41</height>
<height>70</height>
</rect>
</property>
<property name="minimumSize">
@ -72,6 +72,12 @@
</property>
<item>
<widget class="ButtonSwitch" name="startStop">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="toolTip">
<string>start/stop acquisition</string>
</property>
@ -195,12 +201,6 @@
</item>
<item>
<widget class="QLabel" name="sinkSampleRateText">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Analyzer (sink) sample rate</string>
</property>
@ -249,6 +249,119 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="fileNameLayout">
<item>
<widget class="ButtonSwitch" name="record">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Start/stop recording</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/record_off.png</normaloff>:/record_off.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="showFileDialog">
<property name="minimumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Open file</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/preset-load.png</normaloff>:/preset-load.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="fileNameText">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Current recording file</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="recordSilenceTime">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Silence time (s) before recording is stopoed. 0 for continuous recording.</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="recordSilenceText">
<property name="maximumSize">
<size>
<width>30</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Silence time (s) before recording is stopoed</string>
</property>
<property name="text">
<string>10.0</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="spectrumContainer" native="true">

View File

@ -83,6 +83,9 @@ void DemodAnalyzerSettings::resetToDefaults()
m_reverseAPIFeatureSetIndex = 0;
m_reverseAPIFeatureIndex = 0;
m_workspaceIndex = 0;
m_fileRecordName.clear();
m_recordToFile = false;
m_recordSilenceTime = 0;
}
QByteArray DemodAnalyzerSettings::serialize() const
@ -112,6 +115,9 @@ QByteArray DemodAnalyzerSettings::serialize() const
s.writeS32(13, m_workspaceIndex);
s.writeBlob(14, m_geometryBytes);
s.writeString(15, m_fileRecordName);
s.writeBool(16, m_recordToFile);
s.writeS32(17, m_recordSilenceTime);
return s.final();
}
@ -170,6 +176,9 @@ bool DemodAnalyzerSettings::deserialize(const QByteArray& data)
d.readS32(13, &m_workspaceIndex, 0);
d.readBlob(14, &m_geometryBytes);
d.readString(15, &m_fileRecordName);
d.readBool(16, &m_recordToFile, false);
d.readS32(17, &m_recordSilenceTime, 0);
return true;
}

View File

@ -49,6 +49,9 @@ struct DemodAnalyzerSettings
uint16_t m_reverseAPIPort;
uint16_t m_reverseAPIFeatureSetIndex;
uint16_t m_reverseAPIFeatureIndex;
QString m_fileRecordName;
bool m_recordToFile;
int m_recordSilenceTime; //!< 100's ms
Serializable *m_spectrumGUI;
Serializable *m_scopeGUI;
Serializable *m_rollupState;

View File

@ -19,6 +19,7 @@
#include "dsp/scopevis.h"
#include "dsp/datafifo.h"
#include "dsp/wavfilerecord.h"
#include "demodanalyzerworker.h"
@ -28,7 +29,11 @@ MESSAGE_CLASS_DEFINITION(DemodAnalyzerWorker::MsgConnectFifo, Message)
DemodAnalyzerWorker::DemodAnalyzerWorker() :
m_dataFifo(nullptr),
m_msgQueueToFeature(nullptr),
m_sampleBufferSize(0)
m_sampleBufferSize(0),
m_wavFileRecord(nullptr),
m_recordSilenceNbSamples(0),
m_recordSilenceCount(0),
m_nbBytes(0)
{
qDebug("DemodAnalyzerWorker::DemodAnalyzerWorker");
}
@ -47,12 +52,15 @@ void DemodAnalyzerWorker::reset()
void DemodAnalyzerWorker::startWork()
{
QMutexLocker mutexLocker(&m_mutex);
m_wavFileRecord = new WavFileRecord(m_sinkSampleRate);
connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
}
void DemodAnalyzerWorker::stopWork()
{
QMutexLocker mutexLocker(&m_mutex);
delete m_wavFileRecord;
m_wavFileRecord = nullptr;
disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
}
@ -74,6 +82,13 @@ void DemodAnalyzerWorker::feedPart(
nbBytes = 2;
}
if ((nbBytes != m_nbBytes) && m_wavFileRecord)
{
m_wavFileRecord->stopRecording();
m_wavFileRecord->setMono(nbBytes == 2);
}
m_nbBytes = nbBytes;
int countSamples = (end - begin) / nbBytes;
if (countSamples > m_sampleBufferSize)
@ -93,6 +108,62 @@ void DemodAnalyzerWorker::feedPart(
vbegin.push_back(m_sampleBuffer.begin());
m_scopeVis->feed(vbegin, countSamples/(1<<m_settings.m_log2Decim));
}
if (m_settings.m_recordToFile && m_wavFileRecord)
{
for (int is = 0; is < countSamples/(1<<m_settings.m_log2Decim); is++)
{
const Sample& sample = m_sampleBuffer[is];
if ((sample.m_real == 0) && (sample.m_imag == 0))
{
if (m_recordSilenceNbSamples <= 0)
{
writeSampleToFile(sample);
m_recordSilenceCount = 0;
}
else if (m_recordSilenceCount < m_recordSilenceNbSamples)
{
writeSampleToFile(sample);
m_recordSilenceCount++;
}
else
{
if (m_wavFileRecord->isRecording()) {
m_wavFileRecord->stopRecording();
}
}
}
else
{
if (!m_wavFileRecord->isRecording()) {
m_wavFileRecord->startRecording();
}
writeSampleToFile(sample);
m_recordSilenceCount = 0;
}
}
}
}
void DemodAnalyzerWorker::writeSampleToFile(const Sample& sample)
{
if (SDR_RX_SAMP_SZ == 16)
{
if (m_nbBytes == 2) {
m_wavFileRecord->writeMono(sample.m_real);
} else {
m_wavFileRecord->write(sample.m_real, sample.m_imag);
}
}
else
{
if (m_nbBytes == 2) {
m_wavFileRecord->writeMono(sample.m_real >> 8);
} else {
m_wavFileRecord->write(sample.m_real >> 8, sample.m_imag >> 8);
}
}
}
void DemodAnalyzerWorker::decimate(int countSamples)
@ -190,11 +261,95 @@ void DemodAnalyzerWorker::applySettings(const DemodAnalyzerSettings& settings, b
<< " m_title: " << settings.m_title
<< " m_rgbColor: " << settings.m_rgbColor
<< " m_log2Decim: " << settings.m_log2Decim
<< " m_fileRecordName: " << settings.m_fileRecordName
<< " m_recordToFile: " << settings.m_recordToFile
<< " m_recordSilenceTime: " << settings.m_recordSilenceTime
<< " force: " << force;
if ((m_settings.m_fileRecordName != settings.m_fileRecordName) || force)
{
if (m_wavFileRecord)
{
QStringList dotBreakout = settings.m_fileRecordName.split(QLatin1Char('.'));
if (dotBreakout.size() > 1)
{
QString extension = dotBreakout.last();
if (extension != "wav") {
dotBreakout.last() = "wav";
}
}
else
{
dotBreakout.append("wav");
}
QString newFileRecordName = dotBreakout.join(QLatin1Char('.'));
QString fileBase;
FileRecordInterface::guessTypeFromFileName(newFileRecordName, fileBase);
qDebug("DemodAnalyzerWorker::applySettings: newFileRecordName: %s fileBase: %s", qPrintable(newFileRecordName), qPrintable(fileBase));
m_wavFileRecord->setFileName(fileBase);
}
}
if ((m_settings.m_recordToFile != settings.m_recordToFile) || force)
{
if (m_wavFileRecord)
{
if (settings.m_recordToFile)
{
if (!m_wavFileRecord->isRecording()) {
m_wavFileRecord->startRecording();
}
}
else
{
if (m_wavFileRecord->isRecording()) {
m_wavFileRecord->stopRecording();
}
}
m_recordSilenceCount = 0;
}
}
if ((m_settings.m_recordSilenceTime != settings.m_recordSilenceTime)
|| (m_settings.m_log2Decim != settings.m_log2Decim) || force)
{
m_recordSilenceNbSamples = (settings.m_recordSilenceTime * (m_sinkSampleRate / (1<<settings.m_log2Decim))) / 10; // time in 100'ś ms
m_recordSilenceCount = 0;
if (m_wavFileRecord)
{
if (m_wavFileRecord->isRecording()) {
m_wavFileRecord->stopRecording();
}
m_wavFileRecord->setSampleRate(m_sinkSampleRate / (1<<settings.m_log2Decim));
}
}
m_settings = settings;
}
void DemodAnalyzerWorker::applySampleRate(int sampleRate)
{
m_sinkSampleRate = sampleRate;
m_recordSilenceNbSamples = (m_settings.m_recordSilenceTime * (m_sinkSampleRate / (1<<m_settings.m_log2Decim))) / 10; // time in 100'ś ms
m_recordSilenceCount = 0;
if (m_wavFileRecord)
{
if (m_wavFileRecord->isRecording()) {
m_wavFileRecord->stopRecording();
}
m_wavFileRecord->setSampleRate(m_sinkSampleRate / (1<<m_settings.m_log2Decim));
}
}
void DemodAnalyzerWorker::handleData()
{
QMutexLocker mutexLocker(&m_mutex);

View File

@ -37,6 +37,8 @@ class BasebandSampleSink;
class ScopeVis;
class ChannelAPI;
class Feature;
class WavFileRecord;
class DemodAnalyzerWorker : public QObject {
Q_OBJECT
public:
@ -91,12 +93,6 @@ public:
MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; }
void setMessageQueueToFeature(MessageQueue *messageQueue) { m_msgQueueToFeature = messageQueue; }
void feedPart(
const QByteArray::const_iterator& begin,
const QByteArray::const_iterator& end,
DataFifo::DataType dataType
);
void applySampleRate(int sampleRate);
void applySettings(const DemodAnalyzerSettings& settings, bool force = false);
@ -121,10 +117,21 @@ private:
int m_sampleBufferSize;
MovingAverageUtil<double, double, 480> m_channelPowerAvg;
ScopeVis* m_scopeVis;
WavFileRecord* m_wavFileRecord;
int m_recordSilenceNbSamples;
int m_recordSilenceCount;
int m_nbBytes;
QRecursiveMutex m_mutex;
void feedPart(
const QByteArray::const_iterator& begin,
const QByteArray::const_iterator& end,
DataFifo::DataType dataType
);
bool handleMessage(const Message& cmd);
void decimate(int countSamples);
void writeSampleToFile(const Sample& sample);
inline void processSample(
DataFifo::DataType dataType,

View File

@ -73,6 +73,35 @@ This is the resulting sample rate after possible decimation that is used by the
Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band.
<h3>A.8. Record as .wav file</h3>
Use this button to toggle recording. Start or stop recording
Format is always 16 bit little-endian and can be mono (1 channel) or stereo (2 channels) depending on data type.
<h3>A.9. Select recording output file</h3>
Click on this icon to open a file selection dialog that lets you specify the location and name of the output files.
Each recording is written in a new file with the starting timestamp before the `.wav` extension in `yyyy-MM-ddTHH_mm_ss_zzz` format. It keeps the first dot limited groups of the filename before the `.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.wav` then a recording file will be like: `test.2020-08-05T21_39_07_974.wav`
- Given file name: `test.2020-08-05T20_36_15_974.wav` then a recording file will be like (with timestamp updated): `test.2020-08-05T21_41_21_173.wav`
- Given file name: `test.first.wav` then a recording file will be like: `test.2020-08-05T22_00_07_974.wav`
- Given file name: `record.test.first.eav` then a recording file will be like: `reocrd.test.2020-08-05T21_39_52_974.wav`
If a filename is given without `.wav` extension then the `.wav` extension is appended automatically before the above algorithm is applied. If a filename is given with an extension different of `.wav` then the extension is replaced by `.wav` automatically before the above algorithm is applied.
The file path currently being written (or last closed) appears at the right of the button (A.1.10).
<h3>A.1.10. Recording file path</h3>
This is the file path currently being written (or last closed).
<h3>A.1.11 Record silence time</h3>
This is the time in seconds (between 0.1 and 10.0) of silence (null samples) before recording stops. When non null samples come again this will start a new recording. Set the value to 0 to record continuously.
<h2>B. Spectrum view</h2>
This is the same display as with the channel analyzer spectrum view. This is the spectrum of a real signal so it is symmetrical around zero frequency. Details on the spectrum view and controls can be found [here](../../../sdrgui/gui/spectrum.md)

View File

@ -82,8 +82,9 @@ void WavFileRecord::feed(const SampleVector::const_iterator& begin, const Sample
{
(void) positiveOnly;
if(!m_recordOn)
if (!m_recordOn) {
return;
}
if (begin < end) // if there is something to put out
{

View File

@ -94,6 +94,7 @@ public:
virtual int getBytesPerSample() override { return 4; };
const QString& getCurrentFileName() override { return m_currentFileName; }
void setMono(bool mono) { m_nbChannels = mono ? 1 : 2; }
void setSampleRate(quint32 sampleRate) { m_sampleRate = sampleRate; }
void genUniqueFileName(uint deviceUID, int istream = -1);