/* 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); }