From da237640fa9d2d66bb29f0a76c5ca6deb103e9c0 Mon Sep 17 00:00:00 2001 From: Vladisslav P Date: Mon, 10 Jan 2022 18:57:01 +0300 Subject: [PATCH] Implement recording/playback of different sample formats --- src/applications/gqrx/mainwindow.cpp | 48 +++- src/applications/gqrx/mainwindow.h | 6 +- src/applications/gqrx/receiver.cpp | 410 ++++++++++++++++++++++----- src/applications/gqrx/receiver.h | 48 +++- src/dsp/CMakeLists.txt | 1 + src/dsp/format_converter.h | 324 +++++++++++++++++++++ src/qtgui/iq_tool.cpp | 80 +++++- src/qtgui/iq_tool.h | 11 +- src/qtgui/iq_tool.ui | 72 +++-- 9 files changed, 876 insertions(+), 124 deletions(-) create mode 100644 src/dsp/format_converter.h diff --git a/src/applications/gqrx/mainwindow.cpp b/src/applications/gqrx/mainwindow.cpp index f014cccf58..3828d5a7ff 100644 --- a/src/applications/gqrx/mainwindow.cpp +++ b/src/applications/gqrx/mainwindow.cpp @@ -67,6 +67,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_have_audio(true), dec_afsk1200(nullptr) @@ -278,9 +279,9 @@ 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)), this, SLOT(startIqRecording(QString))); + connect(iq_tool, SIGNAL(startRecording(QString, enum receiver::file_formats)), this, SLOT(startIqRecording(QString, enum receiver::file_formats))); connect(iq_tool, SIGNAL(stopRecording()), this, SLOT(stopIqRecording())); - 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))); @@ -1569,25 +1570,50 @@ void MainWindow::stopAudioStreaming() } /** Start I/Q recording. */ -void MainWindow::startIqRecording(const QString& recdir) +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 = (qint64)(rx->get_rf_freq()); auto sr = (qint64)(rx->get_input_rate()); auto dec = (quint32)(rx->get_input_decim()); + QString suffix = "fc"; + switch(fmt) + { + case receiver::FILE_FORMAT_CS8: + suffix = "8"; + break; + case receiver::FILE_FORMAT_CS16L: + suffix = "16"; + break; + case receiver::FILE_FORMAT_CS32L: + suffix = "32"; + break; + case receiver::FILE_FORMAT_CS8U: + suffix = "8u"; + break; + case receiver::FILE_FORMAT_CS16LU: + suffix = "16u"; + break; + case receiver::FILE_FORMAT_CS32LU: + suffix = "32u"; + break; + default: + fmt = receiver::FILE_FORMAT_CF; + suffix = "fc"; + } auto lastRec = QDateTime::currentDateTimeUtc(). - toString("%1/gqrx_yyyyMMdd_hhmmss_%2_%3_fc.'raw'") - .arg(recdir).arg(freq).arg(sr/dec); + toString("%1/gqrx_yyyyMMdd_hhmmss_%2_%3_%4.'raw'") + .arg(recdir).arg(freq).arg(sr/dec).arg(suffix); ui->actionIoConfig->setDisabled(true); ui->actionLoadSettings->setDisabled(true); // start recorder; fails if recording already in progress - if (rx->start_iq_recording(lastRec.toStdString())) + if (rx->start_iq_recording(lastRec.toStdString(), fmt)) { // reset action status ui->statusBar->showMessage(tr("Error starting I/Q recoder")); + iq_tool->cancelRecording(); // show an error message to user QMessageBox msg_box; @@ -1617,7 +1643,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()) { @@ -1626,8 +1652,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; @@ -1640,6 +1664,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(samprate); @@ -1697,9 +1722,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 f4efba9040..013f7e95ce 100644 --- a/src/applications/gqrx/mainwindow.h +++ b/src/applications/gqrx/mainwindow.h @@ -80,8 +80,6 @@ public slots: qint64 d_hw_freq; qint64 d_hw_freq_start{}; qint64 d_hw_freq_stop{}; - qint64 backupFreq; /* for IQ player */ - qint64 backupOffset; /* for IQ player */ bool d_ignore_limits; @@ -182,9 +180,9 @@ private slots: void stopAudioStreaming(); /* I/Q playback and recording*/ - void startIqRecording(const QString& recdir); + 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 8f36d78183..718de1b2ef 100644 --- a/src/applications/gqrx/receiver.cpp +++ b/src/applications/gqrx/receiver.cpp @@ -71,6 +71,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) { @@ -113,6 +115,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(8192u, d_decim_rate, gr::fft::window::WIN_HANN); @@ -198,25 +217,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 @@ -228,21 +240,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(); @@ -251,8 +288,86 @@ 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: + 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. @@ -270,8 +385,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(); @@ -286,6 +413,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); } @@ -490,7 +619,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); } /** @@ -851,7 +980,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; @@ -870,46 +999,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; @@ -1174,13 +1303,94 @@ 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: + 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; @@ -1189,7 +1399,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) { @@ -1197,33 +1407,52 @@ 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: + 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; @@ -1241,7 +1470,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; } @@ -1313,27 +1542,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) @@ -1380,20 +1595,37 @@ 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_sink, 0); + tb->connect(rx, 1, wav_sink, 1); + } - // Recorders and sniffers - if (d_recording_wav) - { - tb->connect(rx, 0, wav_sink, 0); - tb->connect(rx, 1, wav_sink, 1); + if (d_sniffer_active) + { + tb->connect(rx, 0, sniffer_rr, 0); + tb->connect(sniffer_rr, 0, sniffer, 0); + } } - - if (d_sniffer_active) + else { - tb->connect(rx, 0, sniffer_rr, 0); - tb->connect(sniffer_rr, 0, sniffer, 0); + if (d_recording_wav) + { + wav_sink->close(); + wav_sink.reset(); + d_recording_wav = false; + } + + if (d_sniffer_active) + { + d_sniffer_active = false; + + /* delete resampler */ + sniffer_rr.reset(); + } } + } void receiver::get_rds_data(std::string &outbuff, int &num) @@ -1439,6 +1671,28 @@ 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_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 6044f6c543..455d140c3a 100644 --- a/src/applications/gqrx/receiver.h +++ b/src/applications/gqrx/receiver.h @@ -30,9 +30,11 @@ #endif #include +#include #include #include #include +#include #include #include #include @@ -49,6 +51,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" @@ -110,6 +113,18 @@ 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, + }; + receiver(const std::string input_device="", const std::string audio_device="", unsigned int decimation=1); @@ -119,6 +134,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); @@ -186,7 +202,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); @@ -210,7 +228,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; } @@ -232,9 +250,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. */ @@ -253,6 +275,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. */ @@ -277,6 +301,24 @@ class receiver gr::blocks::multiply_const_ff::sptr audio_gain1; /*!< Audio 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/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 e4d102ba18..d244c2e7e2 100644 --- a/src/qtgui/iq_tool.cpp +++ b/src/qtgui/iq_tool.cpp @@ -46,12 +46,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"); error_palette = new QPalette(); @@ -59,6 +60,15 @@ 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); + } CIqTool::~CIqTool() @@ -113,6 +123,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 +155,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 @@ -186,7 +197,7 @@ void CIqTool::on_recButton_clicked(bool checked) if (checked) { switchControlsState(true, false); - emit startRecording(recdir->path()); + emit startRecording(recdir->path(), rec_fmt); refreshDir(); ui->listWidget->setCurrentRow(ui->listWidget->count()-1); @@ -247,7 +258,7 @@ void CIqTool::saveSettings(QSettings *settings) settings->setValue("baseband/rec_dir", dir); else settings->remove("baseband/rec_dir"); - + settings->setValue("baseband/rec_fmt", rec_fmt); } void CIqTool::readSettings(QSettings *settings) @@ -258,9 +269,21 @@ void CIqTool::readSettings(QSettings *settings) // Location of baseband recordings QString dir = settings->value("baseband/rec_dir", QDir::homePath()).toString(); ui->recDirEdit->setText(dir); + 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. */ @@ -311,6 +334,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() { @@ -370,7 +399,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) { @@ -379,6 +407,7 @@ void CIqTool::parseFileName(const QString &filename) bool center_ok; qint64 center; + QString fmt_str = ""; QStringList list = filename.split('_'); if (list.size() < 5) @@ -388,8 +417,47 @@ 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(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 e47e6ca565..c41c2412bc 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); + 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); @@ -80,6 +82,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); @@ -87,7 +90,6 @@ private slots: void parseFileName(const QString &filename); void switchControlsState(bool recording, bool playback); - private: Ui::CIqTool *ui; @@ -100,6 +102,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 e28618dd1d..ed5773c200 100644 --- a/src/qtgui/iq_tool.ui +++ b/src/qtgui/iq_tool.ui @@ -7,7 +7,7 @@ 0 0 482 - 327 + 320 @@ -90,6 +90,12 @@ true + + + 0 + 0 + + 32 @@ -160,7 +166,7 @@ 40 - 20 + 10 @@ -183,7 +189,7 @@ 40 - 20 + 10 @@ -210,20 +216,52 @@ - - - - 0 - 50 - - - - QFrame::Box - - - QFrame::Sunken - - + + + + + + 0 + 0 + + + + + 32 + 0 + + + + Format: + + + + + + + true + + + + 0 + 0 + + + + + 32 + 24 + + + + false + + + true + + + +