wdsp/rnnr.c
Uladzimir Karpenka 89c8a0e2b5 first commit
2026-06-01 15:58:45 +03:00

375 lines
10 KiB
C

/* rnnr.c
This file is part of a program that implements a Software-Defined Radio.
This code/file can be found on GitHub : https://github.com/ramdor/Thetis
Copyright (C) 2000-2025 Original authors
Copyright (C) 2020-2025 Richard Samphire MW0LGE
This program 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 2
of the License, or (at your option) any later version.
This program 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 this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
The author can be reached by email at
mw0lge@grange-lane.co.uk
This code is based on code and ideas from : https://github.com/vu3rdd/wdsp
and and uses RNNoise
https://gitlab.xiph.org/xiph/rnnoise
It uses a non modified version of rmnoise and implements a ringbuffer to handle input/output frame size differences.
*/
//
//============================================================================================//
// Dual-Licensing Statement (Applies Only to Author's Contributions, Richard Samphire MW0LGE) //
// ------------------------------------------------------------------------------------------ //
// For any code originally written by Richard Samphire MW0LGE, or for any modifications //
// made by him, the copyright holder for those portions (Richard Samphire) reserves the //
// right to use, license, and distribute such code under different terms, including //
// closed-source and proprietary licences, in addition to the GNU General Public License //
// granted above. Nothing in this statement restricts any rights granted to recipients under //
// the GNU GPL. Code contributed by others (not Richard Samphire) remains licensed under //
// its original terms and is not affected by this dual-licensing statement in any way. //
// Richard Samphire can be reached by email at : mw0lge@grange-lane.co.uk //
//============================================================================================//
#define _CRT_SECURE_NO_WARNINGS
#include "comm.h"
#include "rnnoise.h"
static inline float db_to_lin(float db) { return powf(10.0f, db / 20.0f); }
static inline float lin_to_db(float lin) { return 20.0f * log10f(fmaxf(lin, 1e-12f)); }
#define AGC_TARGET_DB (60.0f)
#define AGC_MIN_DB (-12.0f)
#define AGC_MAX_DB (+220.0f)
#define AGC_ATTACK_MS (10.0f)
#define AGC_RELEASE_MS (200.0f)
#define AGC_RMS_FLOOR (1e-6f)
#define SAFETY_CEIL (30000.0f)
static float agc_alpha_ms(float ms, float frame_hz) {
const float tc = ms / 1000.0f;
const float a = expf(-(1.0f / frame_hz) / tc);
return a < 0.0f ? 0.0f : (a > 1.0f ? 1.0f : a);
}
void rnnr_agc_init(RNNR a)
{
const float frame_hz = (a->frame_size > 0) ? ((float)a->rate / (float)a->frame_size) : 100.0f;
a->agc_att_a = agc_alpha_ms(AGC_ATTACK_MS, frame_hz);
a->agc_rel_a = agc_alpha_ms(AGC_RELEASE_MS, frame_hz);
a->gain_db = AGC_TARGET_DB;
a->gain = db_to_lin(a->gain_db);
}
static float frame_rms(const float* x, int n) {
double s = 0.0;
for (int i = 0; i < n; i++) { double v = (double)x[i]; s += v * v; }
float r = (float)sqrt(s / (double)n);
return (r < AGC_RMS_FLOOR) ? AGC_RMS_FLOOR : r;
}
//used to track RNNR instances
static RNNR* _rnnr_instances = NULL;
static int _rnnr_count = 0;
static int _rnnr_capacity = 0;
// the model to use when creating new RNNR instances
static RNNModel* _rnnr_model = NULL;
//ringbuffer
static void ring_buffer_init(rnnr_ring_buffer* rb, int capacity)
{
rb->buf = malloc0(capacity * sizeof(float));
rb->capacity = capacity;
rb->head = 0;
rb->tail = 0;
rb->count = 0;
}
static void ring_buffer_free(rnnr_ring_buffer* rb)
{
_aligned_free(rb->buf);
rb->buf = NULL;
rb->capacity = 0;
rb->head = rb->tail = rb->count = 0;
}
static void ring_buffer_put(rnnr_ring_buffer* rb, float v)
{
if (rb->count < rb->capacity)
{
rb->buf[rb->tail] = v;
rb->tail = (rb->tail + 1) % rb->capacity;
rb->count++;
}
}
static int ring_buffer_get_bulk(rnnr_ring_buffer* rb, float* dest, int n)
{
int to_get = n < rb->count ? n : rb->count;
for (int i = 0; i < to_get; i++)
{
dest[i] = rb->buf[rb->head];
rb->head = (rb->head + 1) % rb->capacity;
}
rb->count -= to_get;
return to_get;
}
static void ring_buffer_resize(rnnr_ring_buffer* rb, int new_capacity)
{
if (new_capacity == rb->capacity) return;
float* new_buf = malloc0(new_capacity * sizeof(float));
int cnt = rb->count;
for (int i = 0; i < cnt; i++)
{
new_buf[i] = rb->buf[(rb->head + i) % rb->capacity];
}
_aligned_free(rb->buf);
rb->buf = new_buf;
rb->capacity = new_capacity;
rb->head = 0;
rb->tail = cnt % new_capacity;
}
//
PORT
void SetRXARNNRRun (int channel, int run)
{
RNNR a = rxa[channel].rnnr.p;
if (a->run != run)
{
RXAbp1Check (channel, rxa[channel].amd.p->run, rxa[channel].snba.p->run,
rxa[channel].emnr.p->run, rxa[channel].anf.p->run, rxa[channel].anr.p->run,
run, rxa[channel].sbnr.p->run); // NR3 + NR4 support
EnterCriticalSection (&ch[channel].csDSP);
a->run = run;
RXAbp1Set (channel);
LeaveCriticalSection (&ch[channel].csDSP);
}
}
void setSize_rnnr(RNNR a, int size)
{
_aligned_free(a->output_buffer);
a->buffer_size = size;
a->output_buffer = malloc0(a->buffer_size * sizeof(float));
int new_cap = a->frame_size + a->buffer_size;
ring_buffer_resize(&a->input_ring, new_cap);
ring_buffer_resize(&a->output_ring, new_cap);
}
void setBuffers_rnnr(RNNR a, double* in, double* out)
{
a->in = in;
a->out = out;
}
void setSamplerate_rnnr(RNNR a, int rate)
{
a->rate = rate;
rnnr_agc_init(a);
}
RNNR create_rnnr(int run, int position, int size, double* in, double* out, int rate)
{
RNNR a = malloc0(sizeof(rnnr));
InitializeCriticalSection(&a->cs);
a->run = run;
a->position = position;
a->rate = rate; // not used currently, but here for future use
a->st = rnnoise_create(_rnnr_model);
a->frame_size = rnnoise_get_frame_size();
a->in = in;
a->out = out;
a->buffer_size = size;
rnnr_agc_init(a);
ring_buffer_init(&a->input_ring, a->frame_size + a->buffer_size);
ring_buffer_init(&a->output_ring, a->frame_size + a->buffer_size);
a->to_process_buffer = malloc0(a->frame_size * sizeof(float));
a->processed_output_buffer = malloc0(a->frame_size * sizeof(float));
a->output_buffer = malloc0(a->buffer_size * sizeof(float));
// used to maintain a record of RNNR's here and is used so we can update them all if/when model is changed
if (_rnnr_count == _rnnr_capacity)
{
int new_cap = _rnnr_capacity ? _rnnr_capacity * 2 : 4; // limit number of reallocs by doubling space each time, overkill but that is my middle name ;)
RNNR* tmp = malloc0(new_cap * sizeof(RNNR));
memcpy(tmp, _rnnr_instances, _rnnr_count * sizeof(RNNR));
_aligned_free(_rnnr_instances);
_rnnr_instances = tmp;
_rnnr_capacity = new_cap;
}
_rnnr_instances[_rnnr_count++] = a;
//
return a;
}
void xrnnr(RNNR a, int pos)
{
if (a->st && a->run && pos == a->position)
{
int bs = a->buffer_size;
int fs = a->frame_size;
float* to_process = a->to_process_buffer;
float* process_out = a->processed_output_buffer;
EnterCriticalSection(&a->cs);
for (int i = 0; i < bs; i++)
{
ring_buffer_put(&a->input_ring, (float)a->in[2 * i + 0]);
if (a->input_ring.count >= fs)
{
ring_buffer_get_bulk(&a->input_ring, to_process, fs);
float rms = frame_rms(to_process, fs);
float cur_db = lin_to_db(rms);
float desired_db = AGC_TARGET_DB - cur_db;
float alpha = (desired_db > a->gain_db) ? a->agc_att_a : a->agc_rel_a;
a->gain_db = alpha * a->gain_db + (1.0f - alpha) * desired_db;
if (a->gain_db < AGC_MIN_DB) a->gain_db = AGC_MIN_DB;
if (a->gain_db > AGC_MAX_DB) a->gain_db = AGC_MAX_DB;
a->gain = db_to_lin(a->gain_db);
for (int j = 0; j < fs; j++)
{
float v = to_process[j] * a->gain;
if (v > SAFETY_CEIL) v = SAFETY_CEIL;
if (v < -SAFETY_CEIL) v = -SAFETY_CEIL;
to_process[j] = v;
}
rnnoise_process_frame(a->st, process_out, to_process);
const float inv = (a->gain > 0.0f) ? (1.0f / a->gain) : 0.0f;
for (int j = 0; j < fs; j++)
{
ring_buffer_put(&a->output_ring, process_out[j] * inv);
}
}
}
LeaveCriticalSection(&a->cs);
if (a->output_ring.count >= bs)
{
ring_buffer_get_bulk(&a->output_ring, a->output_buffer, bs);
for (int i = 0; i < bs; i++)
{
a->out[2 * i + 0] = (double)a->output_buffer[i];
a->out[2 * i + 1] = 0;
}
}
else
{
memcpy(a->out, a->in, a->buffer_size * sizeof(complex));
}
}
else if (a->out != a->in)
{
memcpy(a->out, a->in, a->buffer_size * sizeof(complex));
}
}
void destroy_rnnr(RNNR a)
{
// we dont need to maintain order, so just replace with last, and decrement total
for (int i = 0; i < _rnnr_count; i++)
{
if (_rnnr_instances[i] == a)
{
_rnnr_instances[i] = _rnnr_instances[--_rnnr_count];
break;
}
}
EnterCriticalSection(&a->cs);
rnnoise_destroy(a->st);
LeaveCriticalSection(&a->cs);
DeleteCriticalSection(&a->cs);
_aligned_free(a->to_process_buffer);
_aligned_free(a->processed_output_buffer);
_aligned_free(a->output_buffer);
ring_buffer_free(&a->input_ring);
ring_buffer_free(&a->output_ring);
_aligned_free(a);
// tidy if none now in use
if (_rnnr_count == 0)
{
_aligned_free(_rnnr_instances);
_rnnr_instances = NULL;
_rnnr_capacity = 0;
}
}
PORT
void RNNRloadModel(const char* file_path)
{
// destroy any in use
for (int i = 0; i < _rnnr_count; i++)
{
RNNR a = _rnnr_instances[i];
EnterCriticalSection(&a->cs);
a->run_old = a->run;
a->run = 0;
rnnoise_destroy(a->st);
LeaveCriticalSection(&a->cs);
}
// free up any previous loaded model
if (_rnnr_model)
{
rnnoise_model_free(_rnnr_model);
}
_rnnr_model = NULL; // default to baked in model
// try to load
if (file_path && file_path[0])
{
_rnnr_model = rnnoise_model_from_filename(file_path);
}
// recreate any we had created previously and restart if needed
for (int i = 0; i < _rnnr_count; i++)
{
RNNR a = _rnnr_instances[i];
EnterCriticalSection(&a->cs);
a->st = rnnoise_create(_rnnr_model);
a->run = a->run_old;
LeaveCriticalSection(&a->cs);
}
}
PORT
void SetRXARNNRPosition(int channel, int position)
{
EnterCriticalSection(&ch[channel].csDSP);
rxa[channel].rnnr.p->position = position;
rxa[channel].bp1.p->position = position;
LeaveCriticalSection(&ch[channel].csDSP);
}