diff mbox series

[FFmpeg-devel,6/7] avradio/sdr: Support setting gain value manually and automatic

Message ID 20230717002704.3092192-6-michael@niedermayer.cc
State New
Headers show
Series [FFmpeg-devel,1/7] avradio/sdrdemux: icarrier just needs phase 2 block size | expand

Checks

Context Check Description
andriy/configure_x86 warning Failed to apply patch
yinshiyou/configure_loongarch64 warning Failed to apply patch

Commit Message

Michael Niedermayer July 17, 2023, 12:27 a.m. UTC
Signed-off-by: Michael Niedermayer <michael@niedermayer.cc>
---
 libavradio/sdr.h        | 23 +++++++++++++++++-
 libavradio/sdrdemux.c   | 53 ++++++++++++++++++++++++++++++++++++++++-
 libavradio/sdrinradio.c | 41 +++++++++++++++++++++++++++++--
 3 files changed, 113 insertions(+), 4 deletions(-)
diff mbox series

Patch

diff --git a/libavradio/sdr.h b/libavradio/sdr.h
index dc20415457..395b056531 100644
--- a/libavradio/sdr.h
+++ b/libavradio/sdr.h
@@ -68,6 +68,12 @@  typedef enum Modulation {
     //QAM, PSK, ...
 } Modulation;
 
+typedef enum SDR_GAIN {
+    GAIN_DEFAULT = -3,
+    GAIN_SW_AGC = -2,
+    GAIN_SDR_AGC = -1,
+} SDR_GAIN;
+
 #define HISTOGRAMM_SIZE 9
 
 typedef struct Station {
@@ -105,6 +111,7 @@  typedef struct Station {
 
 typedef struct FIFOElement {
     int64_t center_frequency;
+    float gain;
     void *halfblock;
 } FIFOElement;
 
@@ -146,7 +153,15 @@  typedef struct SDRContext {
     int64_t min_center_freq;
     int64_t max_center_freq;
     int sdr_sample_rate;
-    int sdr_agc;
+    float min_gain;
+    float max_gain;
+    int sdr_gain;
+    float agc_min_headroom;
+    float agc_max_headroom;
+    float agc_max_headroom_time;
+    int agc_low_time;
+    float agc_gain;                         ///< current gain, should be accessed only by buffer thread after init
+    atomic_int wanted_gain;
     int sdr_adcc;
     int64_t bandwidth;
     int64_t last_pts;
@@ -212,6 +227,12 @@  typedef struct SDRContext {
      */
     int64_t (*set_frequency_callback)(struct SDRContext *sdr, int64_t frequency);
 
+    /**
+     * Setup the hardware for the requested gain
+     * This must only be called from the buffer thread after setup (or more mutex calls are needed)
+     */
+    int (*set_gain_callback)(struct SDRContext *sdr, float gain);
+
     /**
      * Read from the hardware, block if nothing available with a reasonable timeout
      *
diff --git a/libavradio/sdrdemux.c b/libavradio/sdrdemux.c
index a0b80785ef..5214aea7be 100644
--- a/libavradio/sdrdemux.c
+++ b/libavradio/sdrdemux.c
@@ -1409,6 +1409,7 @@  static void *soapy_needs_bigger_buffers_worker(SDRContext *sdr)
         FIFOElement fifo_element;
         int remaining, ret;
         int empty_blocks, full_blocks;
+        float wanted_gain = atomic_load(&sdr->wanted_gain) / 65536.0;
 
         //i wish av_fifo was thread safe
         pthread_mutex_lock(&sdr->mutex);
@@ -1444,9 +1445,17 @@  static void *soapy_needs_bigger_buffers_worker(SDRContext *sdr)
             //And theres not much else we can do, an error message was already printed by ff_sdr_set_freq() in that case
             block_counter = 0; // we just changed the frequency, do not trust the next blocks content
         }
+        if (sdr->sdr_gain == GAIN_SW_AGC &&
+            fabs(wanted_gain - sdr->agc_gain) > 0.001 &&
+            sdr->set_gain_callback
+        ) {
+            sdr->set_gain_callback(sdr, wanted_gain);
+            sdr->agc_gain = wanted_gain;
+        }
         pthread_mutex_unlock(&sdr->mutex);
 
         fifo_element.center_frequency = block_counter > 0 ? sdr->freq : 0;
+        fifo_element.gain             = sdr->agc_gain; //we make only small changes so slightly mixing should be ok
 
         remaining = sdr->block_size;
         while (remaining && !atomic_load(&sdr->close_requested)) {
@@ -1624,6 +1633,7 @@  int ff_sdr_common_init(AVFormatContext *s)
     av_fifo_auto_grow_limit(sdr-> full_block_fifo, sdr->sdr_sample_rate / sdr->block_size);
 
     atomic_init(&sdr->close_requested, 0);
+    atomic_init(&sdr->wanted_gain, lrint((sdr->min_gain + sdr->max_gain) * 65536 / 2));
     ret = pthread_mutex_init(&sdr->mutex, NULL);
     if (ret) {
         av_log(s, AV_LOG_ERROR, "pthread_mutex_init failed: %s\n", strerror(ret));
@@ -1886,6 +1896,37 @@  process_next_block:
         }
     }
 
+    float smaller_block_gain = FFMIN(fifo_element[0].gain, fifo_element[1].gain);
+    float  bigger_block_gain = FFMAX(fifo_element[0].gain, fifo_element[1].gain);
+
+    if (sdr->sdr_gain == GAIN_SW_AGC) {
+        float inmax = 0;
+        float wanted_gain = atomic_load(&sdr->wanted_gain) / 65536.0;
+        // We only check 25% of the data to safe computations
+        int start = 3*sdr->block_size / 4;
+        int end   = 5*sdr->block_size / 4;
+        for (i = start; i < end; i++) {
+            float v = fmaxf(fabsf(sdr->windowed_block[i].re), fabsf(sdr->windowed_block[i].im));
+            inmax = fmaxf(inmax, v);
+        }
+
+        if (inmax > 1.0 - sdr->agc_min_headroom && wanted_gain > sdr->min_gain) {
+            //according to docs this is a dB scale, in reality it beheaves differnt to that
+            //Because of this we will try to just make small changes and not assume too much
+            wanted_gain = FFMIN(wanted_gain, FFMAX(smaller_block_gain - 1.0, smaller_block_gain * 0.9));
+
+            sdr->agc_low_time = 0;
+        } else if (inmax < 1.0 - sdr->agc_max_headroom && wanted_gain < sdr->max_gain) {
+            sdr->agc_low_time += sdr->block_size;
+            if (sdr->agc_low_time > sdr->agc_max_headroom_time * sdr->sdr_sample_rate) {
+                sdr->agc_low_time = 0;
+                wanted_gain = FFMAX(wanted_gain, FFMIN(bigger_block_gain + 1.0, bigger_block_gain * 1.1));
+            }
+        } else
+            sdr->agc_low_time = 0;
+        atomic_store(&sdr->wanted_gain, (int)lrint(wanted_gain * 65536));
+    }
+
     inject_block_into_fifo(sdr, sdr->empty_block_fifo, &fifo_element[0], "Cannot pass next buffer, freeing it\n");
 #ifdef SYN_TEST //synthetic test signal
     static int64_t synp=0;
@@ -2141,7 +2182,17 @@  const AVOption ff_sdr_options[] = {
     { "rtlsdr_fixes" , "workaround rtlsdr issues", OFFSET(rtlsdr_fixes), AV_OPT_TYPE_INT , {.i64 = -1}, -1, 1, DEC},
     { "sdr_sr"  , "sdr sample rate"  , OFFSET(sdr_sample_rate ), AV_OPT_TYPE_INT , {.i64 = 0}, 0, INT_MAX, DEC},
     { "sdr_freq", "sdr frequency"    , OFFSET(wanted_freq), AV_OPT_TYPE_INT64 , {.i64 = 9000000}, 0, INT64_MAX, DEC},
-    { "sdr_agc" , "sdr automatic gain control",  OFFSET(sdr_agc),  AV_OPT_TYPE_BOOL , {.i64 =  1}, -1, 1, DEC},
+    { "gain" , "sdr overall gain",  OFFSET(sdr_gain),  AV_OPT_TYPE_INT , {.i64 =  GAIN_SDR_AGC}, -3, INT_MAX, DEC, "gain"},
+        { "sdr_agc", "SDR AGC (if supported)", 0, AV_OPT_TYPE_CONST, {.i64 = GAIN_SDR_AGC}, 0, 0, DEC, "gain"},
+        { "sw_agc", "Software AGC", 0, AV_OPT_TYPE_CONST, {.i64 = GAIN_SW_AGC}, 0, 0, DEC, "gain"},
+        { "default_gain", "Never touch gain", 0, AV_OPT_TYPE_CONST, {.i64 = GAIN_DEFAULT}, 0, 0, DEC, "gain"},
+
+    { "agc_min_headroom",  "AGC min headroom",  OFFSET(agc_min_headroom),  AV_OPT_TYPE_FLOAT, {.dbl = 0.4}, 0, 1.0, DEC},
+    { "agc_max_headroom",  "AGC max headroom",  OFFSET(agc_max_headroom),  AV_OPT_TYPE_FLOAT, {.dbl = 0.8}, 0, 1.0, DEC},
+    { "agc_max_headroom_time",  "AGC max headroom time",  OFFSET(agc_max_headroom_time),  AV_OPT_TYPE_FLOAT, {.dbl = 0.1}, 0, INT_MAX, DEC},
+    { "min_gain", "minimum gain", OFFSET(min_gain   ), AV_OPT_TYPE_FLOAT , {.dbl = 0}, 0, INT_MAX, DEC},
+    { "max_gain", "maximum gain", OFFSET(max_gain   ), AV_OPT_TYPE_FLOAT , {.dbl = 0}, 0, INT_MAX, DEC},
+
     { "sdr_adcc" ,"sdr automatic dc correction", OFFSET(sdr_adcc), AV_OPT_TYPE_BOOL , {.i64 = -1}, -1, 1, DEC},
     { "min_freq", "minimum frequency", OFFSET(min_freq   ), AV_OPT_TYPE_INT64 , {.i64 = 0}, 0, INT64_MAX, DEC},
     { "max_freq", "maximum frequency", OFFSET(max_freq   ), AV_OPT_TYPE_INT64 , {.i64 = 0}, 0, INT64_MAX, DEC},
diff --git a/libavradio/sdrinradio.c b/libavradio/sdrinradio.c
index 3956c18375..63a9cade78 100644
--- a/libavradio/sdrinradio.c
+++ b/libavradio/sdrinradio.c
@@ -68,6 +68,30 @@  static int sdrindev_read_callback(SDRContext *sdr, FIFOElement *fifo_element, in
     return ret;
 }
 
+static int sdrindev_set_gain_callback(SDRContext *sdr, float gain)
+{
+    AVFormatContext *avfmt = sdr->avfmt;
+    SoapySDRDevice *soapy = sdr->soapy;
+
+    if (sdr->sdr_gain == GAIN_DEFAULT)
+        return 0;
+
+    if (soapy) {
+        int ret = SoapySDRDevice_setGainMode(soapy, SOAPY_SDR_RX, 0, sdr->sdr_gain == GAIN_SDR_AGC);
+        if (ret) {
+            av_log(avfmt, AV_LOG_WARNING, "Failed to set gain mode %d (%s)\n", sdr->sdr_gain == GAIN_SDR_AGC, SoapySDRDevice_lastError());
+        }
+
+        if (sdr->sdr_gain != GAIN_SDR_AGC) {
+            ret = SoapySDRDevice_setGain(soapy, SOAPY_SDR_RX, 0, gain);
+            if (ret) {
+                av_log(avfmt, AV_LOG_WARNING, "Failed to set gain to %f (%s)\n", gain, SoapySDRDevice_lastError());
+            }
+        }
+    }
+    return 0;
+}
+
 static int64_t sdrindev_set_frequency_callback(SDRContext *sdr, int64_t freq)
 {
     AVFormatContext *avfmt = sdr->avfmt;
@@ -135,6 +159,7 @@  static int sdrindev_initial_hw_setup(AVFormatContext *s)
 
     sdr->read_callback          = sdrindev_read_callback;
     sdr->set_frequency_callback = sdrindev_set_frequency_callback;
+    sdr->set_gain_callback      = sdrindev_set_gain_callback;
 
     // Go over all available soapy devices
     // Print the usable ones, and choose one unless the user has choosen one
@@ -195,8 +220,10 @@  static int sdrindev_initial_hw_setup(AVFormatContext *s)
     //Inform the user if AGC is supported and setup AGC as requested by the user
     has_agc = SoapySDRDevice_hasGainMode(soapy, SOAPY_SDR_RX, 0);
     av_log(s, AV_LOG_INFO, "RX AGC Supported: %s\n", has_agc ? "yes" : "no");
-    if (has_agc && sdr->sdr_agc >= 0)
-        SoapySDRDevice_setGainMode(soapy, SOAPY_SDR_RX, 0, sdr->sdr_agc);
+    if (!has_agc &&  sdr->sdr_gain == GAIN_SDR_AGC) {
+        av_log(s, AV_LOG_WARNING, "hardware AGC unsupported switching to software AGC\n");
+        sdr->sdr_gain = GAIN_SW_AGC;
+    }
 
     //Inform the user if automatic DC correction is supported and setup DC correction as requested by the user
     has_adcc = SoapySDRDevice_hasDCOffsetMode(soapy, SOAPY_SDR_RX, 0);
@@ -208,6 +235,16 @@  static int sdrindev_initial_hw_setup(AVFormatContext *s)
     range = SoapySDRDevice_getGainRange(soapy, SOAPY_SDR_RX, 0);
     av_log(s, AV_LOG_INFO, "Rx Gain range: %f dB - %f dB\n", range.minimum, range.maximum);
 
+    if (!sdr->min_gain)
+        sdr->min_gain = range.minimum;
+
+    if (!sdr->max_gain)
+        sdr->max_gain = range.maximum;
+    if (sdr->min_gain > sdr->max_gain) {
+        av_log(s, AV_LOG_ERROR, "Invalid gain range\n");
+        return AVERROR(EINVAL);
+    }
+
     //Inform the user about the Frequency ranges available, verify the range requested by the user and set the range if the user has not specified one
     ranges = SoapySDRDevice_getFrequencyRange(soapy, SOAPY_SDR_RX, 0, &length);
     av_log(s, AV_LOG_INFO, "Rx freq ranges: ");