375 lines
10 KiB
C
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);
|
|
}
|
|
|