From 754f8b27f16dc488573e25b3a37c2c045fb4e141 Mon Sep 17 00:00:00 2001 From: Vladisslav P Date: Sat, 1 Jan 2022 04:20:37 +0300 Subject: [PATCH 1/6] iq_tool: prevent accidental change of hw freq ...while playing back IQ file. Problem: It is possible to change the center frequency while playing an IQ file. In this case the real file center frequency does not get shifted to a correct position, so the spectrum plot/waterfall becomes shifted from actual played frequency, bookmarks become not valid, freqCtrl shows wrong frequency, plotter shows wrong frequency and so on. This commit changes frequency setting logic to be more staraightfroward and consistent. Independent of frequency change event source (freqctrls, plotter, remote) do the things in a same way: calculate new center and offset, taking into account the fact, that the center frequency may be loked due to IQ file playback, set the new frequency on a receiver side, then update all GUI controls to reflect changes. Enforce new frequency limits on the plotter side when IQ playback is started. --- src/applications/gqrx/mainwindow.cpp | 73 +++++++++++++++++++--------- src/applications/gqrx/mainwindow.h | 2 + src/applications/gqrx/receiver.h | 1 + src/qtgui/freqctrl.cpp | 25 ++++++---- src/qtgui/freqctrl.h | 1 + src/qtgui/plotter.cpp | 4 ++ src/qtgui/plotter.h | 7 +++ 7 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/applications/gqrx/mainwindow.cpp b/src/applications/gqrx/mainwindow.cpp index a9023d2b0f..c8a4b53921 100644 --- a/src/applications/gqrx/mainwindow.cpp +++ b/src/applications/gqrx/mainwindow.cpp @@ -221,9 +221,6 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) /* connect signals and slots */ connect(ui->freqCtrl, SIGNAL(newFrequency(qint64)), this, SLOT(setNewFrequency(qint64))); - connect(ui->freqCtrl, SIGNAL(newFrequency(qint64)), remote, SLOT(setNewFrequency(qint64))); - connect(ui->freqCtrl, SIGNAL(newFrequency(qint64)), uiDockAudio, SLOT(setRxFrequency(qint64))); - connect(ui->freqCtrl, SIGNAL(newFrequency(qint64)), uiDockRxOpt, SLOT(setRxFreq(qint64))); connect(uiDockInputCtl, SIGNAL(lnbLoChanged(double)), this, SLOT(setLnbLo(double))); connect(uiDockInputCtl, SIGNAL(lnbLoChanged(double)), remote, SLOT(setLnbLo(double))); connect(uiDockInputCtl, SIGNAL(gainChanged(QString, double)), this, SLOT(setGain(QString,double))); @@ -237,7 +234,7 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) connect(uiDockInputCtl, SIGNAL(antennaSelected(QString)), this, SLOT(setAntenna(QString))); connect(uiDockInputCtl, SIGNAL(freqCtrlResetChanged(bool)), this, SLOT(setFreqCtrlReset(bool))); connect(uiDockInputCtl, SIGNAL(invertScrollingChanged(bool)), this, SLOT(setInvertScrolling(bool))); - connect(uiDockRxOpt, SIGNAL(rxFreqChanged(qint64)), ui->freqCtrl, SLOT(setFrequency(qint64))); + connect(uiDockRxOpt, SIGNAL(rxFreqChanged(qint64)), this, SLOT(setNewFrequency(qint64))); connect(uiDockRxOpt, SIGNAL(filterOffsetChanged(qint64)), this, SLOT(setFilterOffset(qint64))); connect(uiDockRxOpt, SIGNAL(filterOffsetChanged(qint64)), remote, SLOT(setFilterOffset(qint64))); connect(uiDockRxOpt, SIGNAL(demodSelected(int)), this, SLOT(selectDemod(int))); @@ -330,7 +327,7 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) connect(remote, SIGNAL(newRDSmode(bool)), uiDockRDS, SLOT(setRDSmode(bool))); connect(remote, SIGNAL(newFilterOffset(qint64)), this, SLOT(setFilterOffset(qint64))); connect(remote, SIGNAL(newFilterOffset(qint64)), uiDockRxOpt, SLOT(setFilterOffset(qint64))); - connect(remote, SIGNAL(newFrequency(qint64)), ui->freqCtrl, SLOT(setFrequency(qint64))); + connect(remote, SIGNAL(newFrequency(qint64)), this, SLOT(setNewFrequency(qint64))); connect(remote, SIGNAL(newLnbLo(double)), uiDockInputCtl, SLOT(setLnbLo(double))); connect(remote, SIGNAL(newLnbLo(double)), this, SLOT(setLnbLo(double))); connect(remote, SIGNAL(newMode(int)), this, SLOT(selectDemod(int))); @@ -823,6 +820,8 @@ void MainWindow::updateHWFrequencyRange(bool ignore_limits) { double startd, stopd, stepd; + d_ignore_limits = ignore_limits; + if (ignore_limits) { d_hw_freq_start = (quint64) 0; @@ -859,6 +858,7 @@ void MainWindow::updateFrequencyRange() ui->freqCtrl->setup(0, start, stop, 1, FCTL_UNIT_NONE); uiDockRxOpt->setRxFreqRange(start, stop); + ui->plotter->setFrequencyRange(start, stop); } /** @@ -905,19 +905,53 @@ void MainWindow::updateGainStages(bool read_from_device) */ void MainWindow::setNewFrequency(qint64 rx_freq) { - auto hw_freq = (double)(rx_freq - d_lnb_lo) - rx->get_filter_offset(); - auto center_freq = rx_freq - (qint64)rx->get_filter_offset(); - d_hw_freq = (qint64)hw_freq; + auto new_offset = rx->get_filter_offset(); + auto hw_freq = (double)(rx_freq - d_lnb_lo) - new_offset; + auto center_freq = rx_freq - (qint64)new_offset; + auto max_offset = rx->get_input_rate() / 2; + bool update_offset = rx->is_playing_iq(); - // set receiver frequency rx->set_rf_freq(hw_freq); + d_hw_freq = d_ignore_limits ? hw_freq : (qint64)rx->get_rf_freq(); + update_offset |= (d_hw_freq != (qint64)hw_freq); + + if (rx_freq - d_lnb_lo - d_hw_freq > max_offset) + { + rx_freq = d_lnb_lo + d_hw_freq + max_offset; + update_offset = true; + } + if (rx_freq - d_lnb_lo - d_hw_freq < -max_offset) + { + rx_freq = d_lnb_lo + d_hw_freq - max_offset; + update_offset = true; + } + if (update_offset) + { + new_offset = rx_freq - d_lnb_lo - d_hw_freq; + if (d_hw_freq != (qint64)hw_freq) + { + center_freq = d_hw_freq + d_lnb_lo; + // set RX filter + rx->set_filter_offset((double)new_offset); + + // update RF freq label and channel filter offset + rx_freq = center_freq + new_offset; + } + } // update widgets ui->plotter->setCenterFreq(center_freq); + ui->plotter->setFilterOffset(new_offset); + uiDockRxOpt->setRxFreq(rx_freq); uiDockRxOpt->setHwFreq(d_hw_freq); + uiDockRxOpt->setFilterOffset(new_offset); ui->freqCtrl->setFrequency(rx_freq); uiDockBookmarks->setNewFrequency(rx_freq); + remote->setNewFrequency(rx_freq); + uiDockAudio->setRxFrequency(rx_freq); + if (rx->is_rds_decoder_active()) + rx->reset_rds_parser(); } // Update delta and center (of marker span) when markers are updated @@ -1002,8 +1036,7 @@ void MainWindow::setLnbLo(double freq_mhz) // Update ranges and show updated frequency updateFrequencyRange(); - ui->freqCtrl->setFrequency(d_lnb_lo + rf_freq); - ui->plotter->setCenterFreq(d_lnb_lo + d_hw_freq); + setNewFrequency(d_lnb_lo + rf_freq); // update LNB LO in settings if (freq_mhz == 0.) @@ -1026,16 +1059,12 @@ void MainWindow::setAntenna(const QString& antenna) void MainWindow::setFilterOffset(qint64 freq_hz) { rx->set_filter_offset((double) freq_hz); - ui->plotter->setFilterOffset(freq_hz); updateFrequencyRange(); auto rx_freq = d_hw_freq + d_lnb_lo + freq_hz; - ui->freqCtrl->setFrequency(rx_freq); + setNewFrequency(rx_freq); - if (rx->is_rds_decoder_active()) { - rx->reset_rds_parser(); - } } /** @@ -2102,14 +2131,14 @@ void MainWindow::on_actionIqTool_triggered() void MainWindow::on_plotter_newDemodFreq(qint64 freq, qint64 delta) { // set RX filter - rx->set_filter_offset((double) delta); + if (delta != qint64(rx->get_filter_offset())) + { + rx->set_filter_offset((double) delta); + updateFrequencyRange(); + } - // update RF freq label and channel filter offset - uiDockRxOpt->setFilterOffset(delta); - ui->freqCtrl->setFrequency(freq); + setNewFrequency(freq); - if (rx->is_rds_decoder_active()) - rx->reset_rds_parser(); } /* CPlotter::NewfilterFreq() is emitted or bookmark activated */ diff --git a/src/applications/gqrx/mainwindow.h b/src/applications/gqrx/mainwindow.h index 5151bb2c15..da57a03e0a 100644 --- a/src/applications/gqrx/mainwindow.h +++ b/src/applications/gqrx/mainwindow.h @@ -87,6 +87,8 @@ public slots: qint64 d_hw_freq_start{}; qint64 d_hw_freq_stop{}; + bool d_ignore_limits; + enum receiver::filter_shape d_filter_shape; std::vector d_iqFftData; float d_fftAvg; /*!< FFT averaging parameter set by user (not the true gain). */ diff --git a/src/applications/gqrx/receiver.h b/src/applications/gqrx/receiver.h index d3e8834c79..cb1c6ed4c8 100644 --- a/src/applications/gqrx/receiver.h +++ b/src/applications/gqrx/receiver.h @@ -210,6 +210,7 @@ class receiver status start_iq_recording(const std::string filename); status stop_iq_recording(); status seek_iq_file(long pos); + bool is_playing_iq(void) const { return input_devstr.substr(0, 5).compare("file=") == 0; } /* sample sniffer */ status start_sniffer(unsigned int samplrate, int buffsize); diff --git a/src/qtgui/freqctrl.cpp b/src/qtgui/freqctrl.cpp index d41200eca4..4bb0c91b74 100644 --- a/src/qtgui/freqctrl.cpp +++ b/src/qtgui/freqctrl.cpp @@ -51,7 +51,7 @@ CFreqCtrl::CFreqCtrl(QWidget *parent) : { setAutoFillBackground(false); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - setFocusPolicy(Qt::StrongFocus); + setFocusPolicy(Qt::WheelFocus); setMouseTracking(true); m_BkColor = QColor(0x1F, 0x1D, 0x1D, 0xFF); m_InactiveColor = QColor(0x43, 0x43, 0x43, 0xFF); @@ -193,6 +193,13 @@ void CFreqCtrl::setup(int NumDigits, qint64 Minf, qint64 Maxf, int MinStep, m_NumSeps = (m_NumDigits - 1) / 3 - m_DigStart / 3; } +void CFreqCtrl::localSetFrequency(qint64 freq) +{ + setFrequency(freq); + // signal the new frequency to world + emit newFrequency(m_freq); +} + void CFreqCtrl::setFrequency(qint64 freq) { int i; @@ -243,12 +250,10 @@ void CFreqCtrl::setFrequency(qint64 freq) m_DigitInfo[i].val = -m_DigitInfo[i].val; } } - - // signal the new frequency to world m_Oldfreq = m_freq; - emit newFrequency(m_freq); update(); m_LastLeadZeroPos = m_LeadZeroPos; + } void CFreqCtrl::setDigitColor(QColor col) @@ -466,7 +471,7 @@ void CFreqCtrl::keyPressEvent(QKeyEvent *event) m_freq -= tmp * m_DigitInfo[m_ActiveEditDigit].weight; m_freq = m_freq + (event->key() - '0') * m_DigitInfo[m_ActiveEditDigit].weight; - setFrequency(m_freq); + localSetFrequency(m_freq); } moveCursorRight(); fSkipMsg = true; @@ -654,7 +659,7 @@ void CFreqCtrl::incDigit() } m_freq = tmpl; } - setFrequency(m_freq); + localSetFrequency(m_freq); } } @@ -670,7 +675,7 @@ void CFreqCtrl::incFreq() m_freq = m_freq - m_freq % m_DigitInfo[m_ActiveEditDigit].weight; } - setFrequency(m_freq); + localSetFrequency(m_freq); } } @@ -712,7 +717,7 @@ void CFreqCtrl::decDigit() } m_freq = tmpl; } - setFrequency(m_freq); + localSetFrequency(m_freq); } } @@ -729,7 +734,7 @@ void CFreqCtrl::decFreq() m_DigitInfo[m_ActiveEditDigit].weight; } - setFrequency(m_freq); + localSetFrequency(m_freq); } } @@ -744,7 +749,7 @@ void CFreqCtrl::clearFreq() /* digits below the active one are reset to 0 */ m_freq -= m_freq % m_DigitInfo[m_ActiveEditDigit].weight; - setFrequency(m_freq); + localSetFrequency(m_freq); } } diff --git a/src/qtgui/freqctrl.h b/src/qtgui/freqctrl.h index 3338dcc684..ed225cf3bc 100644 --- a/src/qtgui/freqctrl.h +++ b/src/qtgui/freqctrl.h @@ -85,6 +85,7 @@ public slots: void moveCursorRight(); bool inRect(QRect &rect, QPointF &point); void setActiveDigit(int idx); + void localSetFrequency(qint64 freq); bool m_LRMouseFreqSel; /* Use left/right mouse buttons. If FALSE click area determines up/down. */ diff --git a/src/qtgui/plotter.cpp b/src/qtgui/plotter.cpp index d3a36dbd5a..34af001da6 100644 --- a/src/qtgui/plotter.cpp +++ b/src/qtgui/plotter.cpp @@ -385,6 +385,10 @@ void CPlotter::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::MiddleButton) { + if (m_DemodCenterFreq + delta_hz < m_MinFreq) + delta_hz = m_MinFreq - m_DemodCenterFreq; + if (m_DemodCenterFreq + delta_hz > m_MaxFreq) + delta_hz = m_MaxFreq - m_DemodCenterFreq; m_CenterFreq += delta_hz; m_DemodCenterFreq += delta_hz; emit newDemodFreq(m_DemodCenterFreq, m_DemodCenterFreq - m_CenterFreq); diff --git a/src/qtgui/plotter.h b/src/qtgui/plotter.h index 10ca83c7b0..f9a7689535 100644 --- a/src/qtgui/plotter.h +++ b/src/qtgui/plotter.h @@ -190,6 +190,11 @@ public slots: m_Size = QSize(0,0); resizeEvent(nullptr); } + void setFrequencyRange(qint64 min, qint64 max) + { + m_MinFreq = min; + m_MaxFreq = max; + } protected: //re-implemented widget event handlers @@ -304,6 +309,8 @@ public slots: int m_CursorCaptureDelta; int m_GrabPosition; qreal m_Percent2DScreen; + qint64 m_MinFreq; + qint64 m_MaxFreq; int m_FLowCmin; int m_FLowCmax; From 103288731ddd08e5248bd386c9a7462ec64eb205 Mon Sep 17 00:00:00 2001 From: Vladislav P Date: Tue, 18 Apr 2023 00:50:32 +0300 Subject: [PATCH 2/6] iq_tool: GUI improvements IQ recorder: disable harmful buttons while recording is in progress. Changing IO devices and loading/saving settings does not look like good thing to do while recording an IQ file. IQ tool: Always choose correct sampling rate Reselect file before starting playback. Fixes incorrect sample rate when playback is started, stopped, devices switched, dsp started, stopped and then started playback of the same IQ file. IQ tool: disable/enable controls properly Disable directory selector, file list while playing/recording IQ file. Disable slider while recording IQ file. --- src/applications/gqrx/mainwindow.cpp | 12 +++++++++++ src/applications/gqrx/mainwindow.h | 2 ++ src/qtgui/iq_tool.cpp | 30 +++++++++++++++++++--------- src/qtgui/iq_tool.h | 2 ++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/applications/gqrx/mainwindow.cpp b/src/applications/gqrx/mainwindow.cpp index c8a4b53921..37e200ff16 100644 --- a/src/applications/gqrx/mainwindow.cpp +++ b/src/applications/gqrx/mainwindow.cpp @@ -1708,6 +1708,8 @@ void MainWindow::startIqRecording(const QString& recdir, const QString& format) } } + ui->actionIoConfig->setDisabled(true); + ui->actionLoadSettings->setDisabled(true); // start recorder; fails if recording already in progress if (!ok || rx->start_iq_recording(lastRec.toStdString())) { @@ -1742,6 +1744,8 @@ void MainWindow::stopIqRecording() ui->statusBar->showMessage(tr("Error stopping I/Q recoder")); else ui->statusBar->showMessage(tr("I/Q data recoding stopped"), 5000); + ui->actionIoConfig->setDisabled(false); + ui->actionLoadSettings->setDisabled(false); } void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 center_freq) @@ -1753,6 +1757,8 @@ void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 } storeSession(); + backupFreq = ui->freqCtrl->getFrequency(); + backupOffset = (qint64) rx->get_filter_offset(); auto sri = (int)samprate; auto cf = center_freq; @@ -1784,6 +1790,9 @@ void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 // FIXME: would be nice with good/bad status ui->statusBar->showMessage(tr("Playing %1").arg(filename)); + ui->actionIoConfig->setDisabled(true); + ui->actionLoadSettings->setDisabled(true); + ui->actionSaveSettings->setDisabled(true); on_actionDSP_triggered(true); } @@ -1797,6 +1806,9 @@ void MainWindow::stopIqPlayback() } ui->statusBar->showMessage(tr("I/Q playback stopped"), 5000); + ui->actionIoConfig->setDisabled(false); + ui->actionLoadSettings->setDisabled(false); + ui->actionSaveSettings->setDisabled(false); // restore original input device auto indev = m_settings->value("input/device", "").toString(); diff --git a/src/applications/gqrx/mainwindow.h b/src/applications/gqrx/mainwindow.h index da57a03e0a..cc023d53b0 100644 --- a/src/applications/gqrx/mainwindow.h +++ b/src/applications/gqrx/mainwindow.h @@ -86,6 +86,8 @@ public slots: bool d_show_markers; qint64 d_hw_freq_start{}; qint64 d_hw_freq_stop{}; + qint64 backupFreq; /* for IQ player */ + qint64 backupOffset; /* for IQ player */ bool d_ignore_limits; diff --git a/src/qtgui/iq_tool.cpp b/src/qtgui/iq_tool.cpp index 6a376d7fcc..363cc1a278 100644 --- a/src/qtgui/iq_tool.cpp +++ b/src/qtgui/iq_tool.cpp @@ -101,6 +101,20 @@ void CIqTool::on_listWidget_currentTextChanged(const QString ¤tText) } +/*! \brief Show/hide/enable/disable GUI controls */ + +void CIqTool::switchControlsState(bool recording, bool playback) +{ + ui->recButton->setEnabled(!playback); + + ui->playButton->setEnabled(!recording); + ui->slider->setEnabled(!recording); + + ui->listWidget->setEnabled(!(recording || playback)); + ui->recDirEdit->setEnabled(!(recording || playback)); + ui->recDirButton->setEnabled(!(recording || playback)); +} + /*! \brief Start/stop playback */ void CIqTool::on_playButton_clicked(bool checked) { @@ -126,8 +140,9 @@ void CIqTool::on_playButton_clicked(bool checked) } else { - ui->listWidget->setEnabled(false); - ui->recButton->setEnabled(false); + on_listWidget_currentTextChanged(current_file); + switchControlsState(false, true); + emit startPlayback(recdir->absoluteFilePath(current_file), (float)sample_rate, center_freq); } @@ -135,8 +150,7 @@ void CIqTool::on_playButton_clicked(bool checked) else { emit stopPlayback(); - ui->listWidget->setEnabled(true); - ui->recButton->setEnabled(true); + switchControlsState(false, false); ui->slider->setValue(0); } } @@ -149,9 +163,7 @@ void CIqTool::on_playButton_clicked(bool checked) */ void CIqTool::cancelPlayback() { - ui->playButton->setChecked(false); - ui->listWidget->setEnabled(true); - ui->recButton->setEnabled(true); + switchControlsState(false, false); is_playing = false; } @@ -172,7 +184,7 @@ void CIqTool::on_recButton_clicked(bool checked) if (checked) { - ui->playButton->setEnabled(false); + switchControlsState(true, false); emit startRecording(recdir->path(), ui->formatCombo->currentText()); refreshDir(); @@ -180,7 +192,7 @@ void CIqTool::on_recButton_clicked(bool checked) } else { - ui->playButton->setEnabled(true); + switchControlsState(false, false); emit stopRecording(); } } diff --git a/src/qtgui/iq_tool.h b/src/qtgui/iq_tool.h index dc56bd35a9..54e69efa82 100644 --- a/src/qtgui/iq_tool.h +++ b/src/qtgui/iq_tool.h @@ -87,6 +87,8 @@ private slots: void refreshDir(void); void refreshTimeWidgets(void); void parseFileName(const QString &filename); + void switchControlsState(bool recording, bool playback); + private: Ui::CIqTool *ui; From 7ace028d039448cc8413332954c24759a5617645 Mon Sep 17 00:00:00 2001 From: Vladisslav P Date: Mon, 10 Jan 2022 18:57:01 +0300 Subject: [PATCH 3/6] iq_formats: Implement recording/playback of different sample formats --- src/applications/gqrx/mainwindow.cpp | 70 ++-- src/applications/gqrx/mainwindow.h | 6 +- src/applications/gqrx/receiver.cpp | 417 ++++++++++++++++++----- src/applications/gqrx/receiver.h | 51 ++- src/applications/gqrx/remote_control.cpp | 2 +- src/applications/gqrx/remote_control.h | 2 +- src/dsp/CMakeLists.txt | 1 + src/dsp/format_converter.h | 324 ++++++++++++++++++ src/qtgui/iq_tool.cpp | 92 ++++- src/qtgui/iq_tool.h | 11 +- src/qtgui/iq_tool.ui | 84 +++-- 11 files changed, 905 insertions(+), 155 deletions(-) create mode 100644 src/dsp/format_converter.h diff --git a/src/applications/gqrx/mainwindow.cpp b/src/applications/gqrx/mainwindow.cpp index 37e200ff16..5267f7612a 100644 --- a/src/applications/gqrx/mainwindow.cpp +++ b/src/applications/gqrx/mainwindow.cpp @@ -68,6 +68,7 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) ui(new Ui::MainWindow), d_lnb_lo(0), d_hw_freq(0), + d_ignore_limits(false), d_fftAvg(0.25), d_fftWindowType(0), d_fftNormalizeEnergy(false), @@ -315,11 +316,11 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) connect(&DXCSpots::Get(), SIGNAL(dxcSpotsUpdated()), this, SLOT(updateClusterSpots())); // I/Q playback - connect(iq_tool, SIGNAL(startRecording(QString, QString)), this, SLOT(startIqRecording(QString, QString))); - connect(iq_tool, SIGNAL(startRecording(QString, QString)), remote, SLOT(startIqRecorder(QString, QString))); + connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats)), this, SLOT(startIqRecording(QString, enum receiver::file_formats))); + connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats)), remote, SLOT(startIqRecorder())); connect(iq_tool, SIGNAL(stopRecording()), this, SLOT(stopIqRecording())); connect(iq_tool, SIGNAL(stopRecording()), remote, SLOT(stopIqRecorder())); - connect(iq_tool, SIGNAL(startPlayback(QString,float,qint64)), this, SLOT(startIqPlayback(QString,float,qint64))); + connect(iq_tool, SIGNAL(startPlayback(QString, float, qint64, enum receiver::file_formats)), this, SLOT(startIqPlayback(QString, float, qint64, enum receiver::file_formats))); connect(iq_tool, SIGNAL(stopPlayback()), this, SLOT(stopIqPlayback())); connect(iq_tool, SIGNAL(seek(qint64)), this,SLOT(seekIqFile(qint64))); @@ -1667,20 +1668,51 @@ void MainWindow::stopAudioStreaming() } /** Start I/Q recording. */ -void MainWindow::startIqRecording(const QString& recdir, const QString& format) +void MainWindow::startIqRecording(const QString& recdir, receiver::file_formats fmt) { - qDebug() << __func__; // generate file name using date, time, rf freq in kHz and BW in Hz // gqrx_iq_yyyymmdd_hhmmss_freq_bw_fc.raw auto freq = qRound64(rx->get_rf_freq()); auto sr = qRound64(rx->get_input_rate()); auto dec = (quint32)(rx->get_input_decim()); - auto currentDate = QDateTime::currentDateTimeUtc(); - auto filenameTemplate = currentDate.toString("%1/gqrx_yyyyMMdd_hhmmss_%2_%3_fc.%4").arg(recdir).arg(freq).arg(sr/dec); - bool sigmf = (format == "SigMF"); - auto lastRec = filenameTemplate.arg(sigmf ? "sigmf-data" : "raw"); - - QFile metaFile(filenameTemplate.arg("sigmf-meta")); + QString suffix = "fc.raw"; + QString suffix_meta = "fc.sigmf-meta"; + switch(fmt) + { + case receiver::FILE_FORMAT_CS8: + suffix = "8.raw"; + break; + case receiver::FILE_FORMAT_CS16L: + suffix = "16.raw"; + break; + case receiver::FILE_FORMAT_CS32L: + suffix = "32.raw"; + break; + case receiver::FILE_FORMAT_CS8U: + suffix = "8u.raw"; + break; + case receiver::FILE_FORMAT_CS16LU: + suffix = "16u.raw"; + break; + case receiver::FILE_FORMAT_CS32LU: + suffix = "32u.raw"; + break; + case receiver::FILE_FORMAT_SIGMF: + suffix = "fc.sigmf-data"; + break; + default: + fmt = receiver::FILE_FORMAT_CF; + suffix = "fc.raw"; + } + auto currentDateTime = QDateTime::currentDateTimeUtc(); + auto lastRec = currentDateTime. + toString("%1/gqrx_yyyyMMdd_hhmmss_%2_%3_%4") + .arg(recdir).arg(freq).arg(sr/dec).arg(suffix); + auto metaName = currentDateTime. + toString("%1/gqrx_yyyyMMdd_hhmmss_%2_%3_%4") + .arg(recdir).arg(freq).arg(sr/dec).arg(suffix_meta); + bool sigmf = (fmt == receiver::FILE_FORMAT_SIGMF); + QFile metaFile(metaName); bool ok = true; if (sigmf) { auto meta = QJsonDocument { QJsonObject { @@ -1698,20 +1730,21 @@ void MainWindow::startIqRecording(const QString& recdir, const QString& format) QJsonObject { {"core:sample_start", 0}, {"core:frequency", freq}, - {"core:datetime", currentDate.toString(Qt::ISODateWithMs)}, + {"core:datetime", currentDateTime.toString(Qt::ISODateWithMs)}, }, }}, {"annotations", QJsonArray {}}, }}.toJson(); if (!metaFile.open(QIODevice::WriteOnly) || metaFile.write(meta) != meta.size()) { ok = false; - } + }else + metaFile.close(); } ui->actionIoConfig->setDisabled(true); ui->actionLoadSettings->setDisabled(true); // start recorder; fails if recording already in progress - if (!ok || rx->start_iq_recording(lastRec.toStdString())) + if (!ok || rx->start_iq_recording(lastRec.toStdString(), fmt)) { // remove metadata file if we managed to open it if (sigmf && metaFile.isOpen()) @@ -1719,6 +1752,7 @@ void MainWindow::startIqRecording(const QString& recdir, const QString& format) // reset action status ui->statusBar->showMessage(tr("Error starting I/Q recoder")); + iq_tool->cancelRecording(); // show an error message to user QMessageBox msg_box; @@ -1748,7 +1782,7 @@ void MainWindow::stopIqRecording() ui->actionLoadSettings->setDisabled(false); } -void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 center_freq) +void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 center_freq, enum receiver::file_formats fmt) { if (ui->actionDSP->isChecked()) { @@ -1757,8 +1791,6 @@ void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 } storeSession(); - backupFreq = ui->freqCtrl->getFrequency(); - backupOffset = (qint64) rx->get_filter_offset(); auto sri = (int)samprate; auto cf = center_freq; @@ -1771,6 +1803,7 @@ void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 rx->set_input_device(devstr.toStdString()); updateHWFrequencyRange(false); + rx->set_input_file(filename.toStdString(), samprate, fmt); // sample rate auto actual_rate = rx->set_input_rate((double)samprate); @@ -1828,9 +1861,6 @@ void MainWindow::stopIqPlayback() ui->plotter->setSampleRate(actual_rate); ui->plotter->setSpanFreq((quint32)actual_rate); remote->setBandwidth(sr); - - // not needed as long as we are not recording in iq_tool - //iq_tool->setSampleRate(sr); } // restore frequency, gain, etc... diff --git a/src/applications/gqrx/mainwindow.h b/src/applications/gqrx/mainwindow.h index cc023d53b0..67ae771641 100644 --- a/src/applications/gqrx/mainwindow.h +++ b/src/applications/gqrx/mainwindow.h @@ -86,8 +86,6 @@ public slots: bool d_show_markers; qint64 d_hw_freq_start{}; qint64 d_hw_freq_stop{}; - qint64 backupFreq; /* for IQ player */ - qint64 backupOffset; /* for IQ player */ bool d_ignore_limits; @@ -199,9 +197,9 @@ private slots: void stopAudioStreaming(); /* I/Q playback and recording*/ - void startIqRecording(const QString& recdir, const QString& format); + void startIqRecording(const QString& recdir, enum receiver::file_formats fmt); void stopIqRecording(); - void startIqPlayback(const QString& filename, float samprate, qint64 center_freq); + void startIqPlayback(const QString& filename, float samprate, qint64 center_freq, enum receiver::file_formats fmt); void stopIqPlayback(); void seekIqFile(qint64 seek_pos); diff --git a/src/applications/gqrx/receiver.cpp b/src/applications/gqrx/receiver.cpp index 9bdf979ff3..0b06884f9e 100644 --- a/src/applications/gqrx/receiver.cpp +++ b/src/applications/gqrx/receiver.cpp @@ -72,6 +72,8 @@ receiver::receiver(const std::string input_device, d_iq_rev(false), d_dc_cancel(false), d_iq_balance(false), + d_iq_fmt(FILE_FORMAT_NONE), + d_last_format(FILE_FORMAT_NONE), d_demod(RX_DEMOD_OFF) { @@ -114,6 +116,23 @@ receiver::receiver(const std::string input_device, ddc = make_downconverter_cc(d_ddc_decim, 0.0, d_decim_rate); rx = make_nbrx(d_quad_rate, d_audio_rate); + input_file = gr::blocks::file_source::make(sizeof(gr_complex),get_zero_file().c_str(),false); + input_throttle = gr::blocks::throttle::make(sizeof(gr_complex),192000.0); + + to_s32lc = any_to_any>::make(); + from_s32lc = any_to_any,gr_complex>::make(); + to_s16lc = any_to_any>::make(); + from_s16lc = any_to_any,gr_complex>::make(); + to_s8c = any_to_any>::make(); + from_s8c = any_to_any,gr_complex>::make(); + + to_s32luc = any_to_any>::make(); + from_s32luc = any_to_any,gr_complex>::make(); + to_s16luc = any_to_any>::make(); + from_s16luc = any_to_any,gr_complex>::make(); + to_s8uc = any_to_any>::make(); + from_s8uc = any_to_any,gr_complex>::make(); + iq_swap = make_iq_swap_cc(false); dc_corr = make_dc_corr_cc(d_decim_rate, 1.0); iq_fft = make_rx_fft_c(DEFAULT_FFT_SIZE, d_decim_rate, gr::fft::window::WIN_HANN); @@ -199,25 +218,18 @@ void receiver::set_input_device(const std::string device) tb->wait(); } - if (d_decim >= 2) - { - tb->disconnect(src, 0, input_decim, 0); - tb->disconnect(input_decim, 0, iq_swap, 0); - } - else - { - tb->disconnect(src, 0, iq_swap, 0); - } + tb->disconnect_all(); #if GNURADIO_VERSION < 0x030802 //Work around GNU Radio bug #3184 //temporarily connect dummy source to ensure that previous device is closed src = osmosdr::source::make("file="+escape_filename(get_zero_file())+",freq=428e6,rate=96000,repeat=true,throttle=true"); - tb->connect(src, 0, iq_swap, 0); + auto tmp_sink = gr::blocks::null_sink::make(sizeof(gr_complex)); + tb->connect(src, 0, tmp_sink, 0); tb->start(); tb->stop(); tb->wait(); - tb->disconnect(src, 0, iq_swap, 0); + tb->disconnect_all(); #else src.reset(); #endif @@ -229,21 +241,46 @@ void receiver::set_input_device(const std::string device) catch (std::exception &x) { error = x.what(); - src = osmosdr::source::make("file="+escape_filename(get_zero_file())+",freq=428e6,rate=96000,repeat=true,throttle=true"); + src = osmosdr::source::make("file=" + escape_filename(get_zero_file()) + ",freq=428e6,rate=96000,repeat=true,throttle=true"); } + set_demod(d_demod, FILE_FORMAT_NONE, true); if(src->get_sample_rate() != 0) set_input_rate(src->get_sample_rate()); - if (d_decim >= 2) + if (d_running) + tb->start(); + + if (error != "") { - tb->connect(src, 0, input_decim, 0); - tb->connect(input_decim, 0, iq_swap, 0); + throw std::runtime_error(error); } - else +} + +/** + * @brief Select a file as an input device. + * @param name + * @param sample_rate + * @param fmt + */ +void receiver::set_input_file(const std::string name, const int sample_rate, const enum file_formats fmt) +{ + std::string error = ""; + size_t sample_size = sample_size_from_format(fmt); + + input_file = gr::blocks::file_source::make(sample_size, name.c_str(), false); + + if (d_running) { - tb->connect(src, 0, iq_swap, 0); - } + tb->stop(); + tb->wait(); + }; + + tb->disconnect_all(); + + input_throttle = gr::blocks::throttle::make(sizeof(gr_complex), sample_rate); + set_demod(d_demod, fmt, true); + set_input_rate(sample_rate); if (d_running) tb->start(); @@ -252,8 +289,87 @@ void receiver::set_input_device(const std::string device) { throw std::runtime_error(error); } + input_devstr = "NULL"; } +/** + * @brief Setup input part of the graph for a file ar a device + * @param fmt + */ +void receiver::setup_source(enum file_formats fmt) +{ + gr::basic_block_sptr b; + + if(fmt == FILE_FORMAT_LAST) + fmt = d_last_format; + else + d_last_format = fmt; + + b = input_throttle; + + switch(fmt) + { + case FILE_FORMAT_LAST: + break; + case FILE_FORMAT_NONE: + // Setup source + b = src; + + // Pre-processing + if (d_decim >= 2) + { + tb->connect(b, 0, input_decim, 0); + b = input_decim; + } + + if (d_recording_iq) + { + // We record IQ with minimal pre-processing + connect_iq_recorder(); + } + + tb->connect(b, 0, iq_swap, 0); + return; + case FILE_FORMAT_CF: + case FILE_FORMAT_SIGMF: + tb->connect(input_file, 0 , b, 0); + break; + case FILE_FORMAT_CS32L: + tb->connect(input_file, 0 ,from_s32lc, 0); + tb->connect(from_s32lc, 0, b, 0); + break; + case FILE_FORMAT_CS16L: + tb->connect(input_file, 0 ,from_s16lc, 0); + tb->connect(from_s16lc, 0, b, 0); + break; + case FILE_FORMAT_CS8: + tb->connect(input_file, 0 ,from_s8c, 0); + tb->connect(from_s8c, 0, b, 0); + break; + case FILE_FORMAT_CS32LU: + tb->connect(input_file, 0 ,from_s32luc, 0); + tb->connect(from_s32luc, 0, b, 0); + break; + case FILE_FORMAT_CS16LU: + tb->connect(input_file, 0 ,from_s16luc, 0); + tb->connect(from_s16luc, 0, b, 0); + break; + case FILE_FORMAT_CS8U: + tb->connect(input_file, 0 ,from_s8uc, 0); + tb->connect(from_s8uc, 0, b, 0); + break; + } + + if (d_decim >= 2) + { + tb->connect(b, 0, input_decim, 0); + tb->connect(input_decim, 0, iq_swap, 0); + } + else + { + tb->connect(b, 0, iq_swap, 0); + } +} /** * @brief Select new audio output device. @@ -271,8 +387,20 @@ void receiver::set_output_device(const std::string device) if (d_demod != RX_DEMOD_OFF) { - tb->disconnect(audio_gain0, 0, audio_snk, 0); - tb->disconnect(audio_gain1, 0, audio_snk, 1); + try + { + tb->disconnect(audio_gain0); + } + catch(std::exception &x) + { + } + try + { + tb->disconnect(audio_gain1); + } + catch(std::exception &x) + { + } } audio_snk.reset(); @@ -287,6 +415,8 @@ void receiver::set_output_device(const std::string device) if (d_demod != RX_DEMOD_OFF) { + tb->connect(rx, 0, audio_gain0, 0); + tb->connect(rx, 1, audio_gain1, 0); tb->connect(audio_gain0, 0, audio_snk, 0); tb->connect(audio_gain1, 0, audio_snk, 1); } @@ -491,7 +621,7 @@ void receiver::set_dc_cancel(bool enable) // until we have a way to switch on/off // inside the dc_corr_cc we do a reconf - set_demod(d_demod, true); + set_demod(d_demod, FILE_FORMAT_LAST, true); } /** @@ -862,7 +992,7 @@ receiver::status receiver::set_agc_manual_gain(int gain) return STATUS_OK; // FIXME } -receiver::status receiver::set_demod(rx_demod demod, bool force) +receiver::status receiver::set_demod(rx_demod demod, enum file_formats fmt, bool force) { status ret = STATUS_OK; @@ -881,46 +1011,46 @@ receiver::status receiver::set_demod(rx_demod demod, bool force) switch (demod) { case RX_DEMOD_OFF: - connect_all(RX_CHAIN_NONE); + connect_all(RX_CHAIN_NONE, fmt); break; case RX_DEMOD_NONE: - connect_all(RX_CHAIN_NBRX); + connect_all(RX_CHAIN_NBRX, fmt); rx->set_demod(nbrx::NBRX_DEMOD_NONE); break; case RX_DEMOD_AM: - connect_all(RX_CHAIN_NBRX); + connect_all(RX_CHAIN_NBRX, fmt); rx->set_demod(nbrx::NBRX_DEMOD_AM); break; case RX_DEMOD_AMSYNC: - connect_all(RX_CHAIN_NBRX); + connect_all(RX_CHAIN_NBRX, fmt); rx->set_demod(nbrx::NBRX_DEMOD_AMSYNC); break; case RX_DEMOD_NFM: - connect_all(RX_CHAIN_NBRX); + connect_all(RX_CHAIN_NBRX, fmt); rx->set_demod(nbrx::NBRX_DEMOD_FM); break; case RX_DEMOD_WFM_M: - connect_all(RX_CHAIN_WFMRX); + connect_all(RX_CHAIN_WFMRX, fmt); rx->set_demod(wfmrx::WFMRX_DEMOD_MONO); break; case RX_DEMOD_WFM_S: - connect_all(RX_CHAIN_WFMRX); + connect_all(RX_CHAIN_WFMRX, fmt); rx->set_demod(wfmrx::WFMRX_DEMOD_STEREO); break; case RX_DEMOD_WFM_S_OIRT: - connect_all(RX_CHAIN_WFMRX); + connect_all(RX_CHAIN_WFMRX, fmt); rx->set_demod(wfmrx::WFMRX_DEMOD_STEREO_UKW); break; case RX_DEMOD_SSB: - connect_all(RX_CHAIN_NBRX); + connect_all(RX_CHAIN_NBRX, fmt); rx->set_demod(nbrx::NBRX_DEMOD_SSB); break; @@ -1194,13 +1324,95 @@ receiver::status receiver::stop_udp_streaming() return STATUS_OK; } +/** + * @brief Connect I/Q data recorder blocks. + */ +receiver::status receiver::connect_iq_recorder() +{ + gr::basic_block_sptr b; + + if (d_decim >= 2) + b = input_decim; + else + b = src; + + switch(d_iq_fmt) + { + case FILE_FORMAT_CS8: + { + tb->lock(); + tb->connect(b, 0, to_s8c, 0); + tb->connect(to_s8c, 0, iq_sink, 0); + d_recording_iq = true; + tb->unlock(); + } + break; + case FILE_FORMAT_CS16L: + { + tb->lock(); + tb->connect(b, 0, to_s16lc, 0); + tb->connect(to_s16lc, 0, iq_sink, 0); + d_recording_iq = true; + tb->unlock(); + } + break; + case FILE_FORMAT_CS32L: + { + tb->lock(); + tb->connect(b, 0, to_s32lc, 0); + tb->connect(to_s32lc, 0, iq_sink, 0); + d_recording_iq = true; + tb->unlock(); + } + break; + case FILE_FORMAT_CS8U: + { + tb->lock(); + tb->connect(b, 0, to_s8uc, 0); + tb->connect(to_s8uc, 0, iq_sink, 0); + d_recording_iq = true; + tb->unlock(); + } + break; + case FILE_FORMAT_CS16LU: + { + tb->lock(); + tb->connect(b, 0, to_s16luc, 0); + tb->connect(to_s16luc, 0, iq_sink, 0); + d_recording_iq = true; + tb->unlock(); + } + break; + case FILE_FORMAT_CS32LU: + { + tb->lock(); + tb->connect(b, 0, to_s32luc, 0); + tb->connect(to_s32luc, 0, iq_sink, 0); + d_recording_iq = true; + tb->unlock(); + } + break; + case FILE_FORMAT_CF: + case FILE_FORMAT_SIGMF: + tb->lock(); + tb->connect(b, 0, iq_sink, 0); + d_recording_iq = true; + tb->unlock(); + break; + default: + throw std::runtime_error("receiver::connect_iq_recorder: Invalid IQ file format"); + } + return STATUS_OK; +} + /** * @brief Start I/Q data recorder. * @param filename The filename where to record. ++ * @param bytes_per_sample A hint to choose correct sample format. */ -receiver::status receiver::start_iq_recording(const std::string filename) +receiver::status receiver::start_iq_recording(const std::string filename, const enum file_formats fmt) { - receiver::status status = STATUS_OK; + int sink_bytes_per_sample = sample_size_from_format(fmt); if (d_recording_iq) { std::cout << __func__ << ": already recording" << std::endl; @@ -1209,7 +1421,7 @@ receiver::status receiver::start_iq_recording(const std::string filename) try { - iq_sink = gr::blocks::file_sink::make(sizeof(gr_complex), filename.c_str(), true); + iq_sink = gr::blocks::file_sink::make(sink_bytes_per_sample, filename.c_str(), true); } catch (std::runtime_error &e) { @@ -1217,33 +1429,53 @@ receiver::status receiver::start_iq_recording(const std::string filename) return STATUS_ERROR; } - tb->lock(); - if (d_decim >= 2) - tb->connect(input_decim, 0, iq_sink, 0); - else - tb->connect(src, 0, iq_sink, 0); - d_recording_iq = true; - tb->unlock(); - - return status; -} + d_iq_fmt = fmt; + return connect_iq_recorder(); + } /** Stop I/Q data recorder. */ receiver::status receiver::stop_iq_recording() { - if (!d_recording_iq) { + if (!d_recording_iq){ /* error: we are not recording */ return STATUS_ERROR; } tb->lock(); iq_sink->close(); - - if (d_decim >= 2) - tb->disconnect(input_decim, 0, iq_sink, 0); - else - tb->disconnect(src, 0, iq_sink, 0); - + switch(d_iq_fmt) + { + case FILE_FORMAT_CS8: + tb->disconnect(iq_sink); + tb->disconnect(to_s8c); + break; + case FILE_FORMAT_CS16L: + tb->disconnect(iq_sink); + tb->disconnect(to_s16lc); + break; + case FILE_FORMAT_CS32L: + tb->disconnect(iq_sink); + tb->disconnect(to_s32lc); + break; + case FILE_FORMAT_CS8U: + tb->disconnect(iq_sink); + tb->disconnect(to_s8uc); + break; + case FILE_FORMAT_CS16LU: + tb->disconnect(iq_sink); + tb->disconnect(to_s16luc); + break; + case FILE_FORMAT_CS32LU: + tb->disconnect(iq_sink); + tb->disconnect(to_s32luc); + break; + case FILE_FORMAT_CF: + case FILE_FORMAT_SIGMF: + tb->disconnect(iq_sink); + break; + default: + throw std::runtime_error("receiver::stop_iq_recording: Invalid IQ file format"); + } tb->unlock(); iq_sink.reset(); d_recording_iq = false; @@ -1261,7 +1493,7 @@ receiver::status receiver::seek_iq_file(long pos) tb->lock(); - if (src->seek(pos, SEEK_SET)) + if (input_file->seek(pos, SEEK_SET)) { status = STATUS_OK; } @@ -1333,27 +1565,13 @@ void receiver::get_sniffer_data(float * outbuff, unsigned int &num) } /** Convenience function to connect all blocks. */ -void receiver::connect_all(rx_chain type) +void receiver::connect_all(rx_chain type, enum file_formats fmt) { gr::basic_block_sptr b; // Setup source - b = src; + setup_source(fmt); - // Pre-processing - if (d_decim >= 2) - { - tb->connect(b, 0, input_decim, 0); - b = input_decim; - } - - if (d_recording_iq) - { - // We record IQ with minimal pre-processing - tb->connect(b, 0, iq_sink, 0); - } - - tb->connect(b, 0, iq_swap, 0); b = iq_swap; if (d_dc_cancel) @@ -1400,22 +1618,38 @@ void receiver::connect_all(rx_chain type) tb->connect(rx, 1, audio_gain1, 0); tb->connect(audio_gain0, 0, audio_snk, 0); tb->connect(audio_gain1, 0, audio_snk, 1); + // Recorders and sniffers + if (d_recording_wav) + { + tb->connect(rx, 0, wav_gain0, 0); + tb->connect(rx, 1, wav_gain1, 0); + tb->connect(wav_gain0, 0, wav_sink, 0); + tb->connect(wav_gain1, 0, wav_sink, 1); + } + if (d_sniffer_active) + { + tb->connect(rx, 0, sniffer_rr, 0); + tb->connect(sniffer_rr, 0, sniffer, 0); + } } - - // Recorders and sniffers - if (d_recording_wav) + else { - tb->connect(rx, 0, wav_gain0, 0); - tb->connect(rx, 1, wav_gain1, 0); - tb->connect(wav_gain0, 0, wav_sink, 0); - tb->connect(wav_gain1, 0, wav_sink, 1); - } + if (d_recording_wav) + { + wav_sink->close(); + wav_sink.reset(); + d_recording_wav = false; + } - if (d_sniffer_active) - { - tb->connect(rx, 0, sniffer_rr, 0); - tb->connect(sniffer_rr, 0, sniffer, 0); + if (d_sniffer_active) + { + d_sniffer_active = false; + + /* delete resampler */ + sniffer_rr.reset(); + } } + } void receiver::get_rds_data(std::string &outbuff, int &num) @@ -1461,6 +1695,29 @@ void receiver::reset_rds_parser(void) rx->reset_rds_parser(); } +int receiver::sample_size_from_format(enum file_formats fmt) +{ + switch(fmt) + { + case FILE_FORMAT_LAST: + throw std::runtime_error("receiver::sample_size_from_format: Invalid format requested"); + case FILE_FORMAT_NONE: + case FILE_FORMAT_CF: + case FILE_FORMAT_SIGMF: + case FILE_FORMAT_CS32L: + case FILE_FORMAT_CS32LU: + return 8; + case FILE_FORMAT_CS16L: + case FILE_FORMAT_CS16LU: + return 4; + case FILE_FORMAT_CS8: + case FILE_FORMAT_CS8U: + return 2; + } + throw std::runtime_error("receiver::sample_size_from_format: Invalid format requested"); + return 0; +} + std::string receiver::escape_filename(std::string filename) { std::stringstream ss1; diff --git a/src/applications/gqrx/receiver.h b/src/applications/gqrx/receiver.h index cb1c6ed4c8..9060588dbb 100644 --- a/src/applications/gqrx/receiver.h +++ b/src/applications/gqrx/receiver.h @@ -23,11 +23,13 @@ #ifndef RECEIVER_H #define RECEIVER_H -#include #include +#include +#include #include #include #include +#include #include #include #include @@ -44,6 +46,7 @@ #include "dsp/rx_fft.h" #include "dsp/sniffer_f.h" #include "dsp/resampler_xx.h" +#include "dsp/format_converter.h" #include "interfaces/udp_sink_f.h" #include "receivers/receiver_base.h" @@ -105,6 +108,19 @@ class receiver FILTER_SHAPE_SHARP = 2 /*!< Sharp: Transition band is TBD of width. */ }; + enum file_formats { + FILE_FORMAT_LAST=0, + FILE_FORMAT_NONE, + FILE_FORMAT_CF, + FILE_FORMAT_CS8, + FILE_FORMAT_CS16L, + FILE_FORMAT_CS32L, + FILE_FORMAT_CS8U, + FILE_FORMAT_CS16LU, + FILE_FORMAT_CS32LU, + FILE_FORMAT_SIGMF, + }; + static const unsigned int DEFAULT_FFT_SIZE = 8192; receiver(const std::string input_device="", @@ -116,6 +132,7 @@ class receiver void stop(); void set_input_device(const std::string device); void set_output_device(const std::string device); + void set_input_file(const std::string name, const int sample_rate, const enum file_formats fmt); std::vector get_antennas(void) const; void set_antenna(const std::string &antenna); @@ -183,7 +200,9 @@ class receiver status set_agc_decay(int decay_ms); status set_agc_manual_gain(int gain); - status set_demod(rx_demod demod, bool force=false); + status set_demod(rx_demod demod, + enum file_formats fmt = FILE_FORMAT_LAST, + bool force = false); /* FM parameters */ status set_fm_maxdev(float maxdev_hz); @@ -207,7 +226,7 @@ class receiver status stop_udp_streaming(); /* I/Q recording and playback */ - status start_iq_recording(const std::string filename); + status start_iq_recording(const std::string filename, const enum file_formats fmt); status stop_iq_recording(); status seek_iq_file(long pos); bool is_playing_iq(void) const { return input_devstr.substr(0, 5).compare("file=") == 0; } @@ -229,9 +248,13 @@ class receiver /* utility functions */ static std::string escape_filename(std::string filename); + static int sample_size_from_format(enum file_formats fmt); + private: - void connect_all(rx_chain type); + void connect_all(rx_chain type, enum file_formats fmt); + void setup_source(enum file_formats fmt); + status connect_iq_recorder(); private: bool d_running; /*!< Whether receiver is running or not. */ @@ -250,6 +273,8 @@ class receiver bool d_iq_rev; /*!< Whether I/Q is reversed or not. */ bool d_dc_cancel; /*!< Enable automatic DC removal. */ bool d_iq_balance; /*!< Enable automatic IQ balance. */ + enum file_formats d_iq_fmt; + enum file_formats d_last_format; std::string input_devstr; /*!< Current input device string. */ std::string output_devstr; /*!< Current output device string. */ @@ -276,6 +301,24 @@ class receiver gr::blocks::multiply_const_ff::sptr wav_gain1; /*!< WAV file gain block. */ gr::blocks::file_sink::sptr iq_sink; /*!< I/Q file sink. */ + //Format converters to/from signed integer + any_to_any>::sptr to_s32lc; + any_to_any, gr_complex>::sptr from_s32lc; + any_to_any>::sptr to_s16lc; + any_to_any, gr_complex>::sptr from_s16lc; + any_to_any>::sptr to_s8c; + any_to_any, gr_complex>::sptr from_s8c; + + //Format converters to/from unsigned integer + any_to_any>::sptr to_s32luc; + any_to_any, gr_complex>::sptr from_s32luc; + any_to_any>::sptr to_s16luc; + any_to_any, gr_complex>::sptr from_s16luc; + any_to_any>::sptr to_s8uc; + any_to_any, gr_complex>::sptr from_s8uc; + + gr::blocks::throttle::sptr input_throttle; + gr::blocks::file_source::sptr input_file; gr::blocks::wavfile_sink::sptr wav_sink; /*!< WAV file sink for recording. */ gr::blocks::wavfile_source::sptr wav_src; /*!< WAV file source for playback. */ diff --git a/src/applications/gqrx/remote_control.cpp b/src/applications/gqrx/remote_control.cpp index d76b313f2b..b111cacf41 100644 --- a/src/applications/gqrx/remote_control.cpp +++ b/src/applications/gqrx/remote_control.cpp @@ -393,7 +393,7 @@ void RemoteControl::stopAudioRecorder() } /*! \brief Start IQ recorder (from another window). */ -void RemoteControl::startIqRecorder(QString unused1, QString unused2) +void RemoteControl::startIqRecorder() { iq_recorder_status = true; } diff --git a/src/applications/gqrx/remote_control.h b/src/applications/gqrx/remote_control.h index 4339fb5696..73b5f4ce30 100644 --- a/src/applications/gqrx/remote_control.h +++ b/src/applications/gqrx/remote_control.h @@ -97,7 +97,7 @@ public slots: void setAudioMuted(bool muted); void startAudioRecorder(QString unused); void stopAudioRecorder(); - void startIqRecorder(QString unused1, QString unused2); + void startIqRecorder(); void stopIqRecorder(); bool setGain(QString name, double gain); void setRDSstatus(bool enabled); diff --git a/src/dsp/CMakeLists.txt b/src/dsp/CMakeLists.txt index 3f8e38e808..bff9455c1f 100644 --- a/src/dsp/CMakeLists.txt +++ b/src/dsp/CMakeLists.txt @@ -49,4 +49,5 @@ add_source_files(SRCS_LIST sniffer_f.h stereo_demod.cpp stereo_demod.h + format_converter.h ) diff --git a/src/dsp/format_converter.h b/src/dsp/format_converter.h new file mode 100644 index 0000000000..5c986b811b --- /dev/null +++ b/src/dsp/format_converter.h @@ -0,0 +1,324 @@ +/* -*- c++ -*- */ +/* + * Gqrx SDR: Software defined radio receiver powered by GNU Radio and Qt + * https://gqrx.dk/ + * + * Copyright 2021 vladisslav2011@gmail.com. + * + * Gqrx 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; either version 3, or (at your option) + * any later version. + * + * Gqrx 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with Gqrx; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ +#ifndef INCLUDED_FORMAT_CONVERTER_H +#define INCLUDED_FORMAT_CONVERTER_H + +#include +#include +#include +#include + +namespace dispatcher +{ + template struct tag{}; +} + + /*! + * \brief Convert stream of one format to a stream of another format. + * \ingroup type_converters_blk + * + * \details + * The output stream contains chars with twice as many output + * items as input items. For every complex input item, we produce + * two output chars that contain the real part and imaginary part + * converted to chars: + * + * \li output[0][n] = static_cast(input[0][m].real()); + * \li output[0][n+1] = static_cast(input[0][m].imag()); + */ +template class BLOCKS_API any_to_any : virtual public gr::sync_interpolator +{ +public: +#if GNURADIO_VERSION < 0x030900 + typedef boost::shared_ptr> sptr; +#else + typedef std::shared_ptr> sptr; +#endif + + + /*! + * Build a any-to-any. + */ + static sptr make(const double scale) + { + return gnuradio::get_initial_sptr(new any_to_any(scale)); + } + + static sptr make() + { + return make(dispatcher::tag()); + } + + static sptr make(dispatcher::tag>) + { + return gnuradio::get_initial_sptr(new any_to_any(1.0)); + } + + static sptr make(dispatcher::tag>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT32_MIN))); + } + + static sptr make(dispatcher::tag, gr_complex>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT32_MIN))); + } + + static sptr make(dispatcher::tag>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT32_MIN))); + } + + static sptr make(dispatcher::tag, gr_complex>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT32_MIN))); + } + + static sptr make(dispatcher::tag>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT16_MIN))); + } + + static sptr make(dispatcher::tag, gr_complex>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT16_MIN))); + } + + static sptr make(dispatcher::tag>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT16_MIN))); + } + + static sptr make(dispatcher::tag, gr_complex>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT16_MIN))); + } + + static sptr make(dispatcher::tag>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT8_MIN))); + } + + static sptr make(dispatcher::tag, gr_complex>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT8_MIN))); + } + + static sptr make(dispatcher::tag>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT8_MIN))); + } + + static sptr make(dispatcher::tag, gr_complex>>) + { + return gnuradio::get_initial_sptr(new any_to_any(-float(INT8_MIN))); + } + + any_to_any(const double scale):sync_interpolator("any_to_any", + gr::io_signature::make (1, 1, sizeof(T_IN)), + gr::io_signature::make (1, 1, sizeof(T_OUT)), 1) + { + d_scale = scale; + d_scale_i = 1.0 / scale; + } + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items) + { + return work(noutput_items, input_items, output_items, dispatcher::tag()); + } + +private: + float d_scale; + float d_scale_i; + + template int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag>) + { + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag>) + { + const T_IN *in = (const T_IN *) input_items[0]; + T_OUT *out = (T_OUT *) output_items[0]; + + memcpy(out, in, noutput_items * 2 * sizeof(gr_complex)); + return noutput_items; + } + + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag>>) + { + const float *in = (const float *) input_items[0]; + int32_t *out = (int32_t *) output_items[0]; + + volk_32f_s32f_convert_32i(out, in, d_scale, noutput_items * 2); + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag, gr_complex>>) + { + const int32_t *in = (const int32_t *) input_items[0]; + float *out = (float *) output_items[0]; + + volk_32i_s32f_convert_32f(out, in, d_scale, noutput_items * 2); + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag>>) + { + const float *in = (const float *) input_items[0]; + int32_t *out = (int32_t *) output_items[0]; + uint32_t *out_u = (uint32_t *) output_items[0]; + + volk_32f_s32f_convert_32i(out, in, d_scale, noutput_items * 2); + for(int k = 0;k < noutput_items;k++) + { + *(out_u++) += uint32_t(INT32_MAX) + 1; + *(out_u++) += uint32_t(INT32_MAX) + 1; + } + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag, gr_complex>>) + { + const uint32_t *in = (const uint32_t *) input_items[0]; + float *out = (float *) output_items[0]; + + for(int k = 0;k < noutput_items;k++) + { + *(out++) = (float(*(in++))+float(INT32_MIN)) * d_scale_i; + *(out++) = (float(*(in++))+float(INT32_MIN)) * d_scale_i; + } + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag>>) + { + const float *in = (const float *) input_items[0]; + int16_t *out = (int16_t *) output_items[0]; + + volk_32f_s32f_convert_16i(out, in, d_scale, noutput_items * 2); + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag, gr_complex>>) + { + const int16_t *in = (const int16_t *) input_items[0]; + float *out = (float *) output_items[0]; + + volk_16i_s32f_convert_32f(out, in, d_scale, noutput_items * 2); + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag>>) + { + const float *in = (const float *) input_items[0]; + uint16_t *out_u = (uint16_t *) output_items[0]; + int16_t *out = (int16_t *) output_items[0]; + + volk_32f_s32f_convert_16i(out, in, d_scale, noutput_items * 2); + for(int k = 0;k < noutput_items;k++) + { + *(out_u++) += INT16_MAX + 1; + *(out_u++) += INT16_MAX + 1; + } + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag, gr_complex>>) + { + const uint16_t *in = (const uint16_t *) input_items[0]; + float *out = (float *) output_items[0]; + + for(int k = 0;k < noutput_items;k++) + { + *(out++) = (float(*(in++)) + float(INT16_MIN)) * d_scale_i; + *(out++) = (float(*(in++)) + float(INT16_MIN)) * d_scale_i; + } + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag>>) + { + const float *in = (const float *) input_items[0]; + int8_t *out = (int8_t *) output_items[0]; + + volk_32f_s32f_convert_8i(out, in, d_scale, noutput_items * 2); + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag, gr_complex>>) + { + const int8_t *in = (const int8_t *) input_items[0]; + float *out = (float *) output_items[0]; + + volk_8i_s32f_convert_32f(out, in, d_scale, noutput_items * 2); + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag>>) + { + const float *in = (const float *) input_items[0]; + uint8_t *out_u = (uint8_t *) output_items[0]; + int8_t *out = (int8_t *) output_items[0]; + + volk_32f_s32f_convert_8i(out, in, d_scale, noutput_items * 2); + for(int k = 0;k < noutput_items;k++) + { + *(out_u++) += INT8_MAX + 1; + *(out_u++) += INT8_MAX + 1; + } + return noutput_items; + } + + int work(int noutput_items, gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items, dispatcher::tag, gr_complex>>) + { + const uint8_t *in = (const uint8_t *) input_items[0]; + float *out = (float *) output_items[0]; + for(int k = 0;k < noutput_items;k++) + { + *(out++) = (float(*(in++)) + float(INT8_MIN)) * d_scale_i; + *(out++) = (float(*(in++)) + float(INT8_MIN)) * d_scale_i; + } + return noutput_items; + } +}; + + + + +#endif /* INCLUDED_FORMAT_CONVERTER_H */ diff --git a/src/qtgui/iq_tool.cpp b/src/qtgui/iq_tool.cpp index 363cc1a278..8cc0e783d0 100644 --- a/src/qtgui/iq_tool.cpp +++ b/src/qtgui/iq_tool.cpp @@ -45,12 +45,13 @@ CIqTool::CIqTool(QWidget *parent) : is_recording = false; is_playing = false; bytes_per_sample = 8; + rec_bytes_per_sample = 8; + fmt = receiver::FILE_FORMAT_CF; + rec_fmt = receiver::FILE_FORMAT_CF; sample_rate = 192000; rec_len = 0; center_freq = 1e8; - //ui->recDirEdit->setText(QDir::currentPath()); - recdir = new QDir(QDir::homePath(), "*.raw"); recdir->setNameFilters(recdir->nameFilters() << "*.sigmf-data"); @@ -59,6 +60,16 @@ CIqTool::CIqTool(QWidget *parent) : timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(timeoutFunction())); + connect(ui->formatCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(on_formatCombo_currentIndexChanged(int))); + ui->formatCombo->addItem("gr_complex cf", receiver::FILE_FORMAT_CF); + ui->formatCombo->addItem("int 32", receiver::FILE_FORMAT_CS32L); + ui->formatCombo->addItem("short 16", receiver::FILE_FORMAT_CS16L); + ui->formatCombo->addItem("char 8", receiver::FILE_FORMAT_CS8); + ui->formatCombo->addItem("uint 32", receiver::FILE_FORMAT_CS32LU); + ui->formatCombo->addItem("ushort 16", receiver::FILE_FORMAT_CS16LU); + ui->formatCombo->addItem("uchar 8", receiver::FILE_FORMAT_CS8U); + ui->formatCombo->addItem("SIGMF", receiver::FILE_FORMAT_SIGMF); + } CIqTool::~CIqTool() @@ -113,6 +124,7 @@ void CIqTool::switchControlsState(bool recording, bool playback) ui->listWidget->setEnabled(!(recording || playback)); ui->recDirEdit->setEnabled(!(recording || playback)); ui->recDirButton->setEnabled(!(recording || playback)); + ui->formatCombo->setEnabled(!(recording || playback)); } /*! \brief Start/stop playback */ @@ -144,7 +156,7 @@ void CIqTool::on_playButton_clicked(bool checked) switchControlsState(false, true); emit startPlayback(recdir->absoluteFilePath(current_file), - (float)sample_rate, center_freq); + (float)sample_rate, center_freq, fmt); } } else @@ -185,7 +197,7 @@ void CIqTool::on_recButton_clicked(bool checked) if (checked) { switchControlsState(true, false); - emit startRecording(recdir->path(), ui->formatCombo->currentText()); + emit startRecording(recdir->path(), rec_fmt); refreshDir(); ui->listWidget->setCurrentRow(ui->listWidget->count()-1); @@ -274,12 +286,7 @@ void CIqTool::saveSettings(QSettings *settings) settings->setValue("baseband/rec_dir", dir); else settings->remove("baseband/rec_dir"); - - QString format = ui->formatCombo->currentText(); - if (format != "Raw") - settings->setValue("baseband/rec_format", format); - else - settings->remove("baseband/rec_format"); + settings->setValue("baseband/rec_fmt", rec_fmt); } void CIqTool::readSettings(QSettings *settings) @@ -290,13 +297,21 @@ void CIqTool::readSettings(QSettings *settings) // Location of baseband recordings QString dir = settings->value("baseband/rec_dir", QDir::homePath()).toString(); ui->recDirEdit->setText(dir); - - // Format of baseband recordings - QString format = settings->value("baseband/rec_format", "Raw").toString(); - ui->formatCombo->setCurrentText(format); + int found = ui->formatCombo->findData(settings->value("baseband/rec_fmt", receiver::FILE_FORMAT_CF)); + if(found == -1) + { + rec_fmt = receiver::FILE_FORMAT_CF; + rec_bytes_per_sample = 8; + } + else + { + rec_bytes_per_sample = receiver::sample_size_from_format((enum receiver::file_formats)ui->formatCombo->itemData(found).toInt()); + ui->formatCombo->setCurrentIndex(found); + } } + /*! \brief Slot called when the recordings directory has changed either * because of user input or programmatically. */ @@ -347,6 +362,12 @@ void CIqTool::timeoutFunction(void) refreshTimeWidgets(); } +void CIqTool::on_formatCombo_currentIndexChanged(int index) +{ + rec_fmt = (enum receiver::file_formats)ui->formatCombo->currentData().toInt(); + rec_bytes_per_sample = receiver::sample_size_from_format(rec_fmt); +} + /*! \brief Refresh list of files in current working directory. */ void CIqTool::refreshDir() { @@ -406,7 +427,6 @@ void CIqTool::refreshTimeWidgets(void) .arg(ls, 2, 10, QChar('0'))); } - /*! \brief Extract sample rate and offset frequency from file name */ void CIqTool::parseFileName(const QString &filename) { @@ -415,6 +435,7 @@ void CIqTool::parseFileName(const QString &filename) bool center_ok; qint64 center; + QString fmt_str = ""; QStringList list = filename.split('_'); if (list.size() < 5) @@ -424,8 +445,49 @@ void CIqTool::parseFileName(const QString &filename) sr = list.at(4).toLongLong(&sr_ok); center = list.at(3).toLongLong(¢er_ok); + fmt_str = list.at(5); + list = fmt_str.split('.'); + fmt_str = list.at(0); + if (sr_ok) sample_rate = sr; if (center_ok) center_freq = center; + if(fmt_str.compare("fc") == 0) + { + bytes_per_sample = 8; + fmt = receiver::FILE_FORMAT_CF; + if(list.at(1).compare("sigmf-data") == 0) + fmt = receiver::FILE_FORMAT_SIGMF; + } + if(fmt_str.compare("32") == 0) + { + bytes_per_sample = 8; + fmt = receiver::FILE_FORMAT_CS32L; + } + if(fmt_str.compare("16") == 0) + { + bytes_per_sample = 4; + fmt = receiver::FILE_FORMAT_CS16L; + } + if(fmt_str.compare("8") == 0) + { + bytes_per_sample = 2; + fmt = receiver::FILE_FORMAT_CS8; + } + if(fmt_str.compare("32u") == 0) + { + bytes_per_sample = 8; + fmt = receiver::FILE_FORMAT_CS32LU; + } + if(fmt_str.compare("16u") == 0) + { + bytes_per_sample = 4; + fmt = receiver::FILE_FORMAT_CS16LU; + } + if(fmt_str.compare("8u") == 0) + { + bytes_per_sample = 2; + fmt = receiver::FILE_FORMAT_CS8U; + } } diff --git a/src/qtgui/iq_tool.h b/src/qtgui/iq_tool.h index 54e69efa82..26e27d3d7c 100644 --- a/src/qtgui/iq_tool.h +++ b/src/qtgui/iq_tool.h @@ -31,6 +31,8 @@ #include #include #include +#include "applications/gqrx/receiver.h" + namespace Ui { class CIqTool; @@ -62,9 +64,9 @@ class CIqTool : public QDialog void readSettings(QSettings *settings); signals: - void startRecording(const QString recdir, const QString format); + void startRecording(const QString recdir, enum receiver::file_formats fmt); void stopRecording(); - void startPlayback(const QString filename, float samprate, qint64 center_freq); + void startPlayback(const QString filename, float samprate, qint64 center_freq, enum receiver::file_formats fmt); void stopPlayback(); void seek(qint64 seek_pos); @@ -82,6 +84,7 @@ private slots: void on_slider_valueChanged(int value); void on_listWidget_currentTextChanged(const QString ¤tText); void timeoutFunction(void); + void on_formatCombo_currentIndexChanged(int index); private: void refreshDir(void); @@ -89,7 +92,6 @@ private slots: void parseFileName(const QString &filename); void switchControlsState(bool recording, bool playback); - private: Ui::CIqTool *ui; @@ -102,6 +104,9 @@ private slots: bool is_recording; bool is_playing; int bytes_per_sample; /*!< Bytes per sample (fc = 4) */ + int rec_bytes_per_sample; /*!< Bytes per sample for recording */ + enum receiver::file_formats fmt; + enum receiver::file_formats rec_fmt; int sample_rate; /*!< Current sample rate. */ qint64 center_freq; /*!< Center frequency. */ int rec_len; /*!< Length of a recording in seconds */ diff --git a/src/qtgui/iq_tool.ui b/src/qtgui/iq_tool.ui index 3485db5bc9..ed5773c200 100644 --- a/src/qtgui/iq_tool.ui +++ b/src/qtgui/iq_tool.ui @@ -7,7 +7,7 @@ 0 0 482 - 327 + 320 @@ -32,30 +32,6 @@ - - - - Format: - - - - - - - File format - - - - Raw - - - - - SigMF - - - - @@ -114,6 +90,12 @@ true + + + 0 + 0 + + 32 @@ -184,7 +166,7 @@ 40 - 20 + 10 @@ -207,7 +189,7 @@ 40 - 20 + 10 @@ -233,6 +215,54 @@ + + + + + + + 0 + 0 + + + + + 32 + 0 + + + + Format: + + + + + + + true + + + + 0 + 0 + + + + + 32 + 24 + + + + false + + + true + + + + + From 68f23d28b486aaf130bb7672ef68fce8ca598949 Mon Sep 17 00:00:00 2001 From: Vladisslav P Date: Fri, 18 Feb 2022 22:26:46 +0300 Subject: [PATCH 4/6] iq_tool: implement repeated playback option --- src/applications/gqrx/mainwindow.cpp | 8 +++++--- src/applications/gqrx/mainwindow.h | 3 ++- src/applications/gqrx/receiver.cpp | 5 +++-- src/applications/gqrx/receiver.h | 3 ++- src/qtgui/iq_tool.cpp | 4 +++- src/qtgui/iq_tool.h | 3 ++- src/qtgui/iq_tool.ui | 10 ++++++++++ 7 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/applications/gqrx/mainwindow.cpp b/src/applications/gqrx/mainwindow.cpp index 5267f7612a..450706cb2b 100644 --- a/src/applications/gqrx/mainwindow.cpp +++ b/src/applications/gqrx/mainwindow.cpp @@ -320,7 +320,7 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats)), remote, SLOT(startIqRecorder())); connect(iq_tool, SIGNAL(stopRecording()), this, SLOT(stopIqRecording())); connect(iq_tool, SIGNAL(stopRecording()), remote, SLOT(stopIqRecorder())); - connect(iq_tool, SIGNAL(startPlayback(QString, float, qint64, enum receiver::file_formats)), this, SLOT(startIqPlayback(QString, float, qint64, enum receiver::file_formats))); + connect(iq_tool, SIGNAL(startPlayback(QString, float, qint64, enum receiver::file_formats, bool)), this, SLOT(startIqPlayback(QString, float, qint64, enum receiver::file_formats, bool))); connect(iq_tool, SIGNAL(stopPlayback()), this, SLOT(stopIqPlayback())); connect(iq_tool, SIGNAL(seek(qint64)), this,SLOT(seekIqFile(qint64))); @@ -1782,7 +1782,9 @@ void MainWindow::stopIqRecording() ui->actionLoadSettings->setDisabled(false); } -void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 center_freq, enum receiver::file_formats fmt) +void MainWindow::startIqPlayback(const QString& filename, float samprate, + qint64 center_freq, + enum receiver::file_formats fmt, bool repeat) { if (ui->actionDSP->isChecked()) { @@ -1803,7 +1805,7 @@ void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 rx->set_input_device(devstr.toStdString()); updateHWFrequencyRange(false); - rx->set_input_file(filename.toStdString(), samprate, fmt); + rx->set_input_file(filename.toStdString(), samprate, fmt, repeat); // sample rate auto actual_rate = rx->set_input_rate((double)samprate); diff --git a/src/applications/gqrx/mainwindow.h b/src/applications/gqrx/mainwindow.h index 67ae771641..544591c4bc 100644 --- a/src/applications/gqrx/mainwindow.h +++ b/src/applications/gqrx/mainwindow.h @@ -199,7 +199,8 @@ private slots: /* I/Q playback and recording*/ void startIqRecording(const QString& recdir, enum receiver::file_formats fmt); void stopIqRecording(); - void startIqPlayback(const QString& filename, float samprate, qint64 center_freq, enum receiver::file_formats fmt); + void startIqPlayback(const QString& filename, float samprate, qint64 center_freq, + enum receiver::file_formats fmt, bool repeat); void stopIqPlayback(); void seekIqFile(qint64 seek_pos); diff --git a/src/applications/gqrx/receiver.cpp b/src/applications/gqrx/receiver.cpp index 0b06884f9e..abefbb0af0 100644 --- a/src/applications/gqrx/receiver.cpp +++ b/src/applications/gqrx/receiver.cpp @@ -263,12 +263,13 @@ void receiver::set_input_device(const std::string device) * @param sample_rate * @param fmt */ -void receiver::set_input_file(const std::string name, const int sample_rate, const enum file_formats fmt) +void receiver::set_input_file(const std::string name, const int sample_rate, + const enum file_formats fmt, bool repeat) { std::string error = ""; size_t sample_size = sample_size_from_format(fmt); - input_file = gr::blocks::file_source::make(sample_size, name.c_str(), false); + input_file = gr::blocks::file_source::make(sample_size, name.c_str(), repeat); if (d_running) { diff --git a/src/applications/gqrx/receiver.h b/src/applications/gqrx/receiver.h index 9060588dbb..5f37723cd1 100644 --- a/src/applications/gqrx/receiver.h +++ b/src/applications/gqrx/receiver.h @@ -132,7 +132,8 @@ class receiver void stop(); void set_input_device(const std::string device); void set_output_device(const std::string device); - void set_input_file(const std::string name, const int sample_rate, const enum file_formats fmt); + void set_input_file(const std::string name, const int sample_rate, + const enum file_formats fmt, bool repeat); std::vector get_antennas(void) const; void set_antenna(const std::string &antenna); diff --git a/src/qtgui/iq_tool.cpp b/src/qtgui/iq_tool.cpp index 8cc0e783d0..da078d1e6b 100644 --- a/src/qtgui/iq_tool.cpp +++ b/src/qtgui/iq_tool.cpp @@ -125,6 +125,7 @@ void CIqTool::switchControlsState(bool recording, bool playback) ui->recDirEdit->setEnabled(!(recording || playback)); ui->recDirButton->setEnabled(!(recording || playback)); ui->formatCombo->setEnabled(!(recording || playback)); + ui->repeat->setEnabled(!(recording || playback)); } /*! \brief Start/stop playback */ @@ -156,7 +157,8 @@ void CIqTool::on_playButton_clicked(bool checked) switchControlsState(false, true); emit startPlayback(recdir->absoluteFilePath(current_file), - (float)sample_rate, center_freq, fmt); + (float)sample_rate, center_freq, fmt, + ui->repeat->checkState() == Qt::Checked); } } else diff --git a/src/qtgui/iq_tool.h b/src/qtgui/iq_tool.h index 26e27d3d7c..91820a734b 100644 --- a/src/qtgui/iq_tool.h +++ b/src/qtgui/iq_tool.h @@ -66,7 +66,8 @@ class CIqTool : public QDialog signals: void startRecording(const QString recdir, enum receiver::file_formats fmt); void stopRecording(); - void startPlayback(const QString filename, float samprate, qint64 center_freq, enum receiver::file_formats fmt); + void startPlayback(const QString filename, float samprate, qint64 center_freq, + enum receiver::file_formats fmt, bool repeat); void stopPlayback(); void seek(qint64 seek_pos); diff --git a/src/qtgui/iq_tool.ui b/src/qtgui/iq_tool.ui index ed5773c200..063a0bea5c 100644 --- a/src/qtgui/iq_tool.ui +++ b/src/qtgui/iq_tool.ui @@ -158,6 +158,16 @@ + + + + + + + Repeat + + + From d51b5670fc786dda776f89b0ba564045d83c6281 Mon Sep 17 00:00:00 2001 From: Vladisslav P Date: Fri, 18 Feb 2022 22:09:36 +0300 Subject: [PATCH 5/6] iq_tool: implement buffered file writer To prevent buffer droppouts on HDD housekeeping/background activity --- CMakeLists.txt | 6 + src/applications/gqrx/mainwindow.cpp | 24 +- src/applications/gqrx/mainwindow.h | 2 +- src/applications/gqrx/receiver.cpp | 15 +- src/applications/gqrx/receiver.h | 16 +- src/interfaces/CMakeLists.txt | 2 + src/interfaces/file_sink.cpp | 354 +++++++++++++++++++++++++++ src/interfaces/file_sink.h | 123 ++++++++++ src/qtgui/iq_tool.cpp | 51 +++- src/qtgui/iq_tool.h | 5 +- src/qtgui/iq_tool.ui | 67 +++++ 11 files changed, 652 insertions(+), 13 deletions(-) create mode 100644 src/interfaces/file_sink.cpp create mode 100644 src/interfaces/file_sink.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eefa906dc1..0d2f41b061 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,12 @@ add_definitions(-D${BUILDTYPE}) # We have some custom .cmake scripts not in the official distribution. set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules) +set(CMAKE_THREAD_LIBS_INIT "-lpthread") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") +set(CMAKE_HAVE_THREADS_LIBRARY 1) +set(CMAKE_USE_WIN32_THREADS_INIT 0) +set(CMAKE_USE_PTHREADS_INIT 1) +set(THREADS_PREFER_PTHREAD_FLAG ON) # Add valgrind build options if necessary if(${CMAKE_BUILD_TYPE} MATCHES "Valgrind") diff --git a/src/applications/gqrx/mainwindow.cpp b/src/applications/gqrx/mainwindow.cpp index 450706cb2b..2d56524b8e 100644 --- a/src/applications/gqrx/mainwindow.cpp +++ b/src/applications/gqrx/mainwindow.cpp @@ -316,8 +316,8 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) connect(&DXCSpots::Get(), SIGNAL(dxcSpotsUpdated()), this, SLOT(updateClusterSpots())); // I/Q playback - connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats)), this, SLOT(startIqRecording(QString, enum receiver::file_formats))); - connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats)), remote, SLOT(startIqRecorder())); + connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats, int)), this, SLOT(startIqRecording(QString, enum receiver::file_formats, int))); + connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats, int)), remote, SLOT(startIqRecorder())); connect(iq_tool, SIGNAL(stopRecording()), this, SLOT(stopIqRecording())); connect(iq_tool, SIGNAL(stopRecording()), remote, SLOT(stopIqRecorder())); connect(iq_tool, SIGNAL(startPlayback(QString, float, qint64, enum receiver::file_formats, bool)), this, SLOT(startIqPlayback(QString, float, qint64, enum receiver::file_formats, bool))); @@ -1501,10 +1501,26 @@ double MainWindow::setSqlLevelAuto() void MainWindow::meterTimeout() { float level; + struct receiver::iq_recorder_stats iq_stats; level = rx->get_signal_pwr(); ui->sMeter->setLevel(level); remote->setSignalLevel(level); + // As it looks like this timer is always active (when the DSP is running), + // check iq recorder state here too + rx->get_iq_recorder_stats(iq_stats); + if(iq_stats.active) + { + if(iq_stats.failed) + { + //stop the recorder + iq_tool->updateStats(iq_stats.failed, iq_stats.buffers_used, iq_stats.file_size); + iq_tool->cancelRecording(); + }else{ + //update status + iq_tool->updateStats(iq_stats.failed, iq_stats.buffers_used, iq_stats.file_size); + } + } } /** Baseband FFT plot timeout. */ @@ -1668,7 +1684,7 @@ void MainWindow::stopAudioStreaming() } /** Start I/Q recording. */ -void MainWindow::startIqRecording(const QString& recdir, receiver::file_formats fmt) +void MainWindow::startIqRecording(const QString& recdir, receiver::file_formats fmt, int buffers_max) { // generate file name using date, time, rf freq in kHz and BW in Hz // gqrx_iq_yyyymmdd_hhmmss_freq_bw_fc.raw @@ -1744,7 +1760,7 @@ void MainWindow::startIqRecording(const QString& recdir, receiver::file_formats ui->actionIoConfig->setDisabled(true); ui->actionLoadSettings->setDisabled(true); // start recorder; fails if recording already in progress - if (!ok || rx->start_iq_recording(lastRec.toStdString(), fmt)) + if (!ok || rx->start_iq_recording(lastRec.toStdString(), fmt, buffers_max)) { // remove metadata file if we managed to open it if (sigmf && metaFile.isOpen()) diff --git a/src/applications/gqrx/mainwindow.h b/src/applications/gqrx/mainwindow.h index 544591c4bc..144279a199 100644 --- a/src/applications/gqrx/mainwindow.h +++ b/src/applications/gqrx/mainwindow.h @@ -197,7 +197,7 @@ private slots: void stopAudioStreaming(); /* I/Q playback and recording*/ - void startIqRecording(const QString& recdir, enum receiver::file_formats fmt); + void startIqRecording(const QString& recdir, enum receiver::file_formats fmt, int buffers_max); void stopIqRecording(); void startIqPlayback(const QString& filename, float samprate, qint64 center_freq, enum receiver::file_formats fmt, bool repeat); diff --git a/src/applications/gqrx/receiver.cpp b/src/applications/gqrx/receiver.cpp index abefbb0af0..428855f2d0 100644 --- a/src/applications/gqrx/receiver.cpp +++ b/src/applications/gqrx/receiver.cpp @@ -1411,7 +1411,7 @@ receiver::status receiver::connect_iq_recorder() * @param filename The filename where to record. + * @param bytes_per_sample A hint to choose correct sample format. */ -receiver::status receiver::start_iq_recording(const std::string filename, const enum file_formats fmt) +receiver::status receiver::start_iq_recording(const std::string filename, const enum file_formats fmt, int buffers_max) { int sink_bytes_per_sample = sample_size_from_format(fmt); @@ -1422,7 +1422,7 @@ receiver::status receiver::start_iq_recording(const std::string filename, const try { - iq_sink = gr::blocks::file_sink::make(sink_bytes_per_sample, filename.c_str(), true); + iq_sink = file_sink::make(sink_bytes_per_sample, filename.c_str(), d_input_rate, true, buffers_max); } catch (std::runtime_error &e) { @@ -1508,6 +1508,17 @@ receiver::status receiver::seek_iq_file(long pos) return status; } +void receiver::get_iq_recorder_stats(struct iq_recorder_stats &stats) +{ + stats.active = d_recording_iq; + if(d_recording_iq && iq_sink) + { + stats.failed = iq_sink->get_failed(); + stats.buffers_used = iq_sink->get_buffer_usage(); + stats.file_size = iq_sink->get_written(); + } +} + /** * @brief Start data sniffer. * @param buffsize The buffer that should be used in the sniffer. diff --git a/src/applications/gqrx/receiver.h b/src/applications/gqrx/receiver.h index 5f37723cd1..2a510ac47d 100644 --- a/src/applications/gqrx/receiver.h +++ b/src/applications/gqrx/receiver.h @@ -48,6 +48,7 @@ #include "dsp/resampler_xx.h" #include "dsp/format_converter.h" #include "interfaces/udp_sink_f.h" +#include "interfaces/file_sink.h" #include "receivers/receiver_base.h" #ifdef WITH_PULSEAUDIO @@ -121,6 +122,14 @@ class receiver FILE_FORMAT_SIGMF, }; + struct iq_recorder_stats + { + bool active; + bool failed; + int buffers_used; + size_t file_size; + }; + static const unsigned int DEFAULT_FFT_SIZE = 8192; receiver(const std::string input_device="", @@ -227,10 +236,11 @@ class receiver status stop_udp_streaming(); /* I/Q recording and playback */ - status start_iq_recording(const std::string filename, const enum file_formats fmt); + status start_iq_recording(const std::string filename, const enum file_formats fmt, int buffers_max); status stop_iq_recording(); status seek_iq_file(long pos); - bool is_playing_iq(void) const { return input_devstr.substr(0, 5).compare("file=") == 0; } + void get_iq_recorder_stats(struct iq_recorder_stats &stats); + bool is_playing_iq() const { return !(d_iq_fmt == FILE_FORMAT_NONE); } /* sample sniffer */ status start_sniffer(unsigned int samplrate, int buffsize); @@ -301,7 +311,7 @@ class receiver gr::blocks::multiply_const_ff::sptr wav_gain0; /*!< WAV file gain block. */ gr::blocks::multiply_const_ff::sptr wav_gain1; /*!< WAV file gain block. */ - gr::blocks::file_sink::sptr iq_sink; /*!< I/Q file sink. */ + file_sink::sptr iq_sink; /*!< I/Q file sink. */ //Format converters to/from signed integer any_to_any>::sptr to_s32lc; any_to_any, gr_complex>::sptr from_s32lc; diff --git a/src/interfaces/CMakeLists.txt b/src/interfaces/CMakeLists.txt index 8038698b69..9510e92d0f 100644 --- a/src/interfaces/CMakeLists.txt +++ b/src/interfaces/CMakeLists.txt @@ -3,4 +3,6 @@ add_source_files(SRCS_LIST udp_sink_f.cpp udp_sink_f.h + file_sink.cpp + file_sink.h ) diff --git a/src/interfaces/file_sink.cpp b/src/interfaces/file_sink.cpp new file mode 100644 index 0000000000..c57e082989 --- /dev/null +++ b/src/interfaces/file_sink.cpp @@ -0,0 +1,354 @@ +/* -*- c++ -*- */ +/* + * Gqrx SDR: Software defined radio receiver powered by GNU Radio and Qt + * https://gqrx.dk/ + * + * Copyright 2021 vladisslav2011@gmail.com. + * + * Gqrx 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; either version 3, or (at your option) + * any later version. + * + * Gqrx 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with Gqrx; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// win32 (mingw/msvc) specific +#ifdef HAVE_IO_H +#include +#endif +#ifdef O_BINARY +#define OUR_O_BINARY O_BINARY +#else +#define OUR_O_BINARY 0 +#endif + +// should be handled via configure +#ifdef O_LARGEFILE +#define OUR_O_LARGEFILE O_LARGEFILE +#else +#define OUR_O_LARGEFILE 0 +#endif + +void file_sink::writer() +{ + s_data item; + int written = 0; + char * p; + FILE * old_fp = NULL; + int count = 0; + while (true) + { + std::unique_lock guard(d_mutex); // hold mutex for duration of this block + while (!d_queue.empty()) + { + item = d_queue.front(); + d_queue.pop(); + written = 0; + p = item.data; + while (written < item.len) + { + if(d_updated) + { + old_fp = d_fp; + d_fp = d_new_fp; // install new file pointer + d_new_fp = 0; + d_updated = false; + } + if (d_fp && !d_failed) + { + guard.unlock(); + count = fwrite(p, 1, item.len-written, d_fp); + guard.lock(); + if(count == 0) + { + if(ferror(d_fp)) + { + std::cerr << "file_sink write failed with error " << fileno(d_fp) << std::endl; + d_failed=true; + break; + } + else // is EOF + break; + } + } + else + break; + written += count; + p += count; + } + item.len = 0; + d_free.push(item); + if (old_fp) + { + guard.unlock(); + fclose(old_fp); + guard.lock(); + old_fp = NULL; + } + d_buffers_used--; + d_written += written; + } + if (d_writer_finish) + { + return; + } + else + { + if (d_fp && !d_failed) + { + guard.unlock(); + fflush(d_fp); + guard.lock(); + } + d_writer_ready.notify_one(); + d_writer_trigger.wait(guard); + } + } +} + +file_sink::sptr file_sink::make(size_t itemsize, const char *filename, int sample_rate, bool append, int buffers_max) +{ + return gnuradio::get_initial_sptr + (new file_sink(itemsize, filename, sample_rate, append, buffers_max)); +} + + +file_sink::file_sink(size_t itemsize, const char *filename, int sample_rate, bool append, int buffers_max) + : sync_block("file_sink", + gr::io_signature::make(1, 1, itemsize), + gr::io_signature::make(0, 0, 0)), + d_itemsize(itemsize), + d_fp(0), d_new_fp(0), d_updated(false), d_is_binary(true), + d_append(append), d_writer_finish(false), + d_sd_max(std::max(8192, sample_rate) * itemsize), d_buffers_used(0), d_buffers_max(buffers_max) +{ + if (!open(filename)) + throw std::runtime_error ("can't open file"); + d_writer_thread = new std::thread(std::bind(&file_sink::writer, this)); + d_sd.data = NULL; + d_sd.len = 0; + d_buffers_used = 0; + d_closing = false; + if(d_buffers_max < 2) + d_buffers_max = 2; + for (int k = 0; k < d_buffers_max; k++) + { + d_sd.data = new char [d_sd_max]; + d_sd.size = d_sd_max; + d_sd.len = 0; + memset(d_sd.data, 0x01, d_sd.size); + d_free.push(d_sd); + } + d_sd.data = NULL; +} + +file_sink::~file_sink() +{ + d_closing=true; + if (d_sd.len > 0) + { + d_queue.push(d_sd); + d_sd.len = 0; + d_sd.data = NULL; + } + close(); + d_writer_finish = true; + d_writer_trigger.notify_one(); + d_writer_thread->join(); + delete d_writer_thread; + while (!d_free.empty()) + { + d_sd = d_free.front(); + d_free.pop(); + delete [] d_sd.data; + } + if (d_fp) + { + fclose(d_fp); + d_fp = 0; + } +} + +bool file_sink::open(const char *filename) +{ + + if (d_updated) + return false; + // we use the open system call to get access to the O_LARGEFILE flag. + int fd; + int flags; + if (d_append) + { + flags = O_WRONLY|O_CREAT|O_APPEND|OUR_O_LARGEFILE|OUR_O_BINARY; + } + else + { + flags = O_WRONLY|O_CREAT|O_TRUNC|OUR_O_LARGEFILE|OUR_O_BINARY; + } + if((fd = ::open(filename, flags, 0664)) < 0) + { + perror(filename); + return false; + } + if (d_new_fp) // if we've already got a new one open, close it + { + fclose(d_new_fp); + d_new_fp = 0; + } + + if ((d_new_fp = fdopen (fd, d_is_binary ? "wb" : "w")) == NULL) + { + perror (filename); + ::close(fd); // don't leak file descriptor if fdopen fails. + } + + { + std::unique_lock guard(d_mutex); + d_updated = true; + d_failed = false; + d_closing = false; + d_written = 0; + } + return d_new_fp != 0; +} + +void file_sink::close() +{ + std::unique_lock guard(d_mutex); + //prevent new buffers submission + d_closing = true; + //submit last buffer + if (d_sd.len > 0) + { + d_queue.push(d_sd); + d_sd.data = NULL; + d_sd.len = 0; + } + //wake the thread + d_writer_trigger.notify_one(); + //wait for thread to finish writeng buffers + d_writer_ready.wait(guard); + if (d_new_fp) + { + fclose(d_new_fp); + d_new_fp = 0; + } + d_updated = true; +} + +void file_sink::set_unbuffered(bool unbuffered) +{ + d_unbuffered = unbuffered; +} + + +int file_sink::work(int noutput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items) +{ + char *inbuf = (char*)input_items[0]; + int len_bytes = noutput_items * d_itemsize; + //do not queue more buffers if we are closing the file + std::unique_lock guard(d_mutex); + if (d_closing || d_failed) + return noutput_items; + if (d_sd.data == NULL) + { + if(!d_free.empty()) + { + d_sd = d_free.front(); + d_free.pop(); + } + else + { + d_failed = true; + d_sd.data = NULL; + return noutput_items; + } + } + if (d_sd.len + len_bytes > d_sd_max) + { + d_queue.push(d_sd); + d_writer_trigger.notify_one(); + if (!d_free.empty()) + { + d_sd = d_free.front(); + d_free.pop(); + } + else + { + d_failed = true; + d_sd.data = NULL; + return noutput_items; + } + + if (len_bytes > d_sd_max) + d_sd_max = len_bytes; + if (d_sd.size < d_sd_max) + { + delete [] d_sd.data; + d_sd.data = new char [d_sd_max]; + d_sd.size = d_sd_max; + } + d_sd.len = 0; + ++d_buffers_used; + if (d_buffers_used > d_buffers_max) + { + d_failed = true; + } + } + memcpy(&d_sd.data[d_sd.len], inbuf, len_bytes); + d_sd.len += len_bytes; + return noutput_items; +} + +int file_sink::get_buffer_usage() +{ + return d_buffers_used * 100 / d_buffers_max; +} + +int file_sink::get_buffers_max() +{ + return d_buffers_max; +} + +bool file_sink::get_failed() +{ + return d_failed; +} + +size_t file_sink::get_written() +{ + return d_written; +} + +void file_sink::set_buffers_max(int buffers_max) +{ + //At least one buffer should be present + std::unique_lock guard(d_mutex); + if (buffers_max <= 0) + buffers_max = 1; + d_buffers_max = buffers_max; +} diff --git a/src/interfaces/file_sink.h b/src/interfaces/file_sink.h new file mode 100644 index 0000000000..db0016a4b6 --- /dev/null +++ b/src/interfaces/file_sink.h @@ -0,0 +1,123 @@ +/* -*- c++ -*- */ +/* + * Gqrx SDR: Software defined radio receiver powered by GNU Radio and Qt + * https://gqrx.dk/ + * + * Copyright 2021 vladisslav2011@gmail.com. + * + * Gqrx 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; either version 3, or (at your option) + * any later version. + * + * Gqrx 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with Gqrx; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef GQRX_FILE_SINK_C_H +#define GQRX_FILE_SINK_C_H + +#include +#include +#include +#include +#include + + +/*! + * \brief Write stream to file without blocking. + * \ingroup file_operators_blk + */ +class BLOCKS_API file_sink : virtual public gr::sync_block +{ +public: +#if GNURADIO_VERSION < 0x030900 + typedef boost::shared_ptr sptr; +#else + typedef std::shared_ptr sptr; +#endif + typedef struct { + int len; + int size; + char * data; + } s_data; + + /*! + * \brief Make a file sink. + * \param itemsize size of the input data items. + * \param filename name of the file to open and write output to. + * \param append if true, data is appended to the file instead of + * overwriting the initial content. + */ + static sptr make(size_t itemsize, const char *filename, int sample_rate, bool append=false, int buffers_max=8); + private: + size_t d_itemsize; + + protected: + FILE *d_fp; // current FILE pointer + FILE *d_new_fp; // new FILE pointer + bool d_updated; // is there a new FILE pointer? + bool d_is_binary; + std::mutex d_mutex; + bool d_unbuffered; + bool d_append; + std::queue d_queue; + std::queue d_free; + std::condition_variable d_writer_trigger; + std::condition_variable d_writer_ready; + bool d_writer_finish; + std::thread * d_writer_thread; + s_data d_sd; + int d_sd_max; + int d_buffers_used; + int d_buffers_max; + bool d_failed; + bool d_closing; + size_t d_written; + + private: + void writer(); + + public: + file_sink(size_t itemsize, const char *filename, int sample_rate, bool append, int buffers_max=8); + file_sink() {} + ~file_sink(); + + /*! + * \brief Open filename and begin output to it. + */ + bool open(const char *filename); + + /*! + * \brief Close current output file. + * + * Closes current output file and ignores any output until + * open is called to connect to another file. + */ + void close(); + + /*! + * \brief turn on unbuffered writes for slower outputs + */ + void set_unbuffered(bool unbuffered); + + int get_buffer_usage(); + int get_buffers_max(); + bool get_failed(); + size_t get_written(); + void set_buffers_max(int buffers_max); + + int work(int noutput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items); +}; + + +#endif diff --git a/src/qtgui/iq_tool.cpp b/src/qtgui/iq_tool.cpp index da078d1e6b..324ac38daf 100644 --- a/src/qtgui/iq_tool.cpp +++ b/src/qtgui/iq_tool.cpp @@ -70,6 +70,8 @@ CIqTool::CIqTool(QWidget *parent) : ui->formatCombo->addItem("uchar 8", receiver::FILE_FORMAT_CS8U); ui->formatCombo->addItem("SIGMF", receiver::FILE_FORMAT_SIGMF); + ui->bufferStats->hide(); + ui->sizeStats->hide(); } CIqTool::~CIqTool() @@ -126,6 +128,25 @@ void CIqTool::switchControlsState(bool recording, bool playback) ui->recDirButton->setEnabled(!(recording || playback)); ui->formatCombo->setEnabled(!(recording || playback)); ui->repeat->setEnabled(!(recording || playback)); + ui->buffersSpinBox->setEnabled(!(recording || playback)); + if (recording || playback) + { + ui->formatLabel->hide(); + ui->buffersLabel->hide(); + ui->formatCombo->hide(); + ui->buffersSpinBox->hide(); + ui->bufferStats->show(); + ui->sizeStats->show(); + } + else + { + ui->formatLabel->show(); + ui->buffersLabel->show(); + ui->formatCombo->show(); + ui->buffersSpinBox->show(); + ui->bufferStats->hide(); + ui->sizeStats->hide(); + } } /*! \brief Start/stop playback */ @@ -199,7 +220,7 @@ void CIqTool::on_recButton_clicked(bool checked) if (checked) { switchControlsState(true, false); - emit startRecording(recdir->path(), rec_fmt); + emit startRecording(recdir->path(), rec_fmt, ui->buffersSpinBox->value()); refreshDir(); ui->listWidget->setCurrentRow(ui->listWidget->count()-1); @@ -254,6 +275,30 @@ void CIqTool::cancelRecording() is_recording = false; } +/*! \brief Update GUI ta match current recorder state. + * + * This slot can be periodically activated to show recording progress + */ +void CIqTool::updateStats(bool hasFailed, int buffersUsed, size_t fileSize) +{ + if (hasFailed) + { + QMessageBox msg_box; + msg_box.setIcon(QMessageBox::Critical); + msg_box.setText(tr("IQ recording failed.")); + msg_box.exec(); + } + else + { + if(o_buffersUsed!=buffersUsed) + ui->bufferStats->setText(QString("Buffers: %1/%2").arg(buffersUsed).arg(ui->buffersSpinBox->value())); + if(o_fileSize != fileSize) + ui->sizeStats->setText(QString("Size: %1 bytes").arg(fileSize)); + o_buffersUsed = buffersUsed; + o_fileSize = fileSize; + } + } + /*! \brief Catch window close events. * * This method is called when the user closes the audio options dialog @@ -289,6 +334,7 @@ void CIqTool::saveSettings(QSettings *settings) else settings->remove("baseband/rec_dir"); settings->setValue("baseband/rec_fmt", rec_fmt); + settings->setValue("baseband/rec_buffers", ui->buffersSpinBox->value()); } void CIqTool::readSettings(QSettings *settings) @@ -310,6 +356,7 @@ void CIqTool::readSettings(QSettings *settings) rec_bytes_per_sample = receiver::sample_size_from_format((enum receiver::file_formats)ui->formatCombo->itemData(found).toInt()); ui->formatCombo->setCurrentIndex(found); } + ui->buffersSpinBox->setValue(settings->value("baseband/rec_buffers", 1).toInt()); } @@ -355,7 +402,7 @@ void CIqTool::timeoutFunction(void) if (val < ui->slider->maximum()) { ui->slider->blockSignals(true); - ui->slider->setValue(val+1); + ui->slider->setValue(val + 1); ui->slider->blockSignals(false); refreshTimeWidgets(); } diff --git a/src/qtgui/iq_tool.h b/src/qtgui/iq_tool.h index 91820a734b..a985156109 100644 --- a/src/qtgui/iq_tool.h +++ b/src/qtgui/iq_tool.h @@ -64,7 +64,7 @@ class CIqTool : public QDialog void readSettings(QSettings *settings); signals: - void startRecording(const QString recdir, enum receiver::file_formats fmt); + void startRecording(const QString recdir, enum receiver::file_formats fmt, int buffers_max); void stopRecording(); void startPlayback(const QString filename, float samprate, qint64 center_freq, enum receiver::file_formats fmt, bool repeat); @@ -76,6 +76,7 @@ public slots: void cancelPlayback(); void startIqRecorder(void); /*!< Used if IQ Recorder is started e.g. from remote control */ void stopIqRecorder(void); /*!< Used if IQ Recorder is stopped e.g. from remote control */ + void updateStats(bool hasFailed, int buffersUsed, size_t fileSize); private slots: void on_recDirEdit_textChanged(const QString &text); @@ -111,6 +112,8 @@ private slots: int sample_rate; /*!< Current sample rate. */ qint64 center_freq; /*!< Center frequency. */ int rec_len; /*!< Length of a recording in seconds */ + int o_buffersUsed; + size_t o_fileSize; }; #endif // IQ_TOOL_H diff --git a/src/qtgui/iq_tool.ui b/src/qtgui/iq_tool.ui index 063a0bea5c..53a1c267b6 100644 --- a/src/qtgui/iq_tool.ui +++ b/src/qtgui/iq_tool.ui @@ -227,6 +227,44 @@ + + + + + 0 + 0 + + + + TextLabel + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + TextLabel + + + Qt::AlignCenter + + + @@ -271,6 +309,35 @@ + + + + + 0 + 0 + + + + + 32 + 0 + + + + Buffers: + + + + + + + + 0 + 0 + + + + From 921bf06f8372778e3efc0d21d89f4e30dd973285 Mon Sep 17 00:00:00 2001 From: Vladisslav P Date: Fri, 31 Dec 2021 16:33:33 +0300 Subject: [PATCH 6/6] iq_tool: implement buffered file reader And add IQ player repeat option... --- src/applications/gqrx/mainwindow.cpp | 22 +- src/applications/gqrx/mainwindow.h | 8 +- src/applications/gqrx/receiver.cpp | 21 +- src/applications/gqrx/receiver.h | 21 +- src/interfaces/CMakeLists.txt | 2 + src/interfaces/file_source.cpp | 482 +++++++++++++++++++++++++++ src/interfaces/file_source.h | 112 +++++++ src/qtgui/iq_tool.cpp | 38 ++- src/qtgui/iq_tool.h | 8 +- 9 files changed, 670 insertions(+), 44 deletions(-) create mode 100644 src/interfaces/file_source.cpp create mode 100644 src/interfaces/file_source.h diff --git a/src/applications/gqrx/mainwindow.cpp b/src/applications/gqrx/mainwindow.cpp index 2d56524b8e..d13a9042cf 100644 --- a/src/applications/gqrx/mainwindow.cpp +++ b/src/applications/gqrx/mainwindow.cpp @@ -320,7 +320,8 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats, int)), remote, SLOT(startIqRecorder())); connect(iq_tool, SIGNAL(stopRecording()), this, SLOT(stopIqRecording())); connect(iq_tool, SIGNAL(stopRecording()), remote, SLOT(stopIqRecorder())); - connect(iq_tool, SIGNAL(startPlayback(QString, float, qint64, enum receiver::file_formats, bool)), this, SLOT(startIqPlayback(QString, float, qint64, enum receiver::file_formats, bool))); + connect(iq_tool, SIGNAL(startPlayback(QString, float, qint64, enum receiver::file_formats, int, bool)), + this, SLOT(startIqPlayback(QString, float, qint64, enum receiver::file_formats, int, bool))); connect(iq_tool, SIGNAL(stopPlayback()), this, SLOT(stopIqPlayback())); connect(iq_tool, SIGNAL(seek(qint64)), this,SLOT(seekIqFile(qint64))); @@ -1501,26 +1502,30 @@ double MainWindow::setSqlLevelAuto() void MainWindow::meterTimeout() { float level; - struct receiver::iq_recorder_stats iq_stats; + struct receiver::iq_tool_stats iq_stats; level = rx->get_signal_pwr(); ui->sMeter->setLevel(level); remote->setSignalLevel(level); // As it looks like this timer is always active (when the DSP is running), // check iq recorder state here too - rx->get_iq_recorder_stats(iq_stats); - if(iq_stats.active) + rx->get_iq_tool_stats(iq_stats); + if(iq_stats.recording) { if(iq_stats.failed) { //stop the recorder - iq_tool->updateStats(iq_stats.failed, iq_stats.buffers_used, iq_stats.file_size); + iq_tool->updateStats(iq_stats.failed, iq_stats.buffer_usage, iq_stats.file_pos); iq_tool->cancelRecording(); }else{ //update status - iq_tool->updateStats(iq_stats.failed, iq_stats.buffers_used, iq_stats.file_size); + iq_tool->updateStats(iq_stats.failed, iq_stats.buffer_usage, iq_stats.file_pos); } } + if(iq_stats.playing) + { + iq_tool->updateStats(iq_stats.failed, iq_stats.buffer_usage, iq_stats.file_pos); + } } /** Baseband FFT plot timeout. */ @@ -1800,7 +1805,8 @@ void MainWindow::stopIqRecording() void MainWindow::startIqPlayback(const QString& filename, float samprate, qint64 center_freq, - enum receiver::file_formats fmt, bool repeat) + enum receiver::file_formats fmt, + int buffers_max, bool repeat) { if (ui->actionDSP->isChecked()) { @@ -1821,7 +1827,7 @@ void MainWindow::startIqPlayback(const QString& filename, float samprate, rx->set_input_device(devstr.toStdString()); updateHWFrequencyRange(false); - rx->set_input_file(filename.toStdString(), samprate, fmt, repeat); + rx->set_input_file(filename.toStdString(), samprate, fmt, buffers_max, repeat); // sample rate auto actual_rate = rx->set_input_rate((double)samprate); diff --git a/src/applications/gqrx/mainwindow.h b/src/applications/gqrx/mainwindow.h index 144279a199..7c910239d6 100644 --- a/src/applications/gqrx/mainwindow.h +++ b/src/applications/gqrx/mainwindow.h @@ -197,10 +197,12 @@ private slots: void stopAudioStreaming(); /* I/Q playback and recording*/ - void startIqRecording(const QString& recdir, enum receiver::file_formats fmt, int buffers_max); + void startIqRecording(const QString& recdir, + enum receiver::file_formats fmt, int buffers_max); void stopIqRecording(); - void startIqPlayback(const QString& filename, float samprate, qint64 center_freq, - enum receiver::file_formats fmt, bool repeat); + void startIqPlayback(const QString& filename, float samprate, + qint64 center_freq, enum receiver::file_formats fmt, + int buffers_max, bool repeat); void stopIqPlayback(); void seekIqFile(qint64 seek_pos); diff --git a/src/applications/gqrx/receiver.cpp b/src/applications/gqrx/receiver.cpp index 428855f2d0..585d17c4c0 100644 --- a/src/applications/gqrx/receiver.cpp +++ b/src/applications/gqrx/receiver.cpp @@ -116,7 +116,7 @@ receiver::receiver(const std::string input_device, ddc = make_downconverter_cc(d_ddc_decim, 0.0, d_decim_rate); rx = make_nbrx(d_quad_rate, d_audio_rate); - input_file = gr::blocks::file_source::make(sizeof(gr_complex),get_zero_file().c_str(),false); + input_file = file_source::make(sizeof(gr_complex),get_zero_file().c_str(),0,0,1); input_throttle = gr::blocks::throttle::make(sizeof(gr_complex),192000.0); to_s32lc = any_to_any>::make(); @@ -264,12 +264,14 @@ void receiver::set_input_device(const std::string device) * @param fmt */ void receiver::set_input_file(const std::string name, const int sample_rate, - const enum file_formats fmt, bool repeat) + const enum file_formats fmt, int buffers_max, + bool repeat) { std::string error = ""; size_t sample_size = sample_size_from_format(fmt); - input_file = gr::blocks::file_source::make(sample_size, name.c_str(), repeat); + input_file = file_source::make(sample_size, name.c_str(), 0, 0, sample_rate, + repeat,buffers_max); if (d_running) { @@ -1508,14 +1510,19 @@ receiver::status receiver::seek_iq_file(long pos) return status; } -void receiver::get_iq_recorder_stats(struct iq_recorder_stats &stats) +void receiver::get_iq_tool_stats(struct iq_tool_stats &stats) { - stats.active = d_recording_iq; + stats.recording = d_recording_iq; + stats.playing = (d_last_format != FILE_FORMAT_NONE); if(d_recording_iq && iq_sink) { stats.failed = iq_sink->get_failed(); - stats.buffers_used = iq_sink->get_buffer_usage(); - stats.file_size = iq_sink->get_written(); + stats.buffer_usage = iq_sink->get_buffer_usage(); + stats.file_pos = iq_sink->get_written(); + }else{ + stats.failed = input_file->get_failed(); + stats.buffer_usage = input_file->get_buffer_usage(); + stats.file_pos = input_file->tell(); } } diff --git a/src/applications/gqrx/receiver.h b/src/applications/gqrx/receiver.h index 2a510ac47d..0b31fb8765 100644 --- a/src/applications/gqrx/receiver.h +++ b/src/applications/gqrx/receiver.h @@ -24,8 +24,6 @@ #define RECEIVER_H #include -#include -#include #include #include #include @@ -49,6 +47,7 @@ #include "dsp/format_converter.h" #include "interfaces/udp_sink_f.h" #include "interfaces/file_sink.h" +#include "interfaces/file_source.h" #include "receivers/receiver_base.h" #ifdef WITH_PULSEAUDIO @@ -122,12 +121,13 @@ class receiver FILE_FORMAT_SIGMF, }; - struct iq_recorder_stats + struct iq_tool_stats { - bool active; + bool recording; + bool playing; bool failed; - int buffers_used; - size_t file_size; + int buffer_usage; + size_t file_pos; }; static const unsigned int DEFAULT_FFT_SIZE = 8192; @@ -142,7 +142,8 @@ class receiver void set_input_device(const std::string device); void set_output_device(const std::string device); void set_input_file(const std::string name, const int sample_rate, - const enum file_formats fmt, bool repeat); + const enum file_formats fmt, int buffers_max, + bool repeat); std::vector get_antennas(void) const; void set_antenna(const std::string &antenna); @@ -239,8 +240,8 @@ class receiver status start_iq_recording(const std::string filename, const enum file_formats fmt, int buffers_max); status stop_iq_recording(); status seek_iq_file(long pos); - void get_iq_recorder_stats(struct iq_recorder_stats &stats); - bool is_playing_iq() const { return !(d_iq_fmt == FILE_FORMAT_NONE); } + void get_iq_tool_stats(struct iq_tool_stats &stats); + bool is_playing_iq() { return d_iq_fmt != FILE_FORMAT_NONE; } /* sample sniffer */ status start_sniffer(unsigned int samplrate, int buffsize); @@ -329,7 +330,7 @@ class receiver any_to_any, gr_complex>::sptr from_s8uc; gr::blocks::throttle::sptr input_throttle; - gr::blocks::file_source::sptr input_file; + file_source::sptr input_file; gr::blocks::wavfile_sink::sptr wav_sink; /*!< WAV file sink for recording. */ gr::blocks::wavfile_source::sptr wav_src; /*!< WAV file source for playback. */ diff --git a/src/interfaces/CMakeLists.txt b/src/interfaces/CMakeLists.txt index 9510e92d0f..85f54f2eaf 100644 --- a/src/interfaces/CMakeLists.txt +++ b/src/interfaces/CMakeLists.txt @@ -5,4 +5,6 @@ add_source_files(SRCS_LIST udp_sink_f.h file_sink.cpp file_sink.h + file_source.cpp + file_source.h ) diff --git a/src/interfaces/file_source.cpp b/src/interfaces/file_source.cpp new file mode 100644 index 0000000000..b4c8770631 --- /dev/null +++ b/src/interfaces/file_source.cpp @@ -0,0 +1,482 @@ +/* -*- c++ -*- */ +/* + * Copyright 2012, 2018 Free Software Foundation, Inc. + * + * This file is part of GNU Radio + * + * GNU Radio 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; either version 3, or (at your option) + * any later version. + * + * GNU Radio 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with GNU Radio; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + + +#include "file_source.h" +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#define GR_FSEEK _fseeki64 +#define GR_FTELL _ftelli64 +#define GR_FSTAT _fstat +#define GR_FILENO _fileno +#define GR_STAT _stat +#define S_ISREG(m) (((m)&S_IFMT) == S_IFREG) +#else +#define GR_FSEEK fseeko +#define GR_FTELL ftello +#define GR_FSTAT fstat +#define GR_FILENO fileno +#define GR_STAT stat +#endif + +#define READ_MAX (16*1024*1024) +#define NO_WAIT_START + +void file_source::reader() +{ + uint8_t * last=&d_buf.data()[d_buf.size()]; + uint8_t * p; + FILE * old_fp = NULL; + int count = 0; + while (true) + { + std::unique_lock guard(d_mutex); // hold mutex for duration of this block + do + { + count = 0; + p=d_wp; + if (d_updated) + { + old_fp = d_fp; + d_fp = d_new_fp; // install new file pointer + d_new_fp = 0; + d_updated = false; + d_file_begin = true; + d_eof = false; + } + if(d_seek) + { + guard.unlock(); + d_seek_ok = (GR_FSEEK((FILE*)d_fp, d_seek_point * d_itemsize, SEEK_SET) == 0); + guard.lock(); + d_seek = false; + d_buffering = true; + p=d_rp=d_wp=d_buf.data(); + d_items_remaining = d_length_items + d_start_offset_items - d_seek_point; + d_eof = false; + continue; + } + if(d_eof) + { + break; + } + if (d_fp && !d_failed) + { + if (p == last) + { + if (d_rp == d_buf.data()) + break; + else + p = d_buf.data(); + } + int64_t read_bytes = (last - p); + if (d_rp > p) + read_bytes = d_rp - d_itemsize - p; + if (read_bytes <= 0) + break; + if (read_bytes > READ_MAX) + read_bytes = READ_MAX; + guard.unlock(); + count = fread(p, d_itemsize, read_bytes / d_itemsize, d_fp) * d_itemsize; + guard.lock(); + if (count == 0) + { + if (ferror(d_fp)) + { + std::cerr << "file_source read failed with error " << fileno(d_fp) << std::endl; + d_failed = true; + } + else // is EOF + { + if (d_repeat && d_seekable) + { + guard.unlock(); + d_seek_ok = (GR_FSEEK((FILE*)d_fp, d_start_offset_items * d_itemsize, SEEK_SET) == 0); + guard.lock(); + } + else + d_eof = true; + } + break; + } + } + else + break; + p += count; + d_wp = p; + } + while (count); + if (old_fp) + { + guard.unlock(); + fclose(old_fp); + guard.lock(); + old_fp = NULL; + } + if (d_reader_finish) + { + return; + } + else + { + d_reader_ready.notify_one(); + d_buffering = false; + d_reader_wake.wait(guard); + } + } +} + +file_source::sptr file_source::make(size_t itemsize, + const char* filename, + uint64_t start_offset_items, + uint64_t length_items, int sample_rate, + bool repeat, int buffers_max) +{ + return gnuradio::get_initial_sptr(new file_source( + itemsize, filename, start_offset_items, length_items, + sample_rate, repeat, buffers_max)); +} + +file_source::file_source(size_t itemsize, + const char* filename, + uint64_t start_offset_items, + uint64_t length_items, int sample_rate, + bool repeat, int buffers_max) + : sync_block( + "file_source", gr::io_signature::make(0, 0, 0), gr::io_signature::make(1, 1, itemsize)), + d_itemsize(itemsize), + d_start_offset_items(start_offset_items), + d_length_items(length_items), + d_fp(0), + d_new_fp(0), + d_repeat(repeat), + d_updated(false), + d_file_begin(true), + d_repeat_cnt(0), + d_add_begin_tag(pmt::PMT_NIL), + d_reader_finish(false) +{ + open(filename, repeat, start_offset_items, length_items); + + std::stringstream str; + str << name() << unique_id(); + _id = pmt::string_to_symbol(str.str()); + d_eof = false; + d_closing = false; + d_failed = false; + d_seek = false; + d_buffering = false; + if(buffers_max <= 0) + buffers_max = 1; + d_buffer_size = buffers_max * std::max(8192, sample_rate) * itemsize; + d_buf.resize(d_buffer_size); + d_wp = d_rp = d_buf.data(); + { + std::unique_lock guard(d_mutex); + d_reader_thread = new std::thread(std::bind(&file_source::reader, this)); + d_buffering = true; + d_reader_wake.notify_one(); +#ifdef WAIT_START + d_reader_ready.wait(guard); +#endif + } +} + +file_source::~file_source() +{ + d_reader_finish = true; + d_reader_wake.notify_one(); + d_reader_thread->join(); + delete d_reader_thread; + if (d_fp) + fclose((FILE*)d_fp); + if (d_new_fp) + fclose((FILE*)d_new_fp); +} + +bool file_source::seek(int64_t seek_point, int whence) +{ + if (d_seekable) + { + std::unique_lock guard(d_mutex); + + d_seek_point = seek_point + d_start_offset_items; + switch (whence) + { + case SEEK_SET: + break; + case SEEK_CUR: + d_seek_point += (d_length_items - d_items_remaining); + break; + case SEEK_END: + d_seek_point = d_length_items - seek_point; + break; + default: + GR_LOG_WARN(d_logger, "bad seek mode"); + return 0; + } + + if ((seek_point < (int64_t)d_start_offset_items) || + (seek_point > (int64_t)(d_start_offset_items + d_length_items - 1))) + { + GR_LOG_WARN(d_logger, "bad seek point"); + return 0; + } + d_seek = true; + d_seek_ok = 0; + d_reader_wake.notify_one(); +#ifdef WAIT_START + d_reader_ready.wait(guard); +#endif + return d_seek_ok; + } + else + { + GR_LOG_WARN(d_logger, "file not seekable"); + return 0; + } +} + + +void file_source::open(const char* filename, + bool repeat, + uint64_t start_offset_items, + uint64_t length_items) +{ + // obtain exclusive access for duration of this function + std::unique_lock guard(d_mutex); + + if (d_new_fp) + { + fclose(d_new_fp); + d_new_fp = 0; + } + + if ((d_new_fp = fopen(filename, "rb")) == NULL) + { + GR_LOG_ERROR(d_logger, std::string(filename) + ": " + std::string(strerror(errno))); + throw std::runtime_error("can't open file"); + } + + struct GR_STAT st; + + if (GR_FSTAT(GR_FILENO(d_new_fp), &st)) + { + GR_LOG_ERROR(d_logger, std::string(filename) + ": " + std::string(strerror(errno))); + throw std::runtime_error("can't fstat file"); + } + d_seekable = (S_ISREG(st.st_mode)); + + uint64_t file_size; + + if (d_seekable) + { + // Check to ensure the file will be consumed according to item size + GR_FSEEK(d_new_fp, 0, SEEK_END); + file_size = GR_FTELL(d_new_fp); + + // Make sure there will be at least one item available + if ((file_size / d_itemsize) < (start_offset_items + 1)) + { + if (start_offset_items) + { + GR_LOG_WARN(d_logger, "file is too small for start offset"); + } + else + { + GR_LOG_WARN(d_logger, "file is too small"); + } + fclose(d_new_fp); + throw std::runtime_error("file is too small"); + } + } + else + { + file_size = INT64_MAX; + } + + uint64_t items_available = (file_size / d_itemsize - start_offset_items); + + // If length is not specified, use the remainder of the file. Check alignment at end. + if (length_items == 0) + { + length_items = items_available; + if (file_size % d_itemsize) + { + GR_LOG_WARN(d_logger, "file size is not a multiple of item size"); + } + } + + // Check specified length. Warn and use available items instead of throwing an + // exception. + if (length_items > items_available) + { + length_items = items_available; + GR_LOG_WARN(d_logger, "file too short, will read fewer than requested items"); + } + + // Rewind to start offset + if (d_seekable) + { + GR_FSEEK(d_new_fp, start_offset_items * d_itemsize, SEEK_SET); + } + + d_updated = true; + d_repeat = repeat; + d_start_offset_items = start_offset_items; + d_length_items = length_items; + d_items_remaining = length_items; +} + +void file_source::close() +{ + // obtain exclusive access for duration of this function + std::unique_lock guard(d_mutex); + + if (d_new_fp != NULL) + { + fclose(d_new_fp); + d_new_fp = NULL; + } + d_updated = true; +} + +void file_source::set_begin_tag(pmt::pmt_t val) { d_add_begin_tag = val; } + +int file_source::work(int noutput_items, + gr_vector_const_void_star& input_items, + gr_vector_void_star& output_items) +{ + char* o = (char*)output_items[0]; + uint64_t size = noutput_items; +#if 0 + do_update(); // update d_fp is reqd +#endif + if (d_fp == NULL) + throw std::runtime_error("work with file not open"); + + std::unique_lock guard(d_mutex); + + // No items remaining - all done + if (d_items_remaining == 0) + return WORK_DONE; + + while (size) + { + d_reader_wake.notify_one(); + if(d_wp == d_rp) + d_buffering = true; + if(d_buffering) + { + //d_reader_ready.wait(guard); + //output zeroes while we are buffering + guard.unlock(); + memset(o, 0, d_itemsize * noutput_items); + return noutput_items; + } + uint64_t bytes_avail = (d_wp >= d_rp) ? (d_wp - d_rp): + (&d_buf.data()[d_buf.size()] - d_rp); + if(bytes_avail >= d_itemsize) + { + uint64_t items_to_copy = bytes_avail / d_itemsize; + uint64_t to_copy = std::min(size, items_to_copy); + if(to_copy > d_items_remaining) + to_copy = d_items_remaining; + guard.unlock(); + if (d_file_begin && d_add_begin_tag != pmt::PMT_NIL) + { + add_item_tag(0, + nitems_written(0) + noutput_items - size, + d_add_begin_tag, + pmt::from_long(d_repeat_cnt), + _id); + d_file_begin = false; + } + memcpy(o, d_rp, to_copy * d_itemsize); + guard.lock(); + o += to_copy * d_itemsize; + d_rp += to_copy * d_itemsize; + if(d_rp == &d_buf.data()[d_buf.size()]) + d_rp = d_buf.data(); + size -= to_copy; + d_items_remaining -= to_copy; + if (d_items_remaining == 0) + { + + // Repeat: rewind and request tag + if (d_repeat && d_seekable) + { + if (d_add_begin_tag != pmt::PMT_NIL) + { + add_item_tag(0, + nitems_written(0) + noutput_items - size, + d_add_begin_tag, + pmt::from_long(d_repeat_cnt), + _id); + d_file_begin = false; + } + d_items_remaining = d_length_items - d_start_offset_items; + } + else + break; + } + } + else + d_buffering = true; +#if 0 + // Add stream tag whenever the file starts again + + uint64_t nitems_to_read = std::min(size, d_items_remaining); + + // Since the bounds of the file are known, unexpected nitems is an error + if (nitems_to_read != fread(o, d_itemsize, nitems_to_read, (FILE*)d_fp)) + throw std::runtime_error("fread error"); + + size -= nitems_to_read; + d_items_remaining -= nitems_to_read; + o += nitems_to_read * d_itemsize; + + // Ran out of items ("EOF") +#endif + } + return (noutput_items - size); +} + +uint64_t file_source::tell() +{ + std::unique_lock guard(d_mutex); + return (d_length_items - d_items_remaining) * d_itemsize; +} + +int file_source::get_buffer_usage() +{ + std::unique_lock guard(d_mutex); + return (d_wp >= d_rp ? d_wp - d_rp : + d_wp + d_buffer_size - d_rp) * 100llu / d_buffer_size; +} diff --git a/src/interfaces/file_source.h b/src/interfaces/file_source.h new file mode 100644 index 0000000000..9b197df2ab --- /dev/null +++ b/src/interfaces/file_source.h @@ -0,0 +1,112 @@ +/* -*- c++ -*- */ +/* + * Copyright 2012, 2018 Free Software Foundation, Inc. + * + * This file is part of GNU Radio + * + * GNU Radio 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; either version 3, or (at your option) + * any later version. + * + * GNU Radio 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with GNU Radio; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef GQRX_FILE_SOURCE_C_H +#define GQRX_FILE_SOURCE_C_H + +#include +#include +#include +#include +#include + +class BLOCKS_API file_source : public gr::sync_block +{ +public: +#if GNURADIO_VERSION < 0x030900 + typedef boost::shared_ptr sptr; +#else + typedef std::shared_ptr sptr; +#endif + + /*! + * \brief Make a file sink. + * \param itemsize size of the input data items. + * \param filename name of the file to open and write output to. + * \param append if true, data is appended to the file instead of + * overwriting the initial content. + */ + static sptr make(size_t itemsize, const char *filename, uint64_t offset, + uint64_t len, int sample_rate, bool repeat = false, + int buffers_max = 1); + +private: + size_t d_itemsize; + uint64_t d_start_offset_items; + uint64_t d_length_items; + uint64_t d_items_remaining; + FILE* d_fp; + FILE* d_new_fp; + bool d_repeat; + bool d_updated; + bool d_file_begin; + bool d_seekable; + long d_repeat_cnt; + pmt::pmt_t d_add_begin_tag; + + std::mutex d_mutex; + std::vector d_buf; + uint8_t * d_rp; + uint8_t * d_wp; + std::condition_variable d_reader_wake; + std::condition_variable d_reader_ready; + bool d_reader_finish; + std::thread * d_reader_thread; + bool d_failed; + bool d_eof; + bool d_closing; + bool d_seek; + bool d_buffering; + uint64_t d_seek_point; + uint64_t d_buffer_size; + int d_seek_ok; + pmt::pmt_t _id; +#if 0 + void do_update(); +#endif + void reader(); + +public: + file_source(size_t itemsize, + const char* filename, + uint64_t offset, + uint64_t len, + int sample_rate, + bool repeat = false, + int buffers_max = 8); + ~file_source(); + + bool seek(int64_t seek_point, int whence); + void open(const char* filename, bool repeat, uint64_t offset, uint64_t len); + void close(); + uint64_t tell(); + bool get_failed() const { return d_failed;}; + int get_buffer_usage(); + + int work(int noutput_items, + gr_vector_const_void_star& input_items, + gr_vector_void_star& output_items); + + void set_begin_tag(pmt::pmt_t val); +}; + +#endif diff --git a/src/qtgui/iq_tool.cpp b/src/qtgui/iq_tool.cpp index 324ac38daf..9e36fbf070 100644 --- a/src/qtgui/iq_tool.cpp +++ b/src/qtgui/iq_tool.cpp @@ -123,6 +123,7 @@ void CIqTool::switchControlsState(bool recording, bool playback) ui->playButton->setEnabled(!recording); ui->slider->setEnabled(!recording); + ui->repeat->setEnabled(!(recording || playback)); ui->listWidget->setEnabled(!(recording || playback)); ui->recDirEdit->setEnabled(!(recording || playback)); ui->recDirButton->setEnabled(!(recording || playback)); @@ -179,6 +180,7 @@ void CIqTool::on_playButton_clicked(bool checked) emit startPlayback(recdir->absoluteFilePath(current_file), (float)sample_rate, center_freq, fmt, + ui->buffersSpinBox->value(), ui->repeat->checkState() == Qt::Checked); } } @@ -271,8 +273,7 @@ void CIqTool::stopIqRecorder(void) void CIqTool::cancelRecording() { ui->recButton->setChecked(false); - ui->playButton->setEnabled(true); - is_recording = false; + on_recButton_clicked(false); } /*! \brief Update GUI ta match current recorder state. @@ -281,19 +282,31 @@ void CIqTool::cancelRecording() */ void CIqTool::updateStats(bool hasFailed, int buffersUsed, size_t fileSize) { - if (hasFailed) + if(is_recording) { - QMessageBox msg_box; - msg_box.setIcon(QMessageBox::Critical); - msg_box.setText(tr("IQ recording failed.")); - msg_box.exec(); + if (hasFailed) + { + QMessageBox msg_box; + msg_box.setIcon(QMessageBox::Critical); + msg_box.setText(tr("IQ recording failed.")); + msg_box.exec(); + } + else + { + if(o_buffersUsed!=buffersUsed) + ui->bufferStats->setText(QString("Buffer: %1%").arg(buffersUsed)); + if(o_fileSize != fileSize) + ui->sizeStats->setText(QString("Size: %1 bytes").arg(fileSize)); + o_buffersUsed = buffersUsed; + o_fileSize = fileSize; + } } - else + if(is_playing) { if(o_buffersUsed!=buffersUsed) - ui->bufferStats->setText(QString("Buffers: %1/%2").arg(buffersUsed).arg(ui->buffersSpinBox->value())); + ui->bufferStats->setText(QString("Buffer: %1%").arg(buffersUsed)); if(o_fileSize != fileSize) - ui->sizeStats->setText(QString("Size: %1 bytes").arg(fileSize)); + ui->sizeStats->setText(QString("Pos: %1 bytes").arg(fileSize)); o_buffersUsed = buffersUsed; o_fileSize = fileSize; } @@ -397,12 +410,11 @@ void CIqTool::timeoutFunction(void) if (is_playing) { - // advance slider with one second - int val = ui->slider->value(); + int val = o_fileSize / (sample_rate * bytes_per_sample); if (val < ui->slider->maximum()) { ui->slider->blockSignals(true); - ui->slider->setValue(val + 1); + ui->slider->setValue(val); ui->slider->blockSignals(false); refreshTimeWidgets(); } diff --git a/src/qtgui/iq_tool.h b/src/qtgui/iq_tool.h index a985156109..1bcf648646 100644 --- a/src/qtgui/iq_tool.h +++ b/src/qtgui/iq_tool.h @@ -64,10 +64,12 @@ class CIqTool : public QDialog void readSettings(QSettings *settings); signals: - void startRecording(const QString recdir, enum receiver::file_formats fmt, int buffers_max); + void startRecording(const QString recdir, enum receiver::file_formats fmt, + int buffers_max); void stopRecording(); - void startPlayback(const QString filename, float samprate, qint64 center_freq, - enum receiver::file_formats fmt, bool repeat); + void startPlayback(const QString filename, float samprate, + qint64 center_freq, enum receiver::file_formats fmt, + int buffers_max, bool repeat); void stopPlayback(); void seek(qint64 seek_pos);