diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6be23310..6503afef5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,8 @@ jobs: soapysdr-module-remote \ libuhd-dev \ liborc-0.4-dev \ - libhidapi-dev + libhidapi-dev \ + libsndfile-dev cd /tmp git clone https://gitea.osmocom.org/sdr/rtl-sdr.git @@ -148,7 +149,7 @@ jobs: run: | brew update brew install --HEAD librtlsdr - brew install airspy airspyhf boost dylibbundler gnuradio hackrf libbladerf libserialport portaudio pybind11 six uhd qt@6 || true + brew install airspy airspyhf boost dylibbundler gnuradio hackrf libbladerf libserialport portaudio pybind11 six uhd libsndfile qt@6 || true brew tap pothosware/homebrew-pothos brew install soapyremote diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98565a2f4..9518c6e89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,8 +79,7 @@ jobs: - name: Install dependencies run: | brew update - brew install airspy boost gnuradio hackrf libbladerf librtlsdr pybind11 six uhd qt@6 || true - + brew install airspy boost gnuradio hackrf libbladerf librtlsdr pybind11 six uhd libsndfile qt@6 || true cd /tmp git clone https://gitea.osmocom.org/sdr/gr-osmosdr.git cd gr-osmosdr diff --git a/CMakeLists.txt b/CMakeLists.txt index b0deec9e2..634f6dca7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,7 @@ endif() include(FindPkgConfig) find_package(Gnuradio-osmosdr REQUIRED) +find_package(SNDFILE REQUIRED) set(GR_REQUIRED_COMPONENTS RUNTIME ANALOG AUDIO BLOCKS DIGITAL FILTER FFT PMT) find_package(Gnuradio REQUIRED COMPONENTS analog audio blocks digital filter fft network) diff --git a/README.md b/README.md index f1118d14e..48cf0cb56 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ To compile gqrx from source you need the following dependencies: - Network - Widgets - Svg (runtime-only) +- libsndfile - cmake version >= 3.2.0 Gqrx can be compiled from within Qt Creator or in a terminal: diff --git a/cmake/Modules/FindSNDFILE.cmake b/cmake/Modules/FindSNDFILE.cmake new file mode 100644 index 000000000..14f362d4f --- /dev/null +++ b/cmake/Modules/FindSNDFILE.cmake @@ -0,0 +1,34 @@ +find_package(PkgConfig) +PKG_CHECK_MODULES(PC_SNDFILE "sndfile") + +FIND_PATH(SNDFILE_INCLUDE_DIRS + NAMES sndfile.h + HINTS ${PC_SNDFILE_INCLUDE_DIR} + ${CMAKE_INSTALL_PREFIX}/include + PATHS + /usr/local/include + /usr/include +) + +FIND_LIBRARY(SNDFILE_LIBRARIES + NAMES sndfile ${SNDFILE_LIBRARY_NAME} + HINTS ${PC_SNDFILE_LIBDIR} + ${CMAKE_INSTALL_PREFIX}/lib + ${CMAKE_INSTALL_PREFIX}/lib64 + PATHS + ${SNDFILE_INCLUDE_DIRS}/../lib + /usr/local/lib + /usr/lib +) + +INCLUDE(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(SNDFILE DEFAULT_MSG SNDFILE_LIBRARIES SNDFILE_INCLUDE_DIRS) +MARK_AS_ADVANCED(SNDFILE_LIBRARIES SNDFILE_INCLUDE_DIRS) + +if (SNDFILE_FOUND AND NOT TARGET sndfile::sndfile) + add_library(sndfile::sndfile INTERFACE IMPORTED) + set_target_properties(sndfile::sndfile PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${SNDFILE_INCLUDE_DIRS}" + INTERFACE_LINK_LIBRARIES "${SNDFILE_LIBRARIES}" + ) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 838b0f7f8..6a5fb4b05 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -98,6 +98,7 @@ target_link_libraries(${PROJECT_NAME} ${PULSEAUDIO_LIBRARY} ${PULSE-SIMPLE} ${PORTAUDIO_LIBRARIES} + ${SNDFILE_LIBRARIES} ) if(NOT Gnuradio_VERSION VERSION_LESS "3.10") diff --git a/src/applications/gqrx/mainwindow.cpp b/src/applications/gqrx/mainwindow.cpp index b4ee6f32e..97cea203e 100644 --- a/src/applications/gqrx/mainwindow.cpp +++ b/src/applications/gqrx/mainwindow.cpp @@ -101,6 +101,9 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) /* create receiver object */ rx = new receiver("", "", 1); rx->set_rf_freq(144500000.0); + rx->set_audio_rec_event_handler(std::bind(audio_rec_event, this, + std::placeholders::_1, + std::placeholders::_2)); // remote controller remote = new RemoteControl(); @@ -243,12 +246,16 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) connect(uiDockAudio, SIGNAL(audioMuteChanged(bool)), this, SLOT(setAudioMute(bool))); connect(uiDockAudio, SIGNAL(audioStreamingStarted(QString,int,bool)), this, SLOT(startAudioStream(QString,int,bool))); connect(uiDockAudio, SIGNAL(audioStreamingStopped()), this, SLOT(stopAudioStreaming())); - connect(uiDockAudio, SIGNAL(audioRecStarted(QString)), this, SLOT(startAudioRec(QString))); - connect(uiDockAudio, SIGNAL(audioRecStarted(QString)), remote, SLOT(startAudioRecorder(QString))); - connect(uiDockAudio, SIGNAL(audioRecStopped()), this, SLOT(stopAudioRec())); - connect(uiDockAudio, SIGNAL(audioRecStopped()), remote, SLOT(stopAudioRecorder())); + connect(uiDockAudio, SIGNAL(audioRecStart()), this, SLOT(startAudioRec())); + connect(uiDockAudio, SIGNAL(audioRecStart()), remote, SLOT(startAudioRecorder())); + connect(uiDockAudio, SIGNAL(audioRecStop()), this, SLOT(stopAudioRec())); + connect(uiDockAudio, SIGNAL(audioRecStop()), remote, SLOT(stopAudioRecorder())); connect(uiDockAudio, SIGNAL(audioPlayStarted(QString)), this, SLOT(startAudioPlayback(QString))); connect(uiDockAudio, SIGNAL(audioPlayStopped()), this, SLOT(stopAudioPlayback())); + connect(uiDockAudio, SIGNAL(recDirChanged(QString)), this, SLOT(recDirChanged(QString))); + connect(uiDockAudio, SIGNAL(recSquelchTriggeredChanged(bool)), this, SLOT(recSquelchTriggeredChanged(bool))); + connect(uiDockAudio, SIGNAL(recMinTimeChanged(int)), this, SLOT(recMinTimeChanged(int))); + connect(uiDockAudio, SIGNAL(recMaxGapChanged(int)), this, SLOT(recMaxGapChanged(int))); connect(uiDockAudio, SIGNAL(fftRateChanged(int)), this, SLOT(setAudioFftRate(int))); connect(uiDockFft, SIGNAL(fftSizeChanged(int)), this, SLOT(setIqFftSize(int))); connect(uiDockFft, SIGNAL(fftRateChanged(int)), this, SLOT(setIqFftRate(int))); @@ -310,8 +317,8 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) connect(remote, SIGNAL(newSquelchLevel(double)), uiDockRxOpt, SLOT(setSquelchLevel(double))); connect(remote, SIGNAL(newAudioGain(float)), this, SLOT(setAudioGain(float))); connect(uiDockRxOpt, SIGNAL(sqlLevelChanged(double)), remote, SLOT(setSquelchLevel(double))); - connect(remote, SIGNAL(startAudioRecorderEvent()), uiDockAudio, SLOT(startAudioRecorder())); - connect(remote, SIGNAL(stopAudioRecorderEvent()), uiDockAudio, SLOT(stopAudioRecorder())); + connect(remote, SIGNAL(startAudioRecorderEvent()), this, SLOT(startAudioRec())); + connect(remote, SIGNAL(stopAudioRecorderEvent()), this, SLOT(stopAudioRec())); connect(ui->plotter, SIGNAL(newFilterFreq(int, int)), remote, SLOT(setPassband(int, int))); connect(remote, SIGNAL(newPassband(int)), this, SLOT(setPassband(int))); connect(remote, SIGNAL(gainChanged(QString, double)), uiDockInputCtl, SLOT(setGain(QString,double))); @@ -320,6 +327,7 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent) rds_timer = new QTimer(this); connect(rds_timer, SIGNAL(timeout()), this, SLOT(rdsTimeout())); + connect(this, SIGNAL(sigAudioRecEvent(QString, bool)), this, SLOT(audioRecEvent(QString, bool)), Qt::QueuedConnection); // enable frequency tooltips on FFT plot ui->plotter->setTooltipsEnabled(true); @@ -1551,11 +1559,47 @@ void MainWindow::rdsTimeout() } } +/** + * @brief Set audio recording directory. + * @param dir The directory, where audio files should be created. + */ +void MainWindow::recDirChanged(const QString dir) +{ + rx->set_audio_rec_dir(dir.toStdString()); +} + +/** + * @brief Set audio recording squelch triggered mode. + * @param enabled New state. + */ +void MainWindow::recSquelchTriggeredChanged(const bool enabled) +{ + rx->set_audio_rec_sql_triggered(enabled); +} + +/** + * @brief Set audio recording squelch triggered minimum time. + * @param time_ms New time in milliseconds. + */ +void MainWindow::recMinTimeChanged(const int time_ms) +{ + rx->set_audio_rec_min_time(time_ms); +} + +/** + * @brief Set audio recording squelch triggered maximum gap time. + * @param time_ms New time in milliseconds. + */ +void MainWindow::recMaxGapChanged(const int time_ms) +{ + rx->set_audio_rec_max_gap(time_ms); +} + /** * @brief Start audio recorder. * @param filename The file name into which audio should be recorded. */ -void MainWindow::startAudioRec(const QString& filename) +void MainWindow::startAudioRec() { if (!d_have_audio) { @@ -1567,17 +1611,11 @@ void MainWindow::startAudioRec(const QString& filename) msg_box.exec(); uiDockAudio->setAudioRecButtonState(false); } - else if (rx->start_audio_recording(filename.toStdString())) + else if (rx->start_audio_recording()) { ui->statusBar->showMessage(tr("Error starting audio recorder")); - - /* reset state of record button */ uiDockAudio->setAudioRecButtonState(false); } - else - { - ui->statusBar->showMessage(tr("Recording audio to %1").arg(filename)); - } } /** Stop audio recorder. */ @@ -1587,15 +1625,23 @@ void MainWindow::stopAudioRec() { /* okay, this one would be weird if it really happened */ ui->statusBar->showMessage(tr("Error stopping audio recorder")); - - uiDockAudio->setAudioRecButtonState(true); } - else +} + +/** Audio recording is started or stopped. */ +void MainWindow::audioRecEvent(const QString filename, bool is_running) +{ + if(is_running) { + ui->statusBar->showMessage(tr("Recording audio to %1").arg(filename)); + uiDockAudio->audioRecStarted(QString(filename)); + }else{ + /* reset state of record button */ + uiDockAudio->audioRecStopped(); ui->statusBar->showMessage(tr("Audio recorder stopped"), 5000); - } -} + } +} /** Start playback of audio file. */ void MainWindow::startAudioPlayback(const QString& filename) @@ -2581,3 +2627,14 @@ void MainWindow::checkDXCSpotTimeout() DXCSpots::Get().checkSpotTimeout(); } +/** Called from GNU Radio thread */ +void MainWindow::audioRecEventEmitter(std::string filename, bool is_running) +{ + emit sigAudioRecEvent(QString(filename.data()), is_running); +} + +/** Called from GNU Radio thread */ +void MainWindow::audio_rec_event(MainWindow *self, std::string filename, bool is_running) +{ + self->audioRecEventEmitter(filename, is_running); +} diff --git a/src/applications/gqrx/mainwindow.h b/src/applications/gqrx/mainwindow.h index 7142d723b..843ae4e08 100644 --- a/src/applications/gqrx/mainwindow.h +++ b/src/applications/gqrx/mainwindow.h @@ -55,6 +55,9 @@ class MainWindow : public QMainWindow { Q_OBJECT +signals: + void sigAudioRecEvent(const QString filename, bool is_running); + public: explicit MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent = nullptr); ~MainWindow() override; @@ -134,6 +137,8 @@ public slots: /* key shortcuts */ void frequencyFocusShortcut(); void rxOffsetZeroShortcut(); + void audioRecEventEmitter(std::string filename, bool is_running); + static void audio_rec_event(MainWindow *self, std::string filename, bool is_running); private slots: /* RecentConfig */ @@ -176,8 +181,13 @@ private slots: void setPassband(int bandwidth); /* audio recording and playback */ - void startAudioRec(const QString& filename); + void recDirChanged(const QString dir); + void recSquelchTriggeredChanged(const bool enabled); + void recMinTimeChanged(const int time_ms); + void recMaxGapChanged(const int time_ms); + void startAudioRec(); void stopAudioRec(); + void audioRecEvent(const QString filename, bool is_running); void startAudioPlayback(const QString& filename); void stopAudioPlayback(); diff --git a/src/applications/gqrx/receiver.cpp b/src/applications/gqrx/receiver.cpp index 26394f642..caa24a127 100644 --- a/src/applications/gqrx/receiver.cpp +++ b/src/applications/gqrx/receiver.cpp @@ -147,6 +147,9 @@ receiver::receiver(const std::string input_device, gr::prefs pref; qDebug() << "Using audio backend:" << pref.get_string("audio", "audio_module", "N/A").c_str(); + rx->set_rec_event_handler(std::bind(audio_rec_event, this, + std::placeholders::_1, + std::placeholders::_2)); } receiver::~receiver() @@ -619,6 +622,7 @@ receiver::status receiver::set_rf_freq(double freq_hz) d_rf_freq = freq_hz; src->set_center_freq(d_rf_freq); + rx->set_center_freq(d_rf_freq);//to generate audio filename // FIXME: read back frequency? return STATUS_OK; @@ -736,6 +740,7 @@ receiver::status receiver::set_filter_offset(double offset_hz) { d_filter_offset = offset_hz; ddc->set_center_freq(d_filter_offset - d_cw_offset); + rx->set_offset(offset_hz);//to generate audio filename from return STATUS_OK; } @@ -1096,6 +1101,30 @@ receiver::status receiver::set_amsync_pll_bw(float pll_bw) return STATUS_OK; } +receiver::status receiver::set_audio_rec_dir(const std::string dir) +{ + rx->set_rec_dir(dir); + return STATUS_OK; +} + +receiver::status receiver::set_audio_rec_sql_triggered(const bool enabled) +{ + rx->set_audio_rec_sql_triggered(enabled); + return STATUS_OK; +} + +receiver::status receiver::set_audio_rec_min_time(const int time_ms) +{ + rx->set_audio_rec_min_time(time_ms); + return STATUS_OK; +} + +receiver::status receiver::set_audio_rec_max_gap(const int time_ms) +{ + rx->set_audio_rec_max_gap(time_ms); + return STATUS_OK; +} + /** * @brief Start WAV file recorder. * @param filename The filename where to record. @@ -1105,7 +1134,7 @@ receiver::status receiver::set_amsync_pll_bw(float pll_bw) * file names does not work with WAV files (the initial /tmp/gqrx.wav will not be stopped * because the wav file can not be empty). See https://github.com/gqrx-sdr/gqrx/issues/36 */ -receiver::status receiver::start_audio_recording(const std::string filename) +receiver::status receiver::start_audio_recording() { if (d_recording_wav) { @@ -1122,32 +1151,12 @@ receiver::status receiver::start_audio_recording(const std::string filename) return STATUS_ERROR; } - // if this fails, we don't want to go and crash now, do we - try { -#if GNURADIO_VERSION < 0x030900 - wav_sink = gr::blocks::wavfile_sink::make(filename.c_str(), 2, - (unsigned int) d_audio_rate, - 16); -#else - wav_sink = gr::blocks::wavfile_sink::make(filename.c_str(), 2, - (unsigned int) d_audio_rate, - gr::blocks::FORMAT_WAV, gr::blocks::FORMAT_PCM_16); -#endif + if(rx->start_audio_recording() == 0) + { + return STATUS_OK; } - catch (std::runtime_error &e) { - std::cout << "Error opening " << filename << ": " << e.what() << std::endl; + else return STATUS_ERROR; - } - - tb->lock(); - tb->connect(rx, 0, wav_sink, 0); - tb->connect(rx, 1, wav_sink, 1); - tb->unlock(); - d_recording_wav = true; - - std::cout << "Recording audio to " << filename << std::endl; - - return STATUS_OK; } /** Stop WAV file recorder. */ @@ -1166,27 +1175,17 @@ receiver::status receiver::stop_audio_recording() return STATUS_ERROR; } - - // not strictly necessary to lock but I think it is safer - tb->lock(); - wav_sink->close(); - tb->disconnect(rx, 0, wav_sink, 0); - tb->disconnect(rx, 1, wav_sink, 1); - - // Temporary workaround for https://github.com/gnuradio/gnuradio/issues/5436 - tb->disconnect(ddc, 0, rx, 0); - tb->connect(ddc, 0, rx, 0); - // End temporary workaround - - tb->unlock(); - wav_sink.reset(); - d_recording_wav = false; - - std::cout << "Audio recorder stopped" << std::endl; + rx->stop_audio_recording(); return STATUS_OK; } +/** get last recorded audio file name. */ +std::string receiver::get_last_audio_filename() +{ + return rx->get_last_audio_filename(); +} + /** Start audio playback. */ receiver::status receiver::start_audio_playback(const std::string filename) { @@ -1491,6 +1490,7 @@ void receiver::connect_all(rx_chain type, file_formats fmt) // Visualization tb->connect(b, 0, iq_fft, 0); + receiver_base_cf_sptr old_rx = rx; // RX demod chain switch (type) { @@ -1499,6 +1499,9 @@ void receiver::connect_all(rx_chain type, file_formats fmt) { rx.reset(); rx = make_nbrx(d_quad_rate, d_audio_rate); + rx->set_rec_event_handler(std::bind(audio_rec_event, this, + std::placeholders::_1, + std::placeholders::_2)); } break; @@ -1507,6 +1510,9 @@ void receiver::connect_all(rx_chain type, file_formats fmt) { rx.reset(); rx = make_wfmrx(d_quad_rate, d_audio_rate); + rx->set_rec_event_handler(std::bind(audio_rec_event, this, + std::placeholders::_1, + std::placeholders::_2)); } break; @@ -1514,6 +1520,21 @@ void receiver::connect_all(rx_chain type, file_formats fmt) break; } + if(old_rx.get() != rx.get()) + { + //Temporary workaround for https://github.com/gnuradio/gnuradio/issues/5436 + tb->connect(ddc, 0, rx, 0); + // End temporary workaronud + rx->set_center_freq(d_rf_freq); + rx->set_offset(d_filter_offset); + rx->set_audio_rec_sql_triggered(old_rx->get_audio_rec_sql_triggered()); + rx->set_audio_rec_min_time(old_rx->get_audio_rec_min_time()); + rx->set_audio_rec_max_gap(old_rx->get_audio_rec_max_gap()); + rx->set_rec_dir(old_rx->get_rec_dir()); + //Temporary workaround for https://github.com/gnuradio/gnuradio/issues/5436 + tb->disconnect(ddc, 0, rx, 0); + // End temporary workaronud + } // Audio path (if there is a receiver) if (type != RX_CHAIN_NONE) { @@ -1528,10 +1549,10 @@ void receiver::connect_all(rx_chain type, file_formats fmt) tb->connect(rx, 1, audio_snk, 1); } // Recorders and sniffers - if (d_recording_wav) + if(old_rx.get() != rx.get()) { - tb->connect(rx, 0, wav_sink, 0); - tb->connect(rx, 1, wav_sink, 1); + if (d_recording_wav) + rx->continue_audio_recording(old_rx); } if (d_sniffer_active) @@ -1544,8 +1565,7 @@ void receiver::connect_all(rx_chain type, file_formats fmt) { if (d_recording_wav) { - wav_sink->close(); - wav_sink.reset(); + rx->stop_audio_recording(); d_recording_wav = false; } @@ -1612,3 +1632,20 @@ std::string receiver::escape_filename(std::string filename) ss2 << std::quoted(ss1.str(), '\'', '\\'); return ss2.str(); } + +void receiver::audio_rec_event(receiver * self, std::string filename, bool is_running) +{ + if (is_running) + { + self->d_recording_wav = true; + std::cout << "Recording audio to " << filename << std::endl; + } + else + { + self->d_recording_wav = false; + std::cout << "Audio recorder stopped" << std::endl; + } + + if(self->d_audio_rec_event_handler) + self->d_audio_rec_event_handler(filename, is_running); +} diff --git a/src/applications/gqrx/receiver.h b/src/applications/gqrx/receiver.h index 275cc5504..b2128afc6 100644 --- a/src/applications/gqrx/receiver.h +++ b/src/applications/gqrx/receiver.h @@ -32,7 +32,6 @@ //#include //#include #include -#include #include #include #include @@ -124,6 +123,8 @@ class receiver size_t file_pos; }; + typedef std::function audio_rec_event_handler_t; + receiver(const std::string input_device="", const std::string audio_device="", unsigned int decimation=1); @@ -224,8 +225,13 @@ class receiver status set_amsync_pll_bw(float pll_bw); /* Audio parameters */ - status start_audio_recording(const std::string filename); + status set_audio_rec_dir(const std::string dir); + status set_audio_rec_sql_triggered(const bool enabled); + status set_audio_rec_min_time(const int time_ms); + status set_audio_rec_max_gap(const int time_ms); + status start_audio_recording(); status stop_audio_recording(); + std::string get_last_audio_filename(); status start_audio_playback(const std::string filename); status stop_audio_playback(); @@ -256,6 +262,10 @@ class receiver /* utility functions */ static std::string escape_filename(std::string filename); + template void set_audio_rec_event_handler(T handler) + { + d_audio_rec_event_handler = handler; + } private: void connect_all(rx_chain type, file_formats fmt); @@ -351,7 +361,6 @@ class receiver gr::blocks::throttle::sptr input_throttle; 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. */ gr::blocks::null_sink::sptr audio_null_sink0; /*!< Audio null sink used during playback. */ gr::blocks::null_sink::sptr audio_null_sink1; /*!< Audio null sink used during playback. */ @@ -367,9 +376,11 @@ class receiver #else gr::audio::sink::sptr audio_snk; /*!< gr audio sink */ #endif - + audio_rec_event_handler_t d_audio_rec_event_handler; //! Get a path to a file containing random bytes static std::string get_zero_file(void); + static void audio_rec_event(receiver * self, std::string filename, + bool is_running); }; #endif // RECEIVER_H diff --git a/src/applications/gqrx/remote_control.cpp b/src/applications/gqrx/remote_control.cpp index 05f27e5c9..34d228e29 100644 --- a/src/applications/gqrx/remote_control.cpp +++ b/src/applications/gqrx/remote_control.cpp @@ -369,7 +369,7 @@ void RemoteControl::setAudioGain(float gain) } /*! \brief Start audio recorder (from mainwindow). */ -void RemoteControl::startAudioRecorder(QString unused) +void RemoteControl::startAudioRecorder() { if (rc_mode > 0) audio_recorder_status = true; diff --git a/src/applications/gqrx/remote_control.h b/src/applications/gqrx/remote_control.h index 12d2619c2..1a236e973 100644 --- a/src/applications/gqrx/remote_control.h +++ b/src/applications/gqrx/remote_control.h @@ -93,7 +93,7 @@ public slots: void setPassband(int passband_lo, int passband_hi); void setSquelchLevel(double level); void setAudioGain(float gain); - void startAudioRecorder(QString unused); + void startAudioRecorder(); void stopAudioRecorder(); bool setGain(QString name, double gain); void setRDSstatus(bool enabled); diff --git a/src/dsp/CMakeLists.txt b/src/dsp/CMakeLists.txt index ea48f99f8..28c319c64 100644 --- a/src/dsp/CMakeLists.txt +++ b/src/dsp/CMakeLists.txt @@ -49,4 +49,6 @@ add_source_files(SRCS_LIST stereo_demod.h format_converter.h format_converter.cpp + rx_squelch.cpp + rx_squelch.h ) diff --git a/src/dsp/rx_squelch.cpp b/src/dsp/rx_squelch.cpp new file mode 100644 index 000000000..0bc2bb3df --- /dev/null +++ b/src/dsp/rx_squelch.cpp @@ -0,0 +1,122 @@ +/* -*- c++ -*- */ +/* + * Gqrx SDR: Software defined radio receiver powered by GNU Radio and Qt + * https://gqrx.dk/ + * + * Copyright 2012-2013 Alexandru Csete OZ9AEC. + * + * 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 "dsp/rx_squelch.h" + +rx_sql_cc_sptr make_rx_sql_cc(double db, double alpha) +{ + return gnuradio::get_initial_sptr(new rx_sql_cc(db, alpha)); +} + + +rx_sql_cc::rx_sql_cc(double db, double alpha) + : gr::hier_block2 ("rx_sql_cc", + gr::io_signature::make(1, 1, sizeof(gr_complex)), + gr::io_signature::make(1, 1, sizeof(gr_complex))) +{ + d_impl = SQL_SIMPLE; + sql_pwr = gr::analog::pwr_squelch_cc::make(db, alpha); + sql_simple = gr::analog::simple_squelch_cc::make(db, alpha); + connect(self(), 0, sql_simple, 0); + connect(sql_simple, 0, self(), 0); +} + +rx_sql_cc::~rx_sql_cc() +{ +} + +double rx_sql_cc::threshold() +{ + switch (d_impl) + { + case SQL_SIMPLE: + return sql_simple->threshold(); + case SQL_PWR: + return sql_pwr->threshold(); + } + return 0.0; +} + +void rx_sql_cc::set_threshold(double db) +{ + sql_simple->set_threshold(db); + sql_pwr->set_threshold(db); +} + +void rx_sql_cc::set_alpha(double alpha) +{ + sql_simple->set_alpha(alpha); + sql_pwr->set_alpha(alpha); +} + +bool rx_sql_cc::unmuted() +{ + switch (d_impl) + { + case SQL_SIMPLE: + return sql_simple->unmuted(); + case SQL_PWR: + return sql_pwr->unmuted(); + } + return false; +} + +void rx_sql_cc::set_impl(rx_sql_cc::sql_impl_t impl) +{ + if(d_impl == impl) + return; + lock(); + switch (d_impl) + { + case SQL_SIMPLE: + disconnect(self(), 0, sql_simple, 0); + disconnect(sql_simple, 0, self(), 0); + break; + case SQL_PWR: + disconnect(self(), 0, sql_pwr, 0); + disconnect(sql_pwr, 0, self(), 0); + break; + } + switch (impl) + { + case SQL_SIMPLE: + connect(self(), 0, sql_simple, 0); + connect(sql_simple, 0, self(), 0); + break; + case SQL_PWR: + connect(self(), 0, sql_pwr, 0); + connect(sql_pwr, 0, self(), 0); + break; + } + unlock(); + d_impl = impl; +} + +rx_sql_cc::sql_impl_t rx_sql_cc::get_impl() +{ + return d_impl; +} + diff --git a/src/dsp/rx_squelch.h b/src/dsp/rx_squelch.h new file mode 100644 index 000000000..95f27efe9 --- /dev/null +++ b/src/dsp/rx_squelch.h @@ -0,0 +1,78 @@ +/* -*- c++ -*- */ +/* + * Gqrx SDR: Software defined radio receiver powered by GNU Radio and Qt + * https://gqrx.dk/ + * + * Copyright 2012-2013 Alexandru Csete OZ9AEC. + * + * 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 RX_SQUELCH_CC_H +#define RX_SQUELCH_CC_H + +#include +#include +#include +#include + + +class rx_sql_cc; + +#if GNURADIO_VERSION < 0x030900 +typedef boost::shared_ptr rx_sql_cc_sptr; +#else +typedef std::shared_ptr rx_sql_cc_sptr; +#endif + +/*! \brief Return a shared_ptr to a new instance of rx_sql_cc. + * \param db threshold (in dB) for squelch + * \param alpha Gain of averaging filter. Defaults to 0.0001. + */ +rx_sql_cc_sptr make_rx_sql_cc(double db, double alpha = 0.0001); + +/*! \brief Squelch implementation switching block. + * \ingroup DSP + * + * This block allows to select between simple_squelch and pwr_squelch blocks + */ +class rx_sql_cc : public gr::hier_block2 +{ + friend rx_sql_cc_sptr make_rx_sql_cc(double db, double alpha); + +protected: + rx_sql_cc(double db, double alpha); + +public: + typedef enum{ + SQL_SIMPLE = 0, + SQL_PWR + } sql_impl_t; + + ~rx_sql_cc(); + double threshold(); + void set_threshold(double db); + void set_alpha(double alpha); + bool unmuted(); + void set_impl(sql_impl_t impl); + sql_impl_t get_impl(); + +private: + gr::analog::pwr_squelch_cc::sptr sql_pwr; /*!< Pwr Squelch (squelch-triggered recording mode). */ + gr::analog::simple_squelch_cc::sptr sql_simple; /*!< Simple Squelch (generic mode). */ + sql_impl_t d_impl; +}; + +#endif /* RX_SQUELCH_CC_H */ diff --git a/src/interfaces/CMakeLists.txt b/src/interfaces/CMakeLists.txt index 85f54f2ea..e0cbf6d3a 100644 --- a/src/interfaces/CMakeLists.txt +++ b/src/interfaces/CMakeLists.txt @@ -7,4 +7,6 @@ add_source_files(SRCS_LIST file_sink.h file_source.cpp file_source.h + wav_sink.cpp + wav_sink.h ) diff --git a/src/interfaces/wav_sink.cpp b/src/interfaces/wav_sink.cpp new file mode 100644 index 000000000..158bb6663 --- /dev/null +++ b/src/interfaces/wav_sink.cpp @@ -0,0 +1,525 @@ +/* -*- c++ -*- */ +/* + * Gqrx SDR: Software defined radio receiver powered by GNU Radio and Qt + * https://gqrx.dk/ + * + * Copyright 2008,2009,2013 Free Software Foundation, Inc. + * Copyright 2022 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 "wav_sink.h" +#include +#include +#include +#include +#include +#include + +static const int SQL_REC_MIN_TIME = 10; /* Minimum squelch recorder time, seconds. */ +static const int SQL_REC_MAX_GAP = 10; /* Maximum squelch recorder gap, seconds. */ + +wavfile_sink_gqrx::sptr wavfile_sink_gqrx::make(const char* filename, + int n_channels, + unsigned int sample_rate, + wavfile_format_t format, + wavfile_subformat_t subformat, + bool append) +{ + return gnuradio::get_initial_sptr(new wavfile_sink_gqrx( + filename, n_channels, sample_rate, format, subformat, append)); +} + +wavfile_sink_gqrx::wavfile_sink_gqrx(const char* filename, + int n_channels, + unsigned int sample_rate, + wavfile_format_t format, + wavfile_subformat_t subformat, + bool append) + : sync_block("wavfile_sink_gqrx", + gr::io_signature::make(1, n_channels, sizeof(float)), + gr::io_signature::make(0, 0, 0)), + d_h{}, // Init with zeros + d_append(append), + d_fp(nullptr), + d_new_fp(nullptr), + d_updated(false), + d_center_freq(0), + d_offset(0), + d_rec_dir(""), + d_squelch_triggered(false), + d_min_time_ms(0), + d_max_gap_ms(0), + d_min_time_samp(0), + d_max_gap_samp(0), + d_prev_action(ACT_NONE), + d_prev_roffset(0) +{ + int bits_per_sample = 16; + + if (n_channels > s_max_channels) + throw std::runtime_error("Number of channels greater than " + + std::to_string(s_max_channels) + " not supported."); + + d_h.sample_rate = sample_rate; + d_h.nchans = n_channels; + d_h.format = format; + d_h.subformat = subformat; + switch (subformat) { + case FORMAT_PCM_S8: + bits_per_sample = 8; + break; + case FORMAT_PCM_16: + bits_per_sample = 16; + break; + case FORMAT_PCM_24: + bits_per_sample = 24; + break; + case FORMAT_PCM_32: + bits_per_sample = 32; + break; + case FORMAT_PCM_U8: + bits_per_sample = 8; + break; + case FORMAT_FLOAT: + bits_per_sample = 32; + break; + case FORMAT_DOUBLE: + bits_per_sample = 64; + break; + case FORMAT_VORBIS: + bits_per_sample = 32; + break; + } + set_bits_per_sample_unlocked(bits_per_sample); + d_h.bytes_per_sample = d_bytes_per_sample_new; + + set_max_noutput_items(s_items_size); + d_buffer.resize(s_items_size * d_h.nchans); + + if (filename) + if (!open(filename)) + throw std::runtime_error("Can't open WAV file."); + //FIXME Make this configurable? + d_sob_key = pmt::intern("squelch_sob"); + d_eob_key = pmt::intern("squelch_eob"); + set_history(1 + sample_rate * (SQL_REC_MIN_TIME + SQL_REC_MAX_GAP)); +} + +void wavfile_sink_gqrx::set_center_freq(double center_freq) +{ + std::unique_lock guard(d_mutex); + d_center_freq = center_freq; +} + +void wavfile_sink_gqrx::set_offset(double offset) +{ + std::unique_lock guard(d_mutex); + d_offset = offset; +} + +void wavfile_sink_gqrx::set_rec_dir(std::string dir) +{ + std::unique_lock guard(d_mutex); + d_rec_dir = dir; +} + + +bool wavfile_sink_gqrx::open(const char* filename) +{ + std::unique_lock guard(d_mutex); + return open_unlocked(filename); +} + +bool wavfile_sink_gqrx::open_unlocked(const char* filename) +{ + SF_INFO sfinfo; + + if (d_new_fp) { // if we've already got a new one open, close it + sf_close(d_new_fp); + d_new_fp = nullptr; + } + + if (d_append) { + // We are appending to an existing file, be extra careful here. + sfinfo.format = 0; + if (!(d_new_fp = sf_open(filename, SFM_RDWR, &sfinfo))) { + std::cerr << "sf_open failed: " << filename << " " << strerror(errno) << std::endl; + return false; + } + if (d_h.sample_rate != sfinfo.samplerate || d_h.nchans != sfinfo.channels || + d_h.format != (sfinfo.format & SF_FORMAT_TYPEMASK) || + d_h.subformat != (sfinfo.format & SF_FORMAT_SUBMASK)) { + std::cerr << "Existing WAV file is incompatible with configured options."< guard(d_mutex); + return open_new_unlocked(); +} + +int wavfile_sink_gqrx::open_new_unlocked() +{ + // FIXME: option to use local time + // use toUTC() function compatible with older versions of Qt. + QString file_name = QDateTime::currentDateTime().toUTC().toString("gqrx_yyyyMMdd_hhmmss"); + QString filename = QString("%1/%2_%3.wav").arg(QString(d_rec_dir.data())).arg(file_name).arg(qint64(d_center_freq + d_offset)); + if (open_unlocked(filename.toStdString().data())) + { + if (d_rec_event) + d_rec_event(d_filename = filename.toStdString(), true); + return 0; + } + return 1; +} + +void wavfile_sink_gqrx::close() +{ + std::unique_lock guard(d_mutex); + + if (!d_fp) + return; + close_wav(); +} + +void wavfile_sink_gqrx::close_wav() +{ + sf_write_sync(d_fp); + sf_close(d_fp); + d_fp = nullptr; + if (d_rec_event) + d_rec_event(d_filename, false); +} + +wavfile_sink_gqrx::~wavfile_sink_gqrx() +{ + set_rec_event_handler(nullptr); + if (d_new_fp) { + sf_close(d_new_fp); + d_new_fp = nullptr; + } + close(); + +} + +bool wavfile_sink_gqrx::stop() +{ + if (d_fp) + sf_write_sync(d_fp); + return true; +} + +int wavfile_sink_gqrx::work(int noutput_items, + gr_vector_const_void_star& input_items, + gr_vector_void_star& output_items) +{ + auto in = (float**)&input_items[0]; + int n_in_chans = input_items.size(); + int hist = history() - 1; + int nwritten = hist; + int writecount = noutput_items; + std::vector work_tags; + std::unique_lock guard(d_mutex); // hold mutex for duration of this block + int roffset = 0; /** relative offset*/ + + + if (d_squelch_triggered) + { + uint64_t abs_N = nitems_read(0); + get_tags_in_window(work_tags, 0, 0, noutput_items); + for (const auto& tag : work_tags) + { + roffset = (tag.offset - abs_N); + if (tag.key == d_sob_key) + { + if (d_prev_action == ACT_CLOSE) + { + if (roffset + hist - d_prev_roffset <= d_max_gap_samp) + { + if (d_fp) + { + writeout(d_prev_roffset, roffset + hist - d_prev_roffset, n_in_chans, in); + nwritten = roffset + hist; + writecount = noutput_items - roffset; + } + d_prev_action = ACT_NONE; + } + else + { + if (d_fp) + close_wav(); + } + } + d_prev_roffset = roffset + hist; + if (!d_fp) + d_prev_action = ACT_OPEN; + } + if (tag.key == d_eob_key) + { + if (d_prev_action == ACT_OPEN) + { + if (!d_fp && (roffset + hist - d_prev_roffset >= d_min_time_samp)) + { + open_new_unlocked(); + do_update(); + if (d_fp) + writeout(d_prev_roffset, roffset + hist - d_prev_roffset, n_in_chans, in); + } + } + if (d_fp) + d_prev_action = ACT_CLOSE; + else + d_prev_action = ACT_NONE; + d_prev_roffset = roffset + hist; + } + } + } + switch(d_prev_action) + { + case ACT_NONE: + do_update(); // update: d_fp is read + if (d_fp && writecount) + writeout(nwritten, writecount, n_in_chans, in); + break; + case ACT_OPEN: + if (hist - d_prev_roffset >= d_min_time_samp) + { + d_prev_action = ACT_NONE; + if (!d_fp) + { + open_new_unlocked(); + do_update(); + if (d_fp) + writeout(d_prev_roffset, hist - d_prev_roffset + writecount, n_in_chans, in); + } + } + break; + case ACT_CLOSE: + if (hist - d_prev_roffset >= d_max_gap_samp) + { + if (d_fp) + { + close_wav(); + } + d_prev_action = ACT_NONE; + } + break; + } + d_prev_roffset -= noutput_items; + if (d_prev_roffset < 0) + d_prev_roffset = 0; + return noutput_items; +} + +void wavfile_sink_gqrx::writeout(const int offset, const int writecount, const int n_in_chans, float** in) +{ + int nchans = d_h.nchans; + int nwritten = 0; + int bp = 0; + int errnum; + while(nwritten < writecount) + { + for (bp = 0; (nwritten < writecount) && (bp < s_items_size); nwritten++, bp++) + { + for (int chan = 0; chan < nchans; chan++) + { + // Write zeros to channels which are in the WAV file + // but don't have any inputs here + if (chan < n_in_chans) + d_buffer[chan + (bp * nchans)] = in[chan][nwritten + offset]; + else + d_buffer[chan + (bp * nchans)] = 0; + } + } + sf_write_float(d_fp, &d_buffer[0], nchans * bp); + + errnum = sf_error(d_fp); + if (errnum) { + std::cerr << "sf_error: " << sf_error_number(errnum) << std::endl; + close(); + throw std::runtime_error("File I/O error."); + } + } +} + +void wavfile_sink_gqrx::set_sql_triggered(const bool enabled) +{ + std::unique_lock guard(d_mutex); + d_squelch_triggered = enabled; + d_prev_action = ACT_NONE; +} + +void wavfile_sink_gqrx::set_bits_per_sample(int bits_per_sample) +{ + std::unique_lock guard(d_mutex); + set_bits_per_sample_unlocked(bits_per_sample); +} + +void wavfile_sink_gqrx::set_bits_per_sample_unlocked(int bits_per_sample) +{ + d_bytes_per_sample_new = bits_per_sample / 8; +} + +void wavfile_sink_gqrx::set_append(bool append) +{ + std::unique_lock guard(d_mutex); + d_append = append; +} + +void wavfile_sink_gqrx::set_sample_rate(unsigned int sample_rate) +{ + std::unique_lock guard(d_mutex); + d_h.sample_rate = sample_rate; +} + +int wavfile_sink_gqrx::bits_per_sample() { return d_bytes_per_sample_new; } + +unsigned int wavfile_sink_gqrx::sample_rate() { return d_h.sample_rate; } + +void wavfile_sink_gqrx::do_update() +{ + if (!d_updated) + return; + + if (d_fp) + close_wav(); + + d_fp = d_new_fp; // install new file pointer + d_new_fp = nullptr; + + d_h.bytes_per_sample = d_bytes_per_sample_new; + // Avoid deadlock. + set_bits_per_sample_unlocked(8 * d_bytes_per_sample_new); + d_updated = false; +} + +void wavfile_sink_gqrx::set_rec_min_time(int min_time_ms) +{ + std::unique_lock guard(d_mutex); + d_min_time_ms = min_time_ms; + d_min_time_samp = d_min_time_ms * d_h.sample_rate / 1000; +/* int new_history = 1 + (d_min_time_ms + d_max_gap_ms) * d_h.sample_rate / 1000; + if (int(history()) < new_history) + set_history(new_history);*/ +} + +void wavfile_sink_gqrx::set_rec_max_gap(int max_gap_ms) +{ + std::unique_lock guard(d_mutex); + d_max_gap_ms = max_gap_ms; + d_max_gap_samp = max_gap_ms * d_h.sample_rate / 1000; +/* int new_history = 1 + (d_min_time_ms + d_max_gap_ms) * d_h.sample_rate / 1000; + if (int(history()) < new_history) + set_history(new_history);*/ +} + +int wavfile_sink_gqrx::get_min_time() +{ + return d_min_time_ms; +} + +int wavfile_sink_gqrx::get_max_gap() +{ + return d_max_gap_ms; +} diff --git a/src/interfaces/wav_sink.h b/src/interfaces/wav_sink.h new file mode 100644 index 000000000..ff8da03a2 --- /dev/null +++ b/src/interfaces/wav_sink.h @@ -0,0 +1,192 @@ +/* -*- c++ -*- */ +/* + * Gqrx SDR: Software defined radio receiver powered by GNU Radio and Qt + * https://gqrx.dk/ + * + * Copyright 2008,2009,2013 Free Software Foundation, Inc. + * Copyright 2022 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_WAVFILE_SINK_C_H +#define GQRX_WAVFILE_SINK_C_H + +#include +#include // for SNDFILE +#include +#include + +class wavfile_sink_gqrx : virtual public gr::sync_block +{ +public: + typedef std::function rec_event_handler_t; +#if GNURADIO_VERSION < 0x030900 + typedef boost::shared_ptr sptr; +#else + typedef std::shared_ptr sptr; +#endif + + enum wavfile_format_t { + FORMAT_WAV = 0x010000, + FORMAT_FLAC = 0x170000, + FORMAT_OGG = 0x200000, + FORMAT_RF64 = 0x220000, + }; + + enum wavfile_subformat_t { + FORMAT_PCM_S8 = 1, + FORMAT_PCM_16, + FORMAT_PCM_24, + FORMAT_PCM_32, + FORMAT_PCM_U8, + FORMAT_FLOAT, + FORMAT_DOUBLE, + FORMAT_VORBIS = 0x0060, + }; + +private: + //! WAV file header information. + struct wav_header_info { + + //! sample rate [S/s] + int sample_rate; + + //! Number of channels + int nchans; + + //! Bytes per sample + int bytes_per_sample; + + //! Number of samples per channel + long long samples_per_chan; + + //! sndfile format + int format; + + //! sndfile format + int subformat; + }; + + + typedef enum{ + ACT_NONE=0, + ACT_OPEN, + ACT_CLOSE + } sql_action; + wav_header_info d_h; + int d_bytes_per_sample_new; + bool d_append; + + std::vector d_buffer; + + SNDFILE* d_fp; + SNDFILE* d_new_fp; + bool d_updated; + std::mutex d_mutex; + double d_center_freq; + double d_offset; + std::string d_rec_dir; + std::string d_filename; + + static constexpr int s_items_size = 8192; + static constexpr int s_max_channels = 24; + + rec_event_handler_t d_rec_event; + bool d_squelch_triggered; + pmt::pmt_t d_sob_key, d_eob_key; + int d_min_time_ms; + int d_max_gap_ms; + int d_min_time_samp; + int d_max_gap_samp; + sql_action d_prev_action; + int d_prev_roffset; + + /*! + * \brief If any file changes have occurred, update now. This is called + * internally by work() and thus doesn't usually need to be called by + * hand. + */ + void do_update(); + + /*! + * \brief Implementation of set_bits_per_sample without mutex lock. + */ + void set_bits_per_sample_unlocked(int bits_per_sample); + + /*! + * \brief Writes information to the WAV header which is not available + * a-priori (chunk size etc.) and closes the file. Not thread-safe and + * assumes d_fp is a valid file pointer, should thus only be called by + * other methods. + */ + void close_wav(); + +protected: + bool stop() override; + +public: + static sptr make(const char* filename, + int n_channels, + unsigned int sample_rate, + wavfile_format_t format, + wavfile_subformat_t subformat, + bool append = false); + wavfile_sink_gqrx(const char* filename, + int n_channels, + unsigned int sample_rate, + wavfile_format_t format, + wavfile_subformat_t subformat, + bool append = false); + ~wavfile_sink_gqrx() override; + + virtual void set_center_freq(double center_freq); + virtual void set_offset(double offset); + virtual void set_rec_dir(std::string dir); + template void set_rec_event_handler(T handler) + { + d_rec_event = handler; + } + bool open(const char* filename); + int open_new(); + void close(); + + void set_sample_rate(unsigned int sample_rate); + void set_bits_per_sample(int bits_per_sample); + + void set_append(bool append); + + int bits_per_sample(); + unsigned int sample_rate(); + + int work(int noutput_items, + gr_vector_const_void_star& input_items, + gr_vector_void_star& output_items) override; + void set_sql_triggered(const bool enabled); + bool get_sql_triggered() { return d_squelch_triggered; } + void set_rec_min_time(int min_time_ms); + int get_rec_min_time() { return d_min_time_ms; } + void set_rec_max_gap(int max_gap_ms); + int get_rec_max_gap() { return d_max_gap_ms; } + int get_min_time(); + int get_max_gap(); +private: + bool open_unlocked(const char* filename); + int open_new_unlocked(); + void writeout(const int offset, const int writecount, const int n_in_chans, float** in); +}; + +#endif /* GQRX_WAVFILE_SINK_C_H */ diff --git a/src/qtgui/audio_options.cpp b/src/qtgui/audio_options.cpp index 403e467b3..f4b2319b8 100644 --- a/src/qtgui/audio_options.cpp +++ b/src/qtgui/audio_options.cpp @@ -96,6 +96,20 @@ void CAudioOptions::setUdpStereo(bool stereo) ui->udpStereo->setChecked(stereo); } +void CAudioOptions::setSquelchTriggered(bool value) +{ + ui->squelchTriggered->setChecked(value); +} + +void CAudioOptions::setRecMinTime(int time_ms) +{ + ui->recMinTime->setValue(time_ms); +} + +void CAudioOptions::setRecMaxGap(int time_ms) +{ + ui->recMaxGap->setValue(time_ms); +} void CAudioOptions::setFftSplit(int pct_2d) { @@ -212,6 +226,30 @@ void CAudioOptions::on_recDirEdit_textChanged(const QString &dir) } } +/** + * Slot called when the squelch-triggered mode gets disabled/enabled + */ +void CAudioOptions::on_squelchTriggered_stateChanged(int state) +{ + emit newSquelchTriggered(state == Qt::Checked); +} + +/** + * Slot called when the squelch-triggered recording min time gets changed + */ +void CAudioOptions::on_recMinTime_valueChanged(int value) +{ + emit newRecMinTime(value); +} + +/** + * Slot called when the squelch-triggered recording max gap gets changed + */ +void CAudioOptions::on_recMaxGap_valueChanged(int value) +{ + emit newRecMaxGap(value); +} + /** Slot called when the user clicks on the "Select" button. */ void CAudioOptions::on_recDirButton_clicked() { diff --git a/src/qtgui/audio_options.h b/src/qtgui/audio_options.h index 114103146..d704e8023 100644 --- a/src/qtgui/audio_options.h +++ b/src/qtgui/audio_options.h @@ -47,6 +47,9 @@ class CAudioOptions : public QDialog void setUdpHost(const QString &host); void setUdpPort(int port); void setUdpStereo(bool stereo); + void setSquelchTriggered(bool value); + void setRecMinTime(int time_ms); + void setRecMaxGap(int time_ms); void setFftSplit(int pct_2d); int getFftSplit(void) const; @@ -74,6 +77,9 @@ public slots: void newUdpHost(const QString text); void newUdpPort(int port); void newUdpStereo(bool enabled); + void newSquelchTriggered(bool enabled); + void newRecMinTime(int time_ms); + void newRecMaxGap(int time_ms); private slots: void on_fftSplitSlider_valueChanged(int value); @@ -85,6 +91,9 @@ private slots: void on_udpHost_textChanged(const QString &text); void on_udpPort_valueChanged(int port); void on_udpStereo_stateChanged(int state); + void on_squelchTriggered_stateChanged(int state); + void on_recMinTime_valueChanged(int value); + void on_recMaxGap_valueChanged(int value); private: Ui::CAudioOptions *ui; /*!< The user interface widget. */ diff --git a/src/qtgui/audio_options.ui b/src/qtgui/audio_options.ui index ab5841350..2a9fd32f2 100644 --- a/src/qtgui/audio_options.ui +++ b/src/qtgui/audio_options.ui @@ -7,7 +7,7 @@ 0 0 315 - 182 + 209 @@ -21,7 +21,7 @@ - 0 + 1 @@ -206,17 +206,76 @@ - - - Qt::Vertical + + + Squelch-triggered recording mode - - - 20 - 40 - + + Squelch triggered - + + + + + + + + Min time + + + + + + + + 0 + 0 + + + + Minimum squelch open time to triggere recording start + + + ms + + + 10000 + + + 100 + + + + + + + Max gap + + + + + + + + 0 + 0 + + + + Maximum squelch closed time to keep recording running + + + ms + + + 10000 + + + 100 + + + + diff --git a/src/qtgui/dockaudio.cpp b/src/qtgui/dockaudio.cpp index 7e718adcc..91faf4f32 100644 --- a/src/qtgui/dockaudio.cpp +++ b/src/qtgui/dockaudio.cpp @@ -47,6 +47,9 @@ DockAudio::DockAudio(QWidget *parent) : connect(audioOptions, SIGNAL(newUdpHost(QString)), this, SLOT(setNewUdpHost(QString))); connect(audioOptions, SIGNAL(newUdpPort(int)), this, SLOT(setNewUdpPort(int))); connect(audioOptions, SIGNAL(newUdpStereo(bool)), this, SLOT(setNewUdpStereo(bool))); + connect(audioOptions, SIGNAL(newSquelchTriggered(bool)), this, SLOT(setNewSquelchTriggered(bool))); + connect(audioOptions, SIGNAL(newRecMinTime(int)), this, SLOT(setRecMinTime(int))); + connect(audioOptions, SIGNAL(newRecMaxGap(int)), this, SLOT(setRecMaxGap(int))); connect(ui->audioSpectrum, SIGNAL(pandapterRangeChanged(float,float)), audioOptions, SLOT(setPandapterSliderValues(float,float))); @@ -141,32 +144,9 @@ void DockAudio::setFftFill(bool enabled) ui->audioSpectrum->setFftFill(enabled); } -/*! Public slot to trig audio recording by external events (e.g. satellite AOS). - * - * If a recording is already in progress we ignore the event. - */ -void DockAudio::startAudioRecorder(void) -{ - if (ui->audioRecButton->isChecked()) - { - qDebug() << __func__ << "An audio recording is already in progress"; - return; - } - - // emulate a button click - ui->audioRecButton->click(); -} - -/*! Public slot to stop audio recording by external events (e.g. satellite LOS). - * - * The event is ignored if no audio recording is in progress. - */ -void DockAudio::stopAudioRecorder(void) +bool DockAudio::getSquelchTriggered() { - if (ui->audioRecButton->isChecked()) - ui->audioRecButton->click(); // emulate a button click - else - qDebug() << __func__ << "No audio recording in progress"; + return squelch_triggered; } /*! Public slot to set new RX frequency in Hz. */ @@ -212,25 +192,12 @@ void DockAudio::on_audioStreamButton_clicked(bool checked) void DockAudio::on_audioRecButton_clicked(bool checked) { if (checked) { - // FIXME: option to use local time - // use toUTC() function compatible with older versions of Qt. - QString file_name = QDateTime::currentDateTime().toUTC().toString("gqrx_yyyyMMdd_hhmmss"); - last_audio = QString("%1/%2_%3.wav").arg(rec_dir).arg(file_name).arg(rx_freq); - QFileInfo info(last_audio); // emit signal and start timer - emit audioRecStarted(last_audio); - - ui->audioRecLabel->setText(info.fileName()); - ui->audioRecButton->setToolTip(tr("Stop audio recorder")); - ui->audioPlayButton->setEnabled(false); /* prevent playback while recording */ + emit audioRecStart(); } else { - ui->audioRecLabel->setText("DSP"); - ui->audioRecButton->setToolTip(tr("Start audio recorder")); - emit audioRecStopped(); - - ui->audioPlayButton->setEnabled(true); + emit audioRecStop(); } } @@ -375,6 +342,21 @@ void DockAudio::saveSettings(QSettings *settings) else settings->remove("udp_stereo"); + if (squelch_triggered != false) + settings->setValue("squelch_triggered_recording", squelch_triggered); + else + settings->remove("squelch_triggered_recording"); + + if(recMinTime != 0) + settings->setValue("rec_min_time", recMinTime); + else + settings->remove("rec_min_time"); + + if(recMaxGap != 0) + settings->setValue("rec_max_gap", recMaxGap); + else + settings->remove("rec_max_gap"); + settings->endGroup(); } @@ -430,6 +412,18 @@ void DockAudio::readSettings(QSettings *settings) audioOptions->setUdpPort(udp_port); audioOptions->setUdpStereo(udp_stereo); + squelch_triggered = settings->value("squelch_triggered_recording", false).toBool(); + audioOptions->setSquelchTriggered(squelch_triggered); + + recMinTime = settings->value("rec_min_time", 0).toInt(&conv_ok); + if (!conv_ok) + recMinTime = 0; + audioOptions->setRecMinTime(recMinTime); + recMaxGap = settings->value("rec_max_gap", 0).toInt(&conv_ok); + if (!conv_ok) + recMaxGap = 0; + audioOptions->setRecMaxGap(recMaxGap); + settings->endGroup(); } @@ -449,6 +443,7 @@ void DockAudio::setNewWaterfallRange(int min, int max) void DockAudio::setNewRecDir(const QString &dir) { rec_dir = dir; + emit recDirChanged(dir); } /*! \brief Slot called when a new network host has been entered. */ @@ -472,6 +467,46 @@ void DockAudio::setNewUdpStereo(bool enabled) udp_stereo = enabled; } +/*! \brief Slot called when audio recording is started after clicking rec or being triggered by squelch. */ +void DockAudio::audioRecStarted(const QString filename) +{ + last_audio = filename; + QFileInfo info(last_audio); + ui->audioRecLabel->setText(info.fileName()); + ui->audioRecButton->setToolTip(tr("Stop audio recorder")); + ui->audioPlayButton->setEnabled(false); /* prevent playback while recording */ + setAudioRecButtonState(true); +} + +void DockAudio::audioRecStopped() +{ + ui->audioRecLabel->setText("DSP"); + ui->audioRecButton->setToolTip(tr("Start audio recorder")); + ui->audioPlayButton->setEnabled(true); + setAudioRecButtonState(false); +} + + +void DockAudio::setNewSquelchTriggered(bool enabled) +{ + squelch_triggered = enabled; + ui->audioRecButton->setStyleSheet(enabled?"color: rgb(255,0,0)":""); + emit recSquelchTriggeredChanged(enabled); +} + +void DockAudio::setRecMinTime(int time_ms) +{ + recMinTime = time_ms; + emit recMinTimeChanged(time_ms); +} + +void DockAudio::setRecMaxGap(int time_ms) +{ + recMaxGap = time_ms; + emit recMaxGapChanged(time_ms); +} + + void DockAudio::recordToggleShortcut() { ui->audioRecButton->click(); } diff --git a/src/qtgui/dockaudio.h b/src/qtgui/dockaudio.h index efdbd4318..6ae277c59 100644 --- a/src/qtgui/dockaudio.h +++ b/src/qtgui/dockaudio.h @@ -65,14 +65,16 @@ class DockAudio : public QDockWidget void setFftColor(QColor color); void setFftFill(bool enabled); + bool getSquelchTriggered(); + void saveSettings(QSettings *settings); void readSettings(QSettings *settings); public slots: - void startAudioRecorder(void); - void stopAudioRecorder(void); void setRxFrequency(qint64 freq); void setWfColormap(const QString &cmap); + void audioRecStarted(const QString filename); + void audioRecStopped(); signals: /*! \brief Signal emitted when audio gain has changed. Gain is in dB. */ @@ -85,10 +87,10 @@ public slots: void audioStreamingStopped(); /*! \brief Signal emitted when audio recording is started. */ - void audioRecStarted(const QString filename); + void audioRecStart(); /*! \brief Signal emitted when audio recording is stopped. */ - void audioRecStopped(); + void audioRecStop(); /*! \brief Signal emitted when audio playback is started. */ void audioPlayStarted(const QString filename); @@ -102,6 +104,18 @@ public slots: /*! \brief Signal emitted when audio mute has changed. */ void audioMuteChanged(bool mute); + /*! \brief Signal emitted when recording directory has changed. */ + void recDirChanged(const QString dir); + + /*! \brief Signal emitted when squelch triggered recording mode is changed. */ + void recSquelchTriggeredChanged(const bool enabled); + + /*! \brief Signal emitted when squelch triggered recording min time is changed. */ + void recMinTimeChanged(int time_ms); + + /*! \brief Signal emitted when squelch triggered recording max gap time is changed. */ + void recMaxGapChanged(int time_ms); + private slots: void on_audioGainSlider_valueChanged(int value); void on_audioStreamButton_clicked(bool checked); @@ -115,6 +129,9 @@ private slots: void setNewUdpHost(const QString &host); void setNewUdpPort(int port); void setNewUdpStereo(bool enabled); + void setNewSquelchTriggered(bool enabled); + void setRecMinTime(int time_ms); + void setRecMaxGap(int time_ms); private: @@ -126,6 +143,9 @@ private slots: QString udp_host; /*! UDP client host name. */ int udp_port; /*! UDP client port number. */ bool udp_stereo; /*! Enable stereo streaming for UDP. */ + bool squelch_triggered; /*! Enable squelch-triggered recording */ + int recMinTime; /*! Minimum squelch-triggered recording time */ + int recMaxGap; /*! Maximum gap time in squelch-triggered mode*/ bool autoSpan; /*! Whether to allow mode-dependent auto span. */ diff --git a/src/receivers/receiver_base.cpp b/src/receivers/receiver_base.cpp index 4a51a5ec6..082064608 100644 --- a/src/receivers/receiver_base.cpp +++ b/src/receivers/receiver_base.cpp @@ -23,6 +23,9 @@ #include #include "receivers/receiver_base.h" #include +#include +#include +#include static const int MIN_IN = 1; /* Minimum number of input streams. */ @@ -40,13 +43,22 @@ receiver_base_cf::receiver_base_cf(std::string src_name, float pref_quad_rate, f { iq_resamp = make_resampler_cc(d_pref_quad_rate/d_quad_rate); agc = make_rx_agc_2f(d_audio_rate, false, 0, 0, 100, 500, 500, 0); - sql = gr::analog::simple_squelch_cc::make(-150.0, 0.001); + sql = make_rx_sql_cc(-150.0, 0.001); meter = make_rx_meter_c((double)d_pref_quad_rate); + wav_sink = wavfile_sink_gqrx::make(0, 2, (unsigned int) d_audio_rate, + wavfile_sink_gqrx::FORMAT_WAV, + wavfile_sink_gqrx::FORMAT_PCM_16); + connect(agc, 0, wav_sink, 0); + connect(agc, 1, wav_sink, 1); + wav_sink->set_rec_event_handler(std::bind(rec_event, this, std::placeholders::_1, + std::placeholders::_2)); } receiver_base_cf::~receiver_base_cf() { - + //Prevent segfault + if(wav_sink) + wav_sink->set_rec_event_handler(nullptr); } void receiver_base_cf::set_quad_rate(float quad_rate) @@ -61,6 +73,40 @@ void receiver_base_cf::set_quad_rate(float quad_rate) } } +void receiver_base_cf::set_center_freq(double center_freq) +{ + d_center_freq = center_freq; + wav_sink->set_center_freq(center_freq); +} + +void receiver_base_cf::set_offset(double offset) +{ + d_offset = offset; + wav_sink->set_offset(offset); +} + +void receiver_base_cf::set_rec_dir(std::string dir) +{ + d_rec_dir = dir; + wav_sink->set_rec_dir(dir); +} + +void receiver_base_cf::set_audio_rec_sql_triggered(bool enabled) +{ + sql->set_impl(enabled ? rx_sql_cc::SQL_PWR : rx_sql_cc::SQL_SIMPLE); + wav_sink->set_sql_triggered(enabled); +} + +void receiver_base_cf::set_audio_rec_min_time(const int time_ms) +{ + wav_sink->set_rec_min_time(time_ms); +} + +void receiver_base_cf::set_audio_rec_max_gap(const int time_ms) +{ + wav_sink->set_rec_max_gap(time_ms); +} + float receiver_base_cf::get_signal_level() { return meter->get_level_db(); @@ -205,3 +251,40 @@ bool receiver_base_cf::is_rds_decoder_active() { return false; } + +int receiver_base_cf::start_audio_recording() +{ + return wav_sink->open_new(); +} + +void receiver_base_cf::stop_audio_recording() +{ + wav_sink->close(); +} + +//FIXME Reimplement wavfile_sink correctly to make this work as expected +void receiver_base_cf::continue_audio_recording(receiver_base_cf_sptr from) +{ + if(from.get() == this) + return; + from->disconnect(from->agc, 0, from->wav_sink, 0); + from->disconnect(from->agc, 1, from->wav_sink, 1); + wav_sink = from->wav_sink; + wav_sink->set_rec_event_handler(std::bind(rec_event, this, std::placeholders::_1, + std::placeholders::_2)); + connect(agc, 0, wav_sink, 0); + connect(agc, 1, wav_sink, 1); + from->wav_sink.reset(); +} + +std::string receiver_base_cf::get_last_audio_filename() +{ + return d_audio_filename; +} + +void receiver_base_cf::rec_event(receiver_base_cf * self, std::string filename, bool is_running) +{ + self->d_audio_filename = filename; + if(self->d_rec_event) + self->d_rec_event(filename, is_running); +} diff --git a/src/receivers/receiver_base.h b/src/receivers/receiver_base.h index 53785a14b..89c3d1ea0 100644 --- a/src/receivers/receiver_base.h +++ b/src/receivers/receiver_base.h @@ -24,10 +24,12 @@ #define RECEIVER_BASE_H #include -#include +#include #include "dsp/resampler_xx.h" #include "dsp/rx_meter.h" #include "dsp/rx_agc_xx.h" +#include "dsp/rx_squelch.h" +#include "interfaces/wav_sink.h" class receiver_base_cf; @@ -53,6 +55,7 @@ class receiver_base_cf : public gr::hier_block2 /*! \brief Public constructor. * \param src_name Descriptive name used in the constructor of gr::hier_block2 */ + typedef std::function rec_event_handler_t; receiver_base_cf(std::string src_name, float pref_quad_rate, float quad_rate, int audio_rate); virtual ~receiver_base_cf(); @@ -60,6 +63,16 @@ class receiver_base_cf : public gr::hier_block2 virtual bool stop() = 0; virtual void set_quad_rate(float quad_rate); + virtual void set_center_freq(double center_freq); + virtual void set_offset(double offset); + virtual void set_rec_dir(std::string dir); + virtual std::string get_rec_dir() { return d_rec_dir; } + virtual void set_audio_rec_sql_triggered(bool enabled); + virtual bool get_audio_rec_sql_triggered() { return wav_sink->get_sql_triggered(); } + virtual void set_audio_rec_min_time(const int time_ms); + virtual int get_audio_rec_min_time() { return wav_sink->get_min_time(); } + virtual void set_audio_rec_max_gap(const int time_ms); + virtual int get_audio_rec_max_gap() { return wav_sink->get_max_gap(); } virtual void set_filter(double low, double high, double tw) = 0; virtual void set_cw_offset(double offset) = 0; @@ -110,16 +123,32 @@ class receiver_base_cf : public gr::hier_block2 virtual void stop_rds_decoder(); virtual void reset_rds_parser(); virtual bool is_rds_decoder_active(); + virtual int start_audio_recording(); + virtual void stop_audio_recording(); + virtual void continue_audio_recording(receiver_base_cf_sptr from); + virtual std::string get_last_audio_filename(); + template void set_rec_event_handler(T handler) + { + d_rec_event = handler; + } + protected: - float d_quad_rate; /*!< Input sample rate. */ - int d_audio_rate; /*!< Audio output rate. */ + float d_quad_rate; /*!< Input sample rate. */ + int d_audio_rate; /*!< Audio output rate. */ + double d_center_freq; + double d_offset; + std::string d_rec_dir; + std::string d_audio_filename; resampler_cc_sptr iq_resamp; /*!< Baseband resampler. */ rx_meter_c_sptr meter; /*!< Signal strength. */ rx_agc_2f_sptr agc; /*!< Receiver AGC. */ - gr::analog::simple_squelch_cc::sptr sql; /*!< Squelch. */ + rx_sql_cc_sptr sql; /*!< Squelch. */ + wavfile_sink_gqrx::sptr wav_sink; /*!< WAV file sink for recording. */ private: float d_pref_quad_rate; + rec_event_handler_t d_rec_event; + static void rec_event(receiver_base_cf * self, std::string filename, bool is_running); }; #endif // RECEIVER_BASE_H