ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
spectral.c
Go to the documentation of this file.
2#include <ascii-chat/common.h>
5#include <fftw3.h>
6#include <math.h>
7#include <string.h>
8
9#define SPECTRAL_MAX_SIZE 2048
10#define SPECTRAL_MAX_BINS (SPECTRAL_MAX_SIZE / 2 + 1)
11
32
33// FFTW execution is independent per instance; planning and destruction share state.
35
37 return (spectral_config_t){.sample_rate = sample_rate,
38 .fft_size = 1024,
39 .noise_gate = true,
40 .reduction_db = 18.0f,
41 .crossovers = {250, 2000, 8000},
42 .thresholds_db = {-18, -18, -18, -18},
43 .ratios = {2, 3, 3, 2}};
44}
45
47 if (!p)
48 return;
49 memset(p->input, 0, sizeof(p->input));
50 memset(p->overlap, 0, sizeof(p->overlap));
51 memset(p->ready, 0, sizeof(p->ready));
52 memset(p->noise, 0, sizeof(p->noise));
53 memset(p->smoothed, 0, sizeof(p->smoothed));
54 memset(p->magnitude, 0, sizeof(p->magnitude));
55 memset(p->band_average, 0, sizeof(p->band_average));
56 for (int i = 0; i < SPECTRAL_MAX_BINS; ++i)
57 p->gains[i] = 1;
58 for (int i = 0; i < 4; ++i)
59 p->band_gain[i] = p->eq_gain[i] = 1;
60 p->pending = p->cursor = 0;
61 memset(p->previous_phase, 0, sizeof(p->previous_phase));
62 memset(p->synthesis_phase, 0, sizeof(p->synthesis_phase));
63 p->pitch_hz = 0;
64 p->auto_gain = 1;
65 p->frames = 0;
66}
67
69 if (!out)
70 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing spectral output pointer");
71 *out = NULL;
72 if (!c || c->sample_rate < 16000 || c->sample_rate > 192000 ||
73 (c->fft_size != 512 && c->fft_size != 1024 && c->fft_size != 2048) || !isfinite(c->reduction_db) ||
74 c->reduction_db < 0 || c->reduction_db > 60)
75 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid spectral configuration");
76 for (int b = 0; b < 4; ++b) {
77 if (!isfinite(c->ratios[b]) || c->ratios[b] < 1 || c->ratios[b] > 20 || !isfinite(c->thresholds_db[b]) ||
78 c->thresholds_db[b] > 0 || c->thresholds_db[b] < -90)
79 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid spectral compressor band");
80 if (b < 3 && (!isfinite(c->crossovers[b]) || c->crossovers[b] <= 0 || c->crossovers[b] > c->sample_rate / 2.0f ||
81 (b && c->crossovers[b] <= c->crossovers[b - 1])))
82 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid spectral crossover");
83 }
85 if (!p)
86 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate spectral processor");
87 p->config = *c;
88 (void)NAMED_REGISTER_MUTEX(&audio_fftw_planner_lock, "audio.fftw_planner", NULL);
89 static_mutex_lock(&audio_fftw_planner_lock);
90 p->forward = fftw_plan_dft_r2c_1d(c->fft_size, p->time, p->frequency, FFTW_ESTIMATE);
91 p->inverse = fftw_plan_dft_c2r_1d(c->fft_size, p->frequency, p->restored, FFTW_ESTIMATE);
92 static_mutex_unlock(&audio_fftw_planner_lock);
93 if (!p->forward || !p->inverse) {
95 return SET_ERRNO(ERROR_MEMORY, "Cannot create FFTW plans");
96 }
97 for (int i = 0; i < c->fft_size; ++i)
98 p->window[i] = (float)(0.5 - 0.5 * cos(6.283185307179586 * i / c->fft_size));
100 *out = p;
101 return ASCIICHAT_OK;
102}
103
105 if (!p)
106 return;
107 static_mutex_lock(&audio_fftw_planner_lock);
108 if (p->forward)
109 fftw_destroy_plan(p->forward);
110 if (p->inverse)
111 fftw_destroy_plan(p->inverse);
112 static_mutex_unlock(&audio_fftw_planner_lock);
113 SAFE_FREE(p);
114}
115
116static int band_for(const spectral_processor_t *p, int bin) {
117 float hz = (float)bin * p->config.sample_rate / p->config.fft_size;
118 int band = 0;
119 while (band < 3 && hz >= p->config.crossovers[band])
120 ++band;
121 return band;
122}
123
124// Normalized autocorrelation with a two-period minimum observation window.
125static float detect_pitch(spectral_processor_t *p) {
126 int n = p->config.fft_size;
127 int first = p->config.sample_rate / 500;
128 int last = n / 2;
129 if (last > p->config.sample_rate / 70)
130 last = p->config.sample_rate / 70;
131 float correlation[SPECTRAL_MAX_SIZE / 2 + 1] = {0};
132 double mean = 0, power = 0;
133 for (int i = 0; i < n; ++i)
134 mean += p->input[i] / (double)n;
135 for (int i = 0; i < n; ++i)
136 power += (p->input[i] - mean) * (p->input[i] - mean);
137 if (power / n < 1e-6)
138 return 0;
139 float best = 0;
140 for (int lag = first; lag <= last; ++lag) {
141 double cross = 0, a = 0, b = 0;
142 for (int i = 0; i < n - lag; ++i) {
143 double x = p->input[i] - mean, y = p->input[i + lag] - mean;
144 cross += x * y;
145 a += x * x;
146 b += y * y;
147 }
148 correlation[lag] = (float)(cross / sqrt(a * b + 1e-20));
149 if (correlation[lag] > best)
150 best = correlation[lag];
151 }
152 if (best < 0.85f)
153 return 0;
154 for (int lag = first + 1; lag < last; ++lag) {
155 float value = correlation[lag];
156 if (value > 0.95f * best && value > correlation[lag - 1] && value >= correlation[lag + 1]) {
157 float delta = 0.5f * (correlation[lag - 1] - correlation[lag + 1]) /
158 (correlation[lag - 1] - 2 * value + correlation[lag + 1]);
159 return p->config.sample_rate / (lag + delta);
160 }
161 }
162 return 0;
163}
164
165static void correct_pitch(spectral_processor_t *p) {
166 const double two_pi = 6.283185307179586;
167 int n = p->config.fft_size, hop = n / 4;
168 double factor = 1;
169 if (p->pitch_hz > 0) {
170 double note = round(69 + 12 * log2(p->pitch_hz / 440.0));
171 factor = 440 * pow(2, (note - 69) / 12) / p->pitch_hz;
172 }
173 memset(p->shifted_magnitude, 0, sizeof(p->shifted_magnitude));
174 memset(p->shifted_frequency, 0, sizeof(p->shifted_frequency));
175 for (int k = 0; k <= n / 2; ++k) {
176 double phase = atan2(p->frequency[k][1], p->frequency[k][0]);
177 double expected = two_pi * k * hop / n;
178 double delta = remainder(phase - p->previous_phase[k] - expected, two_pi);
179 p->previous_phase[k] = phase;
180 double frequency = (expected + delta) / hop;
181 int target = (int)round(k * factor);
182 if (target <= n / 2) {
183 double magnitude = hypot(p->frequency[k][0], p->frequency[k][1]);
184 p->shifted_magnitude[target] += magnitude;
185 p->shifted_frequency[target] += magnitude * frequency * factor;
186 }
187 }
188 for (int k = 0; k <= n / 2; ++k) {
189 double magnitude = p->shifted_magnitude[k];
190 double frequency = magnitude > 1e-15 ? p->shifted_frequency[k] / magnitude : two_pi * k / n;
191 p->synthesis_phase[k] = remainder(p->synthesis_phase[k] + frequency * hop, two_pi);
192 p->frequency[k][0] = magnitude * cos(p->synthesis_phase[k]);
193 p->frequency[k][1] = (k == 0 || k == n / 2) ? 0 : magnitude * sin(p->synthesis_phase[k]);
194 }
195}
196
197static void process_frame(spectral_processor_t *p) {
198 int n = p->config.fft_size, hop = n / 4;
199 float dt = (float)hop / p->config.sample_rate;
200 for (int i = 0; i < n; ++i)
201 p->time[i] = p->input[i] * p->window[i];
202 fftw_execute(p->forward);
203 float energy[SPECTRAL_BAND_COUNT] = {0};
204 for (int k = 0; k <= n / 2; ++k) {
205 double re = p->frequency[k][0], im = p->frequency[k][1];
206 float power = (float)(re * re + im * im);
207 p->magnitude[k] = sqrtf(power) * ((k == 0 || k == n / 2) ? 2.0f : 4.0f) / n;
208 // Parseval normalization includes the periodic Hann window's mean square (3/8).
209 energy[band_for(p, k)] += power * ((k == 0 || k == n / 2) ? 1 : 2) / (n * n * 0.375f);
210 if (p->config.noise_gate) {
211 float level = p->magnitude[k];
212 // Learn only after a complete input window. Falling noise estimates track
213 // quickly; rising estimates move slowly so speech is not learned as noise.
214 p->smoothed[k] += (1 - expf(-dt / 0.05f)) * (level - p->smoothed[k]);
215 size_t warmup = 3 + (size_t)(0.25f / dt);
216 if (p->frames >= 3 && p->frames < warmup)
217 p->noise[k] += (level - p->noise[k]) / (float)(p->frames - 2);
218 if (p->noise[k] < 1e-9f && level > 1e-9f)
219 p->noise[k] = level;
220 if (p->frames >= warmup) {
221 float tau = p->smoothed[k] < p->noise[k] ? 0.5f : 8.0f;
222 p->noise[k] += (1 - expf(-dt / tau)) * (p->smoothed[k] - p->noise[k]);
223 float ratio = level / fmaxf(p->noise[k], 1e-10f);
224 float floor = powf(10.0f, -p->config.reduction_db / 20.0f);
225 float target = floor + (1 - floor) * fminf(1, fmaxf(0, (ratio - 1.5f) / 2.5f));
226 float coefficient = 1 - expf(-dt / (target > p->gains[k] ? 0.005f : 0.08f));
227 p->gains[k] += coefficient * (target - p->gains[k]);
228 }
229 }
230 }
231 float total = energy[0] + energy[1] + energy[2] + energy[3];
232 if (p->config.pitch_correct)
233 p->pitch_hz = detect_pitch(p);
234 if (p->config.auto_gain) {
235 float desired = total > 1e-5f ? fminf(4, fmaxf(0.25f, 0.15f / sqrtf(total))) : 1;
236 p->auto_gain += (1 - expf(-dt / (desired < p->auto_gain ? 0.01f : 0.5f))) * (desired - p->auto_gain);
237 }
238 for (int b = 0; b < 4; ++b) {
239 float db = 10 * log10f(fmaxf(energy[b], 1e-12f));
240 float reduction = fmaxf(0, db - p->config.thresholds_db[b]) * (1 - 1 / p->config.ratios[b]);
241 float target = powf(10, -reduction / 20);
242 p->band_gain[b] += (1 - expf(-dt / (target < p->band_gain[b] ? 0.01f : 0.15f))) * (target - p->band_gain[b]);
243 // Bounded spectral balancing; no claim of measured room-response inversion.
244 if (total > 1e-6f) {
245 p->band_average[b] += (1 - expf(-dt / 3)) * (energy[b] - p->band_average[b]);
246 float average = 0;
247 for (int j = 0; j < 4; ++j)
248 average += p->band_average[j] / 4;
249 float eq = sqrtf(average / fmaxf(p->band_average[b], 1e-10f));
250 eq = fminf(1.41254f, fmaxf(0.70795f, eq));
251 p->eq_gain[b] += (1 - expf(-dt / 1)) * (eq - p->eq_gain[b]);
252 }
253 }
254 for (int k = 0; k <= n / 2; ++k) {
255 int b = band_for(p, k);
256 float gain = p->config.noise_gate ? p->gains[k] : 1;
257 if (p->config.compressor)
258 gain *= p->band_gain[b];
259 if (p->config.adaptive_eq)
260 gain *= p->eq_gain[b];
261 if (p->config.auto_gain)
262 gain *= p->auto_gain;
263 p->frequency[k][0] *= gain;
264 p->frequency[k][1] *= gain;
265 }
266 if (p->config.pitch_correct)
267 correct_pitch(p);
268 fftw_execute(p->inverse);
269 // Four overlapping periodic Hann windows have a constant squared sum of 1.5.
270 for (int i = 0; i < n; ++i)
271 p->overlap[i] += (float)(p->restored[i] * p->window[i] / (n * 1.5));
272 memcpy(p->ready, p->overlap, (size_t)hop * sizeof(float));
273 memmove(p->overlap, p->overlap + hop, (size_t)(n - hop) * sizeof(float));
274 memset(p->overlap + n - hop, 0, (size_t)hop * sizeof(float));
275 memmove(p->input, p->input + hop, (size_t)(n - hop) * sizeof(float));
276 p->frames++;
277}
278
279asciichat_error_t spectral_process(spectral_processor_t *p, const float *input, float *output, size_t count) {
280 if (!p || (!input && count) || (!output && count))
281 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid spectral buffer");
282 int n = p->config.fft_size, hop = n / 4;
283 for (size_t i = 0; i < count; ++i) {
284 float sample = input[i];
285 output[i] = p->ready[p->cursor++];
286 p->input[n - hop + p->pending++] = isfinite(sample) ? sample : 0;
287 if (p->pending == hop) {
288 process_frame(p);
289 p->pending = p->cursor = 0;
290 }
291 }
292 return ASCIICHAT_OK;
293}
294
295const float *spectral_spectrum(const spectral_processor_t *p, size_t *bins) {
296 if (bins)
297 *bins = p ? (size_t)p->config.fft_size / 2 + 1 : 0;
298 return p ? p->magnitude : NULL;
299}
300
302 return p ? p->pitch_hz : 0;
303}
#define SET_ERRNO(code,...)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#define NAMED_REGISTER_MUTEX(mutex, name, parent_ptr)
Register a mutex with automatic format specifier.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
#define STATIC_MUTEX_INIT
🔢 Mathematical Utility Functions
static bool size_t gotel_query_t * out
Definition queries.h:8
#define SPECTRAL_MAX_SIZE
Definition spectral.c:9
asciichat_error_t spectral_create(const spectral_config_t *c, spectral_processor_t **out)
Definition spectral.c:68
asciichat_error_t spectral_process(spectral_processor_t *p, const float *input, float *output, size_t count)
Definition spectral.c:279
float spectral_pitch_hz(const spectral_processor_t *p)
Definition spectral.c:301
spectral_config_t spectral_default_config(int sample_rate)
Definition spectral.c:36
static_mutex_t audio_fftw_planner_lock
Definition spectral.c:34
#define SPECTRAL_MAX_BINS
Definition spectral.c:10
void spectral_destroy(spectral_processor_t *p)
Definition spectral.c:104
const float * spectral_spectrum(const spectral_processor_t *p, size_t *bins)
Definition spectral.c:295
void spectral_reset(spectral_processor_t *p)
Definition spectral.c:46
struct spectral_processor spectral_processor_t
Definition spectral.h:15
#define SPECTRAL_BAND_COUNT
Definition spectral.h:4
🔤 String Manipulation and Shell Escaping Utilities
For spectral config, this is the live record that ties sample_rate, fft_size, noise_gate,...
Definition spectral.h:19
float ratios[4]
Definition spectral.h:30
float crossovers[4 - 1]
Definition spectral.h:28
float thresholds_db[4]
Definition spectral.h:29
spectral processor keeps the values that move through the subsystem: config, forward,...
Definition spectral.c:17
fftw_plan forward
Definition spectral.c:19
float band_gain[SPECTRAL_BAND_COUNT]
Definition spectral.c:25
spectral_config_t config
Definition spectral.c:18
double time[2048]
Definition spectral.c:20
double shifted_frequency[(2048/2+1)]
Definition spectral.c:27
double previous_phase[(2048/2+1)]
Definition spectral.c:26
float band_average[4]
Definition spectral.c:25
float overlap[2048]
Definition spectral.c:22
float eq_gain[4]
Definition spectral.c:25
float window[2048]
Definition spectral.c:23
fftw_complex frequency[(2048/2+1)]
Definition spectral.c:21
double restored[2048]
Definition spectral.c:20
float input[2048]
Definition spectral.c:22
float smoothed[(2048/2+1)]
Definition spectral.c:24
double synthesis_phase[(2048/2+1)]
Definition spectral.c:26
float gains[(2048/2+1)]
Definition spectral.c:24
double shifted_magnitude[(2048/2+1)]
Definition spectral.c:27
fftw_plan inverse
Definition spectral.c:19
float magnitude[(2048/2+1)]
Definition spectral.c:23
float noise[(2048/2+1)]
Definition spectral.c:24
float ready[2048/4]
Definition spectral.c:22
Static mutex structure for global mutexes requiring static initialization.
Definition common/sync.h:45