ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
fft.c
Go to the documentation of this file.
2
4
10
11#include <math.h>
12#include <string.h>
13#include <fftw3.h>
14
15#define FFT_SIZE 1024
16#define FFT_CACHE_CELLS (MAX_TERMINAL_WIDTH * MAX_TERMINAL_HEIGHT * 2)
17#define FFT_REFRESH_NS (100 * NS_PER_MS_INT)
18
19static static_mutex_t fft_cache_mutex = STATIC_MUTEX_INIT;
20static struct {
22 unsigned char colors[FFT_CACHE_CELLS * 3];
23 unsigned int width, height;
27 bool valid;
28} fft_cache;
29
30static static_mutex_t fftw_mutex = STATIC_MUTEX_INIT;
31static double fftw_input[FFT_SIZE];
32static fftw_complex fftw_output[FFT_SIZE / 2 + 1];
33static fftw_plan fftw_forward;
35
36static bool parse_fft_scale_value(const char *arg, fft_scale_t *parsed) {
37 parsed->mode = FFT_SCALE_DB;
38 parsed->min_db = -62.0f;
39 parsed->max_db = -6.0f;
40 if (!arg || !*arg)
41 return false;
42 if (!strcmp(arg, "linear")) {
43 parsed->mode = FFT_SCALE_LINEAR;
44 return true;
45 }
46 if (!strcmp(arg, "log")) {
47 parsed->mode = FFT_SCALE_LOG;
48 return true;
49 }
50 char *end = NULL;
51 parsed->min_db = strtof(arg, &end);
52 if (end == arg || *end != ':')
53 return false;
54 char *max_end = NULL;
55 parsed->max_db = strtof(end + 1, &max_end);
56 return max_end != end + 1 && *max_end == '\0' && isfinite(parsed->min_db) && isfinite(parsed->max_db) &&
57 parsed->min_db >= -120.0f && parsed->max_db <= 0.0f && parsed->max_db - parsed->min_db >= 12.0f &&
58 parsed->max_db > parsed->min_db;
59}
60
62 (void)NAMED_REGISTER_MUTEX(&fft_cache_mutex, "audio.visualization.fft_cache", NULL);
63 (void)NAMED_REGISTER_MUTEX(&fftw_mutex, "audio.visualization.fftw", NULL);
64 (void)NAMED_REGISTER_MUTEX(&audio_fftw_planner_lock, "audio.fftw_planner", NULL);
65 static_mutex_lock(&fftw_mutex);
66 if (!fftw_forward) {
67 static_mutex_lock(&audio_fftw_planner_lock);
68 fftw_forward = fftw_plan_dft_r2c_1d(FFT_SIZE, fftw_input, fftw_output, FFTW_ESTIMATE);
69 static_mutex_unlock(&audio_fftw_planner_lock);
70 }
71 bool ready = fftw_forward != NULL;
72 static_mutex_unlock(&fftw_mutex);
73 return ready ? ASCIICHAT_OK : SET_ERRNO(ERROR_MEMORY, "Cannot create FFTW visualization plan");
74}
75
76static bool visualization_fft(const float *input, float *magnitude) {
77 static_mutex_lock(&fftw_mutex);
78 if (!fftw_forward) {
79 static_mutex_unlock(&fftw_mutex);
80 return false;
81 }
82 for (size_t i = 0; i < FFT_SIZE; i++)
83 fftw_input[i] = input[i];
84 fftw_execute(fftw_forward);
85 for (size_t i = 0; i <= FFT_SIZE / 2; i++)
86 magnitude[i] = (float)hypot(fftw_output[i][0], fftw_output[i][1]) / (FFT_SIZE * 0.25f);
87 static_mutex_unlock(&fftw_mutex);
88 return true;
89}
90
92 bool use_color, int color_mode, bool flip_x, bool flip_y) {
95 return NULL;
96
97 if (GET_OPTION(render_mode) == RENDER_MODE_HALF_BLOCK)
98 height *= 2;
99 static_mutex_lock(&fft_cache_mutex);
100 uint64_t now = time_get_ns();
101 if (fft_cache.valid && fft_cache.width == width && fft_cache.height == height && fft_cache.source == source &&
102 fft_cache.color_mode == color_mode && now >= fft_cache.updated_ns &&
103 now - fft_cache.updated_ns < FFT_REFRESH_NS) {
104 char *frame =
105 visualization_encode(fft_cache.cells, fft_cache.colors, false, width, height, use_color, flip_x, flip_y);
106 static_mutex_unlock(&fft_cache_mutex);
107 return frame;
108 }
109 float *samples = SAFE_MALLOC(VISUALIZATION_HISTORY * sizeof(float), float *);
110 if (!samples) {
111 static_mutex_unlock(&fft_cache_mutex);
112 return NULL;
113 }
115 size_t cell_count = (size_t)width * height;
116 char *cells = fft_cache.cells;
117 unsigned char *colors = fft_cache.colors;
118 memset(cells, ' ', cell_count);
119 memset(colors, 0, cell_count * 3);
120
121 /* Each column is a 21 ms Hann-windowed spectrum; adjacent columns overlap heavily. */
122 float windowed[FFT_SIZE], magnitude[FFT_SIZE / 2 + 1];
123 fft_scale_t scale;
124 if (!parse_fft_scale_value(GET_OPTION(fft_scale), &scale))
125 scale = (fft_scale_t){.mode = FFT_SCALE_DB, .min_db = -62.0f, .max_db = -6.0f};
126 static const char levels[] = " .:-=+*#%@";
127 const unsigned int spectrum_height = height;
128 for (unsigned int x = 0; x < width; x++) {
129 size_t center = (size_t)x * (VISUALIZATION_HISTORY - 1) / (width - 1);
130 size_t start = center > FFT_SIZE / 2 ? center - FFT_SIZE / 2 : 0;
131 if (start + FFT_SIZE > VISUALIZATION_HISTORY)
133 for (size_t i = 0; i < FFT_SIZE; i++) {
134 float window = 0.5f - 0.5f * cosf(6.28318530718f * (float)i / (float)(FFT_SIZE - 1));
135 windowed[i] = samples[start + i] * window;
136 }
137 if (!visualization_fft(windowed, magnitude)) {
138 SAFE_FREE(samples);
139 static_mutex_unlock(&fft_cache_mutex);
140 return NULL;
141 }
142
143 for (unsigned int row = 0; row < spectrum_height; row++) {
144 float top_fraction = (float)(spectrum_height - row - 1) / (float)spectrum_height;
145 float bottom_fraction = (float)(spectrum_height - row) / (float)spectrum_height;
146 float low_hz = 35.0f * powf(18000.0f / 35.0f, top_fraction);
147 float high_hz = 35.0f * powf(18000.0f / 35.0f, bottom_fraction);
148 size_t low_bin = (size_t)fmaxf(1.0f, floorf(low_hz * FFT_SIZE / AUDIO_VISUALIZATION_SAMPLE_RATE));
149 size_t high_bin =
150 (size_t)fmaxf((float)(low_bin + 1), ceilf(high_hz * FFT_SIZE / AUDIO_VISUALIZATION_SAMPLE_RATE));
151 if (high_bin > FFT_SIZE / 2)
152 high_bin = FFT_SIZE / 2;
153 float power = 0.0f;
154 size_t bins = 0;
155 for (size_t bin = low_bin; bin < high_bin; bin++) {
156 power += magnitude[bin] * magnitude[bin];
157 bins++;
158 }
159 /* The default dB range leaves room for quiet speech while keeping loud music detailed. */
160 float amplitude = bins ? sqrtf(power / (float)bins) : 0.0f;
161 float intensity;
162 if (scale.mode == FFT_SCALE_LINEAR)
163 intensity = fminf(1.0f, amplitude * 4.0f);
164 else if (scale.mode == FFT_SCALE_LOG)
165 intensity = fminf(1.0f, log1pf(amplitude * 31.0f) / log1pf(31.0f));
166 else {
167 float db = 20.0f * log10f(fmaxf(amplitude, 0.00001f));
168 intensity = fmaxf(0.0f, fminf(1.0f, (db - scale.min_db) / (scale.max_db - scale.min_db)));
169 }
170 int level = (int)lrintf(intensity * (float)(sizeof(levels) - 2));
171 size_t cell = (size_t)row * width + x;
172 cells[cell] = levels[level];
173 float band_hz = sqrtf(low_hz * high_hz);
174 visualization_color_for_level(band_hz, intensity, color_mode, &colors[cell * 3], &colors[cell * 3 + 1],
175 &colors[cell * 3 + 2]);
176 }
177 }
178
179 fft_cache.width = width;
180 fft_cache.height = height;
181 fft_cache.source = source;
182 fft_cache.color_mode = color_mode;
183 fft_cache.updated_ns = time_get_ns();
184 fft_cache.valid = true;
185 char *frame = visualization_encode(cells, colors, false, width, height, use_color, flip_x, flip_y);
186 SAFE_FREE(samples);
187 static_mutex_unlock(&fft_cache_mutex);
188 return frame;
189}
#define SET_ERRNO(code,...)
Named object registry for debugging — log identifiable resource names.
int color_mode
Definition fft.c:25
char cells[(512 *256 *2)]
Definition fft.c:21
bool valid
Definition fft.c:27
asciichat_error_t audio_visualization_fft_init(void)
Definition fft.c:61
uint64_t updated_ns
Definition fft.c:26
unsigned char colors[(512 *256 *2) *3]
Definition fft.c:22
#define FFT_SIZE
Definition fft.c:15
#define FFT_REFRESH_NS
Definition fft.c:17
audio_visualization_source_t source
Definition fft.c:24
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
#define FFT_CACHE_CELLS
Definition fft.c:16
char * audio_visualization_render_fft(unsigned int width, unsigned int height, audio_visualization_source_t source, bool use_color, int color_mode, bool flip_x, bool flip_y)
Definition fft.c:91
@ FFT_SCALE_LOG
Definition fft.h:8
@ FFT_SCALE_LINEAR
Definition fft.h:8
@ FFT_SCALE_DB
Definition fft.h:8
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
unsigned long long uint64_t
Definition common.h:63
#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
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
#define STATIC_MUTEX_INIT
🔢 Mathematical Utility Functions
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
static_mutex_t audio_fftw_planner_lock
Definition spectral.c:34
🔤 String Manipulation and Shell Escaping Utilities
fft_scale_mode_t mode
Definition fft.h:11
float max_db
Definition fft.h:13
float min_db
Definition fft.h:12
Static mutex structure for global mutexes requiring static initialization.
Definition common/sync.h:45
⏱️ High-precision timing utilities using sokol_time.h and uthash
char * visualization_encode(const char *cells, const unsigned char *colors, bool column_colors, unsigned int width, unsigned int height, bool use_color, bool flip_x, bool flip_y)
void visualization_color_for_level(float frequency_hz, float level, int color_mode, unsigned char *red, unsigned char *green, unsigned char *blue)
void audio_visualization_read(audio_visualization_source_t source, float *samples, size_t count)
audio_visualization_source_t
@ AUDIO_VISUALIZATION_SOURCE_MIX
#define AUDIO_VISUALIZATION_SAMPLE_RATE
#define MAX_TERMINAL_WIDTH
#define VISUALIZATION_HISTORY
#define MAX_TERMINAL_HEIGHT