ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
visualization.c
Go to the documentation of this file.
2
6
7#include <math.h>
8#include <stdio.h>
9#include <string.h>
10
11#define VISUALIZATION_HISTORY 48000
12#define MAX_TERMINAL_WIDTH 512
13#define MAX_TERMINAL_HEIGHT 256
14
15typedef struct {
16 float frequency;
17 unsigned char red;
18 unsigned char green;
19 unsigned char blue;
21
23static size_t write_position[AUDIO_VISUALIZATION_SOURCE_COUNT];
24static static_mutex_t history_mutex = STATIC_MUTEX_INIT;
25
26static void visualization_color_for_frequency(float centroid_hz, unsigned char *red, unsigned char *green,
27 unsigned char *blue) {
28 static const waveform_color_stop_t stops[] = {
29 {45.0f, 255, 72, 176}, {100.0f, 255, 54, 106}, {220.0f, 255, 104, 72}, {350.0f, 190, 86, 220},
30 {700.0f, 74, 112, 255}, {1400.0f, 55, 190, 255}, {2800.0f, 55, 226, 180}, {5200.0f, 150, 244, 92},
31 {10000.0f, 255, 224, 76}, {18000.0f, 255, 246, 150},
32 };
33 const size_t stop_count = sizeof(stops) / sizeof(stops[0]);
34 if (centroid_hz <= stops[0].frequency) {
35 *red = stops[0].red;
36 *green = stops[0].green;
37 *blue = stops[0].blue;
38 return;
39 }
40 for (size_t i = 1; i < stop_count; i++) {
41 if (centroid_hz <= stops[i].frequency) {
42 float low = logf(stops[i - 1].frequency);
43 float high = logf(stops[i].frequency);
44 float amount = (logf(centroid_hz) - low) / (high - low);
45 *red = (unsigned char)lrintf((float)stops[i - 1].red + amount * ((float)stops[i].red - stops[i - 1].red));
46 *green = (unsigned char)lrintf((float)stops[i - 1].green + amount * ((float)stops[i].green - stops[i - 1].green));
47 *blue = (unsigned char)lrintf((float)stops[i - 1].blue + amount * ((float)stops[i].blue - stops[i - 1].blue));
48 return;
49 }
50 }
51 *red = stops[stop_count - 1].red;
52 *green = stops[stop_count - 1].green;
53 *blue = stops[stop_count - 1].blue;
54}
55
56static void visualization_color_for_level(float frequency_hz, float level, int color_mode, unsigned char *red,
57 unsigned char *green, unsigned char *blue) {
58 visualization_color_for_frequency(frequency_hz, red, green, blue);
59 float brightness = 0.5f + 0.5f * level;
60 float white_mix = 0.0f;
61 if (color_mode == AUDIO_VISUALIZATION_COLOR_DARKER) {
62 brightness = 0.24f + 0.76f * level;
63 } else if (color_mode == AUDIO_VISUALIZATION_COLOR_BRIGHTER) {
64 brightness = 0.62f + 0.38f * level;
65 white_mix = 0.10f;
66 }
67 *red = (unsigned char)lrintf(fminf(255.0f, *red * brightness + 255.0f * white_mix));
68 *green = (unsigned char)lrintf(fminf(255.0f, *green * brightness + 255.0f * white_mix));
69 *blue = (unsigned char)lrintf(fminf(255.0f, *blue * brightness + 255.0f * white_mix));
70}
71
72static float waveform_spectral_centroid(const float *samples, size_t sample_count, size_t center) {
73 static const float frequencies[] = {55.0f, 110.0f, 220.0f, 440.0f, 880.0f, 1760.0f, 3520.0f, 7040.0f, 14080.0f};
74 const size_t window_size = 1024;
75 const size_t half_window = window_size / 2;
76 size_t start = center > half_window ? center - half_window : 0;
77 if (start + window_size > sample_count)
78 start = sample_count - window_size;
79
80 float weighted_log_frequency = 0.0f;
81 float total_weight = 0.0f;
82 for (size_t frequency_index = 0; frequency_index < sizeof(frequencies) / sizeof(frequencies[0]); frequency_index++) {
83 float coefficient = 2.0f * cosf(6.28318530718f * frequencies[frequency_index] / AUDIO_VISUALIZATION_SAMPLE_RATE);
84 float q1 = 0.0f, q2 = 0.0f;
85 for (size_t i = 0; i < window_size; i++) {
86 float position = (float)i / (float)(window_size - 1);
87 float taper = 1.0f - fabsf(2.0f * position - 1.0f);
88 float q0 = coefficient * q1 - q2 + samples[start + i] * taper;
89 q2 = q1;
90 q1 = q0;
91 }
92 float power = fmaxf(q1 * q1 + q2 * q2 - coefficient * q1 * q2, 0.0f);
93 float magnitude = sqrtf(power);
94 weighted_log_frequency += magnitude * logf(frequencies[frequency_index]);
95 total_weight += magnitude;
96 }
97 return total_weight > 0.0001f ? expf(weighted_log_frequency / total_weight) : 900.0f;
98}
99
100void audio_visualization_submit(audio_visualization_source_t source, const float *samples, size_t count) {
101 if ((!GET_OPTION(waveform) && !GET_OPTION(fft)) || !samples || source < AUDIO_VISUALIZATION_SOURCE_MIC ||
103 return;
104 static_mutex_lock(&history_mutex);
105 size_t position = write_position[source];
106 for (size_t i = 0; i < count; i++) {
107 float sample = samples[i];
108 history[source][position] = isfinite(sample) ? fmaxf(-1.0f, fminf(1.0f, sample)) : 0.0f;
109 position = (position + 1) % VISUALIZATION_HISTORY;
110 }
111 write_position[source] = position;
112 static_mutex_unlock(&history_mutex);
113}
114
115void audio_visualization_read(audio_visualization_source_t source, float *samples, size_t count) {
116 if (!samples)
117 return;
118 if (source < AUDIO_VISUALIZATION_SOURCE_MIC || source >= AUDIO_VISUALIZATION_SOURCE_COUNT) {
119 memset(samples, 0, count * sizeof(float));
120 return;
121 }
122 static_mutex_lock(&history_mutex);
123 size_t available_count = count < VISUALIZATION_HISTORY ? count : VISUALIZATION_HISTORY;
124 size_t output_offset = count - available_count;
125 for (size_t i = 0; i < count; i++)
126 samples[i] = 0.0f;
127 size_t first_source = source;
128 size_t last_source = source;
129 if (source == AUDIO_VISUALIZATION_SOURCE_MIX) {
130 first_source = AUDIO_VISUALIZATION_SOURCE_MIC;
132 } else if (source == AUDIO_VISUALIZATION_SOURCE_LOCAL_MIX) {
133 first_source = AUDIO_VISUALIZATION_SOURCE_MIC;
135 }
136 for (size_t current = first_source; current <= last_source; current++) {
137 size_t start = (write_position[current] + VISUALIZATION_HISTORY - available_count) % VISUALIZATION_HISTORY;
138 for (size_t i = 0; i < available_count; i++)
139 samples[output_offset + i] += history[current][(start + i) % VISUALIZATION_HISTORY];
140 }
141 static_mutex_unlock(&history_mutex);
142 for (size_t i = 0; i < count; i++)
143 samples[i] = fmaxf(-1.0f, fminf(1.0f, samples[i]));
144}
145
146char *audio_visualization_render_waveform(unsigned int width, unsigned int height, audio_visualization_source_t source,
147 bool use_color, int color_mode, bool flip_x, bool flip_y) {
148 if (width < 8 || height < 4 || width > MAX_TERMINAL_WIDTH || height > MAX_TERMINAL_HEIGHT ||
149 source < AUDIO_VISUALIZATION_SOURCE_MIC || source > AUDIO_VISUALIZATION_SOURCE_MIX)
150 return NULL;
151
152 /* Keep a fixed one-second view so new audio enters on the right smoothly. */
153 const size_t sample_count = VISUALIZATION_HISTORY;
154 float *samples = SAFE_MALLOC(sample_count * sizeof(float), float *);
155 if (!samples)
156 return NULL;
157 audio_visualization_read(source, samples, sample_count);
158
159 const unsigned int grid_height = height;
160 const unsigned int center = grid_height / 2;
161 const unsigned int half_height = grid_height - 1 - center;
162 const size_t cell_count = (size_t)width * grid_height;
163 char *cells = SAFE_MALLOC(cell_count, char *);
164 if (!cells) {
165 SAFE_FREE(samples);
166 return NULL;
167 }
168 memset(cells, ' ', cell_count);
169 unsigned char palette[MAX_TERMINAL_WIDTH][3] = {{0}};
170
171 float positive_peak = 0.0f;
172 float negative_peak = 0.0f;
173 size_t x = 0;
174
175 for (size_t i = 0; i < sample_count; i++) {
176 float sample = samples[i];
177 positive_peak = fmaxf(positive_peak, sample);
178 negative_peak = fmaxf(negative_peak, -sample);
179
180 size_t next_bucket = ((i + 1) * width) / sample_count;
181 if (next_bucket != x || i + 1 == sample_count) {
182 size_t begin = x * sample_count / width;
183 size_t end = (x + 1) * sample_count / width;
184 size_t bucket_center = begin + (end - begin) / 2;
185 float centroid = waveform_spectral_centroid(samples, sample_count, bucket_center);
186 /* A gentle square-root scale keeps speech visible without flattening loud peaks. */
187 float level = sqrtf(fminf(fmaxf(positive_peak, negative_peak), 1.0f));
188 visualization_color_for_level(centroid, level, color_mode, &palette[x][0], &palette[x][1], &palette[x][2]);
189 unsigned int positive_amplitude = (unsigned int)lrintf(sqrtf(fminf(positive_peak, 1.0f)) * (float)half_height);
190 unsigned int negative_amplitude = (unsigned int)lrintf(sqrtf(fminf(negative_peak, 1.0f)) * (float)half_height);
191 if (positive_amplitude > half_height)
192 positive_amplitude = half_height;
193 if (negative_amplitude > half_height)
194 negative_amplitude = half_height;
195 unsigned int top = center - positive_amplitude;
196 unsigned int bottom = center + negative_amplitude;
197 for (unsigned int y = top; y <= bottom; y++) {
198 char ch = (y == top || y == bottom) ? '+' : '#';
199 cells[(size_t)y * width + x] = ch;
200 }
201
202 /* Advance to the next time slice in the left-to-right history. */
203 positive_peak = 0.0f;
204 negative_peak = 0.0f;
205 x = next_bucket;
206 }
207 }
208
209 size_t capacity = cell_count * (use_color ? 24U : 1U) + (size_t)height + 1;
210 char *frame = SAFE_MALLOC(capacity, char *);
211 if (!frame) {
212 SAFE_FREE(cells);
213 SAFE_FREE(samples);
214 return NULL;
215 }
216 size_t used = 0;
217 for (unsigned int y = 0; y < grid_height; y++) {
218 for (unsigned int x = 0; x < width; x++) {
219 unsigned int source_x = flip_x ? width - x - 1 : x;
220 unsigned int source_y = flip_y ? grid_height - y - 1 : y;
221 char ch = cells[(size_t)source_y * width + source_x];
222 if (use_color && ch != ' ') {
223 used += (size_t)snprintf(frame + used, capacity - used, "\033[38;2;%u;%u;%um%c\033[0m", palette[source_x][0],
224 palette[source_x][1], palette[source_x][2], ch);
225 } else {
226 frame[used++] = ch;
227 }
228 }
229 frame[used++] = '\n';
230 }
231 frame[used] = '\0';
232 SAFE_FREE(cells);
233 SAFE_FREE(samples);
234 return frame;
235}
236
237#define FFT_SIZE 1024
238
239static void visualization_fft(float *real, float *imaginary) {
240 for (size_t i = 1, j = 0; i < FFT_SIZE; i++) {
241 size_t bit = FFT_SIZE >> 1;
242 for (; j & bit; bit >>= 1)
243 j ^= bit;
244 j ^= bit;
245 if (i < j) {
246 float swap = real[i];
247 real[i] = real[j];
248 real[j] = swap;
249 swap = imaginary[i];
250 imaginary[i] = imaginary[j];
251 imaginary[j] = swap;
252 }
253 }
254 for (size_t length = 2; length <= FFT_SIZE; length <<= 1) {
255 float angle = -6.28318530718f / (float)length;
256 float step_real = cosf(angle), step_imaginary = sinf(angle);
257 for (size_t base = 0; base < FFT_SIZE; base += length) {
258 float twiddle_real = 1.0f, twiddle_imaginary = 0.0f;
259 for (size_t offset = 0; offset < length / 2; offset++) {
260 size_t even = base + offset, odd = even + length / 2;
261 float odd_real = real[odd] * twiddle_real - imaginary[odd] * twiddle_imaginary;
262 float odd_imaginary = real[odd] * twiddle_imaginary + imaginary[odd] * twiddle_real;
263 real[odd] = real[even] - odd_real;
264 imaginary[odd] = imaginary[even] - odd_imaginary;
265 real[even] += odd_real;
266 imaginary[even] += odd_imaginary;
267 float next_real = twiddle_real * step_real - twiddle_imaginary * step_imaginary;
268 twiddle_imaginary = twiddle_real * step_imaginary + twiddle_imaginary * step_real;
269 twiddle_real = next_real;
270 }
271 }
272 }
273}
274
275char *audio_visualization_render_fft(unsigned int width, unsigned int height, audio_visualization_source_t source,
276 bool use_color, int color_mode, bool flip_x, bool flip_y) {
277 if (width < 16 || height < 6 || width > MAX_TERMINAL_WIDTH || height > MAX_TERMINAL_HEIGHT ||
278 source < AUDIO_VISUALIZATION_SOURCE_MIC || source > AUDIO_VISUALIZATION_SOURCE_MIX)
279 return NULL;
280
281 float *samples = SAFE_MALLOC(VISUALIZATION_HISTORY * sizeof(float), float *);
282 if (!samples)
283 return NULL;
285 size_t cell_count = (size_t)width * height;
286 char *cells = SAFE_MALLOC(cell_count, char *);
287 unsigned char *colors = SAFE_MALLOC(cell_count * 3, unsigned char *);
288 if (!cells || !colors) {
289 SAFE_FREE(colors);
290 SAFE_FREE(cells);
291 SAFE_FREE(samples);
292 return NULL;
293 }
294 memset(cells, ' ', cell_count);
295 memset(colors, 0, cell_count * 3);
296
297 /* Each column is a 21 ms Hann-windowed spectrum; adjacent columns overlap heavily. */
298 float real[FFT_SIZE], imaginary[FFT_SIZE];
299 static const char levels[] = " .:-=+*#%@";
300 const unsigned int spectrum_height = height;
301 for (unsigned int x = 0; x < width; x++) {
302 size_t center = (size_t)x * (VISUALIZATION_HISTORY - 1) / (width - 1);
303 size_t start = center > FFT_SIZE / 2 ? center - FFT_SIZE / 2 : 0;
304 if (start + FFT_SIZE > VISUALIZATION_HISTORY)
306 for (size_t i = 0; i < FFT_SIZE; i++) {
307 float window = 0.5f - 0.5f * cosf(6.28318530718f * (float)i / (float)(FFT_SIZE - 1));
308 real[i] = samples[start + i] * window;
309 imaginary[i] = 0.0f;
310 }
311 visualization_fft(real, imaginary);
312
313 for (unsigned int row = 0; row < spectrum_height; row++) {
314 float top_fraction = (float)(spectrum_height - row - 1) / (float)spectrum_height;
315 float bottom_fraction = (float)(spectrum_height - row) / (float)spectrum_height;
316 float low_hz = 35.0f * powf(18000.0f / 35.0f, top_fraction);
317 float high_hz = 35.0f * powf(18000.0f / 35.0f, bottom_fraction);
318 size_t low_bin = (size_t)fmaxf(1.0f, floorf(low_hz * FFT_SIZE / AUDIO_VISUALIZATION_SAMPLE_RATE));
319 size_t high_bin =
320 (size_t)fmaxf((float)(low_bin + 1), ceilf(high_hz * FFT_SIZE / AUDIO_VISUALIZATION_SAMPLE_RATE));
321 if (high_bin > FFT_SIZE / 2)
322 high_bin = FFT_SIZE / 2;
323 float power = 0.0f;
324 size_t bins = 0;
325 for (size_t bin = low_bin; bin < high_bin; bin++) {
326 float magnitude = hypotf(real[bin], imaginary[bin]) / (FFT_SIZE * 0.25f);
327 power += magnitude * magnitude;
328 bins++;
329 }
330 float magnitude = bins ? sqrtf(power / (float)bins) : 0.0f;
331 /* dB scaling leaves room for quiet speech while keeping loud music detailed. */
332 float db = 20.0f * log10f(fmaxf(magnitude, 0.00001f));
333 float intensity = fmaxf(0.0f, fminf(1.0f, (db + 58.0f) / 52.0f));
334 int level = (int)lrintf(intensity * (float)(sizeof(levels) - 2));
335 unsigned int source_x = flip_x ? width - x - 1 : x;
336 unsigned int source_row = flip_y ? height - row - 1 : row;
337 size_t cell = (size_t)source_row * width + source_x;
338 cells[cell] = levels[level];
339 float band_hz = sqrtf(low_hz * high_hz);
340 visualization_color_for_level(band_hz, intensity, color_mode, &colors[cell * 3], &colors[cell * 3 + 1],
341 &colors[cell * 3 + 2]);
342 }
343 }
344
345 size_t capacity = cell_count * (use_color ? 24U : 1U) + (size_t)height + 1;
346 char *frame = SAFE_MALLOC(capacity, char *);
347 if (!frame) {
348 SAFE_FREE(colors);
349 SAFE_FREE(cells);
350 SAFE_FREE(samples);
351 return NULL;
352 }
353 size_t used = 0;
354 for (unsigned int row = 0; row < height; row++) {
355 for (unsigned int x = 0; x < width; x++) {
356 size_t cell = (size_t)row * width + x;
357 char ch = cells[cell];
358 if (use_color && ch != ' ') {
359 used += (size_t)snprintf(frame + used, capacity - used, "\033[38;2;%u;%u;%um%c\033[0m", colors[cell * 3],
360 colors[cell * 3 + 1], colors[cell * 3 + 2], ch);
361 } else {
362 frame[used++] = ch;
363 }
364 }
365 frame[used++] = '\n';
366 }
367 frame[used] = '\0';
368 SAFE_FREE(colors);
369 SAFE_FREE(cells);
370 SAFE_FREE(samples);
371 return frame;
372}
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Platform initialization and static synchronization helpers.
🔢 Mathematical Utility Functions
Cross-platform memory allocation utilities.
int frame
Definition splash.c:99
Static mutex structure for global mutexes requiring static initialization.
void audio_visualization_submit(audio_visualization_source_t source, const float *samples, size_t count)
#define FFT_SIZE
#define MAX_TERMINAL_WIDTH
char * audio_visualization_render_waveform(unsigned int width, unsigned int height, audio_visualization_source_t source, bool use_color, int color_mode, bool flip_x, bool flip_y)
#define VISUALIZATION_HISTORY
#define MAX_TERMINAL_HEIGHT
void audio_visualization_read(audio_visualization_source_t source, float *samples, size_t count)
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)
audio_visualization_source_t
@ AUDIO_VISUALIZATION_SOURCE_MIX
@ AUDIO_VISUALIZATION_SOURCE_MEDIA
@ AUDIO_VISUALIZATION_SOURCE_LOCAL_MIX
@ AUDIO_VISUALIZATION_SOURCE_COUNT
@ AUDIO_VISUALIZATION_SOURCE_MIC
@ AUDIO_VISUALIZATION_SOURCE_REMOTE
#define AUDIO_VISUALIZATION_COLOR_BRIGHTER
#define AUDIO_VISUALIZATION_COLOR_DARKER
#define AUDIO_VISUALIZATION_SAMPLE_RATE