ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
src/common/session/display.c
Go to the documentation of this file.
1
13#include "session/display.h"
14#include "session/render.h"
16#include <ascii-chat/common.h>
17#include <ascii-chat/log/log.h>
37
38#include <string.h>
39#include <stdio.h>
40#include <ascii-chat/atomic.h>
41#include <stdlib.h>
42
43/* ============================================================================
44 * Session Display Context Structure
45 * ============================================================================ */
46
111
112/* ============================================================================
113 * Internal Helper Functions
114 * ============================================================================ */
115
124/* ============================================================================
125 * Session Display Lifecycle Functions
126 * ============================================================================ */
127
129 if (!ctx) {
130 return SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL");
131 }
132
133 const char *render_file_opt = GET_OPTION(render_file);
134 if (!render_file_opt || render_file_opt[0] == '\0') {
135 return ASCIICHAT_OK;
136 }
137 if (ctx->render_file) {
138 return ASCIICHAT_OK;
139 }
140
141 uint32_t encoder_fps = fps;
142 if (encoder_fps == 0) {
143 encoder_fps = (uint32_t)GET_OPTION(fps);
144 }
145 if (encoder_fps == 0) {
146 encoder_fps = 60;
147 }
148
149 int width = (int)GET_OPTION(width);
150 int height = (int)GET_OPTION(height);
151 ctx->render_fps = encoder_fps;
152
153 log_info("render-file: Initializing encoder after media timing selection: path='%s', grid=%dx%d, fps=%u",
154 render_file_opt, width, height, encoder_fps);
156 render_file_create(render_file_opt, width, height, (int)encoder_fps, GET_OPTION(render_theme), &ctx->render_file);
157 if (err != ASCIICHAT_OK) {
158 log_error("render-file: Failed to create encoder with error %d", err);
159 return err;
160 }
161
162 log_info("render-file: Encoder initialized successfully at %u FPS", encoder_fps);
163 return ASCIICHAT_OK;
164}
165
167 // Auto-create config from command-line options if NULL
168 session_display_config_t auto_config = {0};
169 if (!config) {
170 auto_config.snapshot_mode = GET_OPTION(snapshot_mode);
171 auto_config.palette_type = GET_OPTION(palette_type);
172 auto_config.custom_palette = GET_OPTION(palette_custom_set) ? GET_OPTION(palette_custom) : NULL;
173 auto_config.color_mode = TERM_COLOR_AUTO;
174 config = &auto_config;
175 }
176
177 // Check if we should exit before starting initialization
178 if (config->should_exit_callback && config->should_exit_callback(config->callback_data)) {
179 return NULL;
180 }
181
182 // Allocate context
184
185 // Store configuration
186 ctx->snapshot_mode = config->snapshot_mode;
187 ctx->palette_type = config->palette_type;
189 ctx->audio_ctx = config->audio_ctx;
190 ctx->render_fps = config->render_fps;
191 atomic_store_bool(&ctx->first_frame, true);
193
194 // Initialize FPS counter
196
197 // Get TTY info for direct terminal access
198 ctx->tty_info = get_current_tty();
199
200 // Determine if we have a valid TTY
201 // Check if stdout is a TTY, not just any fd (stdin/stderr could be TTY while stdout is piped).
202 // If stdout is piped/redirected, never perform terminal operations regardless of other fds.
203 if (ctx->tty_info.fd >= 0) {
205 }
206
207 // In piped mode, force all logs to stderr to prevent frame data corruption
210 // Disable buffering for stdout to ensure frames appear immediately
211 // Each frame ends with newline, so unbuffered output is critical for real-time display
212 (void)setvbuf(stdout, NULL, _IONBF, 0);
213 }
214
215 // Also disable buffering for TTY mode to ensure smooth 40+ FPS animation
216 setvbuf(stdout, NULL, _IONBF, 0);
217
218 // Detect terminal capabilities
220
221 // Set wants_padding based on terminal output mode
222 // Enable padding for all rendering modes (including snapshot) to center output
223 // The padding adds newlines at the top, which is useful for visual centering
224 bool is_snapshot_mode = config->snapshot_mode;
225 ctx->caps.wants_padding = true;
226
227 // Calculate pad_height: when padding is enabled, add top padding for centering
228 // Halfblock mode uses 2 source rows per output row, so padding is halved
229 if (ctx->caps.wants_padding) {
230 ctx->caps.pad_height = 5; // Halfblock needs half the padding (10 / 2 = 5)
231 } else {
232 ctx->caps.pad_height = 0;
233 }
234
235 log_debug("Padding mode: wants_padding=%d (snapshot=%d, has_tty=%d, stdin_tty=%d, stdout_tty=%d), pad_height=%zu",
236 ctx->caps.wants_padding, is_snapshot_mode, ctx->has_tty, terminal_is_stdin_tty(), terminal_is_stdout_tty(),
237 ctx->caps.pad_height);
238
239 // Apply color mode override if specified
240 if (config->color_mode != TERM_COLOR_AUTO) {
241 ctx->caps.color_level = config->color_mode;
242 }
243
244 // Apply command-line overrides
246
247 // Initialize palette
248 int palette_result = initialize_client_palette(config->palette_type, config->custom_palette, ctx->palette_chars,
249 &ctx->palette_len, ctx->luminance_palette);
250 if (palette_result != ASCIICHAT_OK) {
251 log_warn("Failed to initialize palette, using default");
252 // Fall back to standard palette
254 ctx->luminance_palette);
255 }
256
257 // Pre-warm UTF-8 palette cache during initialization (not during rendering)
258 // This is expensive on first use (creates lookup tables), so warm it up now to avoid frame lag
259 // This function is imported from lib/video/simd/common.h
260 (void)get_utf8_palette_cache((const char *)ctx->palette_chars);
261 log_debug("UTF-8 palette cache pre-warmed during display initialization");
262
263 // Initialize ANSI fast lookup tables based on terminal capabilities
266 } else if (ctx->caps.color_level == TERM_COLOR_256) {
268 } else if (ctx->caps.color_level == TERM_COLOR_16) {
270 }
271 // TERM_COLOR_MONO requires no init
272
273 // Initialize ASCII output system if we have a TTY
274 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
275 ascii_write_init(ctx->tty_info.fd, false);
276 }
277
278 // Initialize digital rain effect if enabled
279 if (GET_OPTION(matrix_rain)) {
280 // Get terminal dimensions for grid size
281 unsigned short int width_us = terminal_get_effective_width();
282 unsigned short int height_us = terminal_get_effective_height();
283
284 int width = (int)width_us;
285 int height = (int)height_us;
286
287 ctx->digital_rain = digital_rain_init(width, height);
288 if (ctx->digital_rain) {
289 // Set color from color filter if active
290 color_filter_t filter = GET_OPTION(color_filter);
292 log_info("Digital rain effect enabled: %dx%d grid", width, height);
293 } else {
294 log_warn("Failed to initialize digital rain effect");
295 }
297 }
298
299 // Initialize render-file unless media setup needs to determine its timing first.
300 const char *render_file_opt = GET_OPTION(render_file);
301 log_info("DISPLAY_CREATE: render-file opt='%s' (len=%zu), deferred=%d", render_file_opt ? render_file_opt : "(null)",
302 render_file_opt ? strlen(render_file_opt) : 0, config->defer_render_file);
303 if (!config->defer_render_file) {
305 if (render_err != ASCIICHAT_OK) {
306 log_error("render-file: Initialization failed with error %d", render_err);
307 }
308 }
309
310 ctx->initialized = true;
311 return ctx;
312}
313
315 if (!ctx) {
316 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
317 return;
318 }
319
320 // Cleanup ASCII rendering if we had a TTY
321 // Don't reset terminal in snapshot mode to preserve the rendered output
322 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
324 }
325
326 // Close the controlling terminal if we opened it
327 if (ctx->tty_info.owns_fd && ctx->tty_info.fd >= 0) {
328 (void)platform_close(ctx->tty_info.fd);
329 ctx->tty_info.fd = -1;
330 ctx->tty_info.owns_fd = false;
331 }
332
333 // Cleanup digital rain effect
334 if (ctx->digital_rain) {
336 ctx->digital_rain = NULL;
337 }
338
339 // Cleanup FPS counter
340 if (ctx->fps_counter) {
342 ctx->fps_counter = NULL;
343 }
344
345 // Cleanup render-file if active
346 if (ctx->render_file) {
348 ctx->render_file = NULL;
349 }
350
351 ctx->initialized = false;
352
353 // Clear the cached display context in splash state to prevent use-after-free
354 // when worker threads try to access the display after it's been freed
356
357 SAFE_FREE(ctx);
358}
359
361 if (!ctx) {
362 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL");
363 return;
364 }
365
366 ctx->stdin_reader = (terminal_fd_reader_t *)reader;
367 log_debug("session_display: stdin_reader set");
368}
369
370/* ============================================================================
371 * Session Display Query Functions
372 * ============================================================================ */
373
375 if (!ctx || !ctx->initialized) {
376 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
377 return false;
378 }
379 return ctx->has_tty;
380}
381
383 if (!ctx || !ctx->initialized) {
384 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
385 return NULL;
386 }
387 return &ctx->caps;
388}
389
391 if (!ctx || !ctx->initialized) {
392 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
393 return NULL;
394 }
395 return ctx->palette_chars;
396}
397
399 if (!ctx || !ctx->initialized) {
400 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
401 return 0;
402 }
403 return ctx->palette_len;
404}
405
407 if (!ctx || !ctx->initialized) {
408 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
409 return NULL;
410 }
411 return ctx->luminance_palette;
412}
413
415 if (!ctx || !ctx->initialized) {
416 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
417 return -1;
418 }
419 return ctx->tty_info.fd;
420}
421
423 if (!ctx || !ctx->initialized) {
424 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
425 return NULL;
426 }
427
428 return ctx->stdin_reader;
429}
430
432 if (!ctx)
433 return 0;
434 return ctx->render_fps;
435}
436
438 if (!ctx)
439 return;
440 ctx->render_fps = fps;
441 if (fps > 0) {
442 log_debug("session_display_set_render_fps: Updated render FPS to %u (for correct video duration)", fps);
443 }
444}
445
447 if (!ctx) {
448 return false;
449 }
450 // Return true if first_frame flag is false (meaning first frame has been rendered)
451 return !atomic_load_bool(&ctx->first_frame);
452}
453
455 if (!ctx) {
456 return;
457 }
458 // Reset first_frame flag to true so splash screen can be shown again on next render
459 atomic_store_bool(&ctx->first_frame, true);
460}
461
463 if (!ctx) {
464 return false;
465 }
466 return ctx->render_file != NULL;
467}
468
469/* ============================================================================
470 * Session Display ASCII Conversion Functions
471 * ============================================================================ */
472
474 if (!ctx) {
475 SET_ERRNO(ERROR_INVALID_PARAM, "session_display_convert_to_ascii: ctx is NULL");
476 return NULL;
477 }
478
479 if (!ctx->initialized) {
480 SET_ERRNO(ERROR_INVALID_STATE, "session_display_convert_to_ascii: ctx not initialized");
481 return NULL;
482 }
483
484 if (!image) {
485 SET_ERRNO(ERROR_INVALID_PARAM, "session_display_convert_to_ascii: image is NULL");
486 return NULL;
487 }
488
489 // Get conversion parameters from command-line options
490 unsigned short int width = GET_OPTION(width);
491 unsigned short int height = GET_OPTION(height);
492 bool stretch = GET_OPTION(stretch);
493 bool preserve_aspect_ratio = !stretch;
494
495 // Determine if we should apply flip_x and flip_y
496 bool flip_x_enabled = GET_OPTION(flip_x);
497 bool flip_y_enabled = GET_OPTION(flip_y);
498
499 color_filter_t color_filter = GET_OPTION(color_filter);
500
501 // Handle dynamic matrix rain effect toggle
502 bool matrix_rain_enabled = GET_OPTION(matrix_rain);
503 if (matrix_rain_enabled && !ctx->digital_rain) {
504 // Initialize digital rain if it's now enabled but wasn't before
505 unsigned short int width_us = terminal_get_effective_width();
506 unsigned short int height_us = terminal_get_effective_height();
507 int width_for_rain = (int)width_us;
508 int height_for_rain = (int)height_us;
509 ctx->digital_rain = digital_rain_init(width_for_rain, height_for_rain);
510 if (ctx->digital_rain) {
512 log_info("Matrix rain effect: enabled");
513 }
514 } else if (!matrix_rain_enabled && ctx->digital_rain) {
515 // Disable digital rain if it's now disabled but was enabled before
517 ctx->digital_rain = NULL;
518 log_info("Matrix rain effect: disabled");
519 }
520
521 // Make a mutable copy of terminal capabilities for ascii_convert_with_capabilities
522 terminal_capabilities_t caps_copy = ctx->caps;
523
524 // Re-evaluate render_mode and color_level on every frame to pick up live changes
525 caps_copy.render_mode = (render_mode_t)GET_OPTION(render_mode);
526 terminal_color_mode_t color_mode_opt = (terminal_color_mode_t)GET_OPTION(color_mode);
527 if (color_mode_opt != TERM_COLOR_AUTO) {
528 caps_copy.color_level = color_mode_opt;
529 }
530
531 // MEASURE EVERY OPERATION - Debug systematic timing
532 uint64_t t_flip_start = time_get_ns();
533
534 // Apply horizontal and/or vertical flips if requested
535 image_t *flipped_image = NULL;
536 const image_t *display_image = image;
537
538 if ((flip_x_enabled || flip_y_enabled) && image->w > 1 && image->h > 1 && image->pixels) {
539 START_TIMER("image_flip");
540 uint64_t t_flip_alloc_start = time_get_ns();
541 flipped_image = image_new((size_t)image->w, (size_t)image->h);
542 uint64_t t_flip_alloc_end = time_get_ns();
543
544 if (flipped_image) {
545 uint64_t t_flip_memcpy_start = time_get_ns();
546 // OPTIMIZATION: Copy entire image first (sequential memory access - cache-friendly)
547 memcpy(flipped_image->pixels, image->pixels, (size_t)image->w * (size_t)image->h * sizeof(rgb_pixel_t));
548 uint64_t t_flip_memcpy_end = time_get_ns();
549
550 uint64_t t_flip_reverse_start = time_get_ns();
551
552 // Apply horizontal flip (X-axis)
553 if (flip_x_enabled) {
554#if SIMD_SUPPORT_NEON
555 // Use NEON-accelerated flip on ARM processors
556 image_flip_horizontal_neon(flipped_image);
557#else
558 // Scalar fallback for non-NEON systems
559 for (int y = 0; y < image->h; y++) {
560 rgb_pixel_t *row = &flipped_image->pixels[y * image->w];
561 for (int x = 0; x < image->w / 2; x++) {
562 rgb_pixel_t temp = row[x];
563 row[x] = row[image->w - 1 - x];
564 row[image->w - 1 - x] = temp;
565 }
566 }
567#endif
568 }
569
570 // Apply vertical flip (Y-axis)
571 if (flip_y_enabled) {
572 for (int y = 0; y < image->h / 2; y++) {
573 rgb_pixel_t *top_row = &flipped_image->pixels[y * image->w];
574 rgb_pixel_t *bottom_row = &flipped_image->pixels[(image->h - 1 - y) * image->w];
575 for (int x = 0; x < image->w; x++) {
576 rgb_pixel_t temp = top_row[x];
577 top_row[x] = bottom_row[x];
578 bottom_row[x] = temp;
579 }
580 }
581 }
582
583 uint64_t t_flip_reverse_end = time_get_ns();
584 display_image = flipped_image;
585
586 log_dev("TIMING_FLIP: alloc=%llu us, memcpy=%llu us, flip=%llu us (x=%d, y=%d)",
587 (t_flip_alloc_end - t_flip_alloc_start) / 1000, (t_flip_memcpy_end - t_flip_memcpy_start) / 1000,
588 (t_flip_reverse_end - t_flip_reverse_start) / 1000, flip_x_enabled, flip_y_enabled);
589 }
590 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 3 * NS_PER_MS_INT, "image_flip",
591 "IMAGE_FLIP: Flip complete (%.2f ms)");
592 }
593 uint64_t t_flip_end = time_get_ns();
594
595 uint64_t t_filter_start = time_get_ns();
596
597 // Apply color filter to RGB pixels before ASCII conversion
598 // Rainbow filter is handled separately via ANSI color replacement
599 image_t *filtered_image = NULL;
600 if (color_filter != COLOR_FILTER_NONE && color_filter != COLOR_FILTER_RAINBOW) {
601 // Create a copy to avoid modifying the original
602 filtered_image = image_new((size_t)display_image->w, (size_t)display_image->h);
603 if (filtered_image && filtered_image->pixels && display_image->pixels) {
604 memcpy(filtered_image->pixels, display_image->pixels,
605 (size_t)display_image->w * (size_t)display_image->h * sizeof(rgb_pixel_t));
606
607 // Apply filter to the copy
608 int stride = (int)display_image->w * 3;
609 float time_seconds = (float)t_filter_start / (float)NS_PER_SEC_INT;
610 apply_color_filter((uint8_t *)filtered_image->pixels, (uint32_t)display_image->w, (uint32_t)display_image->h,
611 (uint32_t)stride, color_filter, time_seconds);
612 }
613 }
614
615 const image_t *ascii_input_image = filtered_image ? filtered_image : display_image;
616 uint64_t t_filter_end = time_get_ns();
617
618 uint64_t t_convert_start = time_get_ns();
619 // Call the standard ASCII conversion using the filtered (or original) image
620 START_TIMER("ascii_convert_with_capabilities");
621 char *result = ascii_convert_with_capabilities(ascii_input_image, width, height, &caps_copy, preserve_aspect_ratio,
622 stretch, ctx->palette_chars);
623 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "ascii_convert_with_capabilities",
624 "ASCII_CONVERT: Conversion complete (%.2f ms)");
625 uint64_t t_convert_end = time_get_ns();
626
627 // Apply rainbow color filter to ANSI output by replacing RGB values
628 // This preserves character selection while applying the filter colors
629 if (result && color_filter == COLOR_FILTER_RAINBOW) {
630 uint64_t t_color_replace_start = time_get_ns();
631 float time_seconds = (float)t_filter_start / (float)NS_PER_SEC_INT;
632
633 char *rainbow_result = rainbow_replace_ansi_colors(result, time_seconds);
634 if (rainbow_result) {
635 SAFE_FREE(result);
636 result = rainbow_result;
637 }
638
639 uint64_t t_color_replace_end = time_get_ns();
640 char color_replace_str[32];
641 time_pretty(t_color_replace_end - t_color_replace_start, -1, color_replace_str, sizeof(color_replace_str));
642 log_dev("COLOR_REPLACE: %s", color_replace_str);
643 }
644
645 // Apply digital rain effect if enabled
646 if (result && ctx->digital_rain) {
647 uint64_t t_rain_start = time_get_ns();
648 uint64_t current_time_ns = t_rain_start;
649 float delta_time = (float)(current_time_ns - ctx->last_frame_time_ns) / (float)NS_PER_SEC_INT;
650 ctx->last_frame_time_ns = current_time_ns;
651
652 // Update digital rain color from current filter (allows live filter changes)
653 color_filter_t current_filter = GET_OPTION(color_filter);
655
656 char *rain_result = digital_rain_apply(ctx->digital_rain, result, delta_time);
657 if (rain_result) {
658 SAFE_FREE(result);
659 result = rain_result;
660 }
661
662 uint64_t t_rain_end = time_get_ns();
663 char rain_str[32];
664 time_pretty(t_rain_end - t_rain_start, -1, rain_str, sizeof(rain_str));
665 log_dev("DIGITAL_RAIN: Effect applied (%s)", rain_str);
666 }
667
668 uint64_t t_cleanup_start = time_get_ns();
669 // Clean up flipped and filtered images if created
670 START_TIMER("ascii_convert_cleanup");
671 if (filtered_image) {
672 image_destroy(filtered_image);
673 }
674 if (flipped_image) {
675 image_destroy(flipped_image);
676 }
677 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 2 * NS_PER_MS_INT, "ascii_convert_cleanup",
678 "ASCII_CONVERT_CLEANUP: Cleanup complete (%.2f ms)");
679 uint64_t t_cleanup_end = time_get_ns();
680
681 // Log total breakdown with actual measured times
682 log_dev("CONVERT_TIMING: flip=%llu us, filter=%llu us, convert=%llu us, cleanup=%llu us, TOTAL=%llu us",
683 (t_flip_end - t_flip_start) / 1000, (t_filter_end - t_filter_start) / 1000,
684 (t_convert_end - t_convert_start) / 1000, (t_cleanup_end - t_cleanup_start) / 1000,
685 (t_cleanup_end - t_flip_start) / 1000);
686
687 return result;
688}
689
690/* ============================================================================
691 * Session Display Rendering Functions
692 * ============================================================================ */
693
694/* Forward declaration for FPS overlay rendering */
696
697static char *session_display_create_visualization_frame(session_display_ctx_t *ctx) {
698 unsigned int width = terminal_get_effective_width();
699 unsigned int height = terminal_get_effective_height();
700 bool use_color = ctx->caps.color_level != TERM_COLOR_NONE && GET_OPTION(color) != COLOR_SETTING_FALSE;
701 int color_mode =
703 audio_source_t selected_audio = GET_OPTION(audio_source);
705 if (selected_audio == AUDIO_SOURCE_MEDIA)
706 visual_source = AUDIO_VISUALIZATION_SOURCE_MEDIA;
707 else if (selected_audio == AUDIO_SOURCE_MIC)
708 visual_source = AUDIO_VISUALIZATION_SOURCE_MIC;
709 else if (selected_audio == AUDIO_SOURCE_CALL)
710 visual_source = AUDIO_VISUALIZATION_SOURCE_REMOTE;
711 if (GET_OPTION(fft))
712 return audio_visualization_render_fft(width, height, visual_source, use_color, color_mode, GET_OPTION(flip_x),
713 GET_OPTION(flip_y));
714 return audio_visualization_render_waveform(width, height, visual_source, use_color, color_mode, GET_OPTION(flip_x),
715 GET_OPTION(flip_y));
716}
717
718void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data) {
719 // Legacy function: shim that calls both new split functions for backward compatibility
720 // This maintains API compatibility with existing code while delegating to specialized functions
721
722 if (!ctx || !frame_data) {
723 SET_ERRNO(ERROR_INVALID_PARAM, "Display context or frame data is NULL");
724 return;
725 }
726
727 // Write the selected visualization to the render file as well as the terminal.
728 char *visualization_frame = NULL;
729 const char *render_frame = frame_data;
730 if (GET_OPTION(waveform) || GET_OPTION(fft)) {
731 visualization_frame = session_display_create_visualization_frame(ctx);
732 if (visualization_frame)
733 render_frame = visualization_frame;
734 }
735
736 // Write ASCII to terminal (main thread output path)
737 if (ctx && ctx->render_file) {
740 if (err != ASCIICHAT_OK)
741 log_warn_every(5 * NS_PER_SEC_INT, "Recording received frame failed: %s", asciichat_error_string(err));
742 }
743 SAFE_FREE(visualization_frame);
744 session_display_write_ascii(ctx, frame_data);
745}
746
748 if (!ctx || !ctx->initialized) {
749 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL or uninitialized");
750 return;
751 }
752
753 if (!ascii) {
754 SET_ERRNO(ERROR_INVALID_PARAM, "ASCII data is NULL");
755 return;
756 }
757
758 // Re-evaluate color_level on every frame to pick up live color_mode changes
760 if (color_mode != TERM_COLOR_AUTO) {
761 ctx->caps.color_level = color_mode;
762 }
763
764 // Suppress frame rendering when help screen is active
766 return;
767 }
768
769 bool visualization_enabled = GET_OPTION(waveform) || GET_OPTION(fft);
770 char *visualization_frame = visualization_enabled ? session_display_create_visualization_frame(ctx) : NULL;
771 if (visualization_frame)
772 ascii = visualization_frame;
773
774 // Calculate frame length
775 size_t frame_len = strnlen(ascii, 1024 * 1024); // Max 1MB frame
776 if (frame_len == 0) {
777 SET_ERRNO(ERROR_INVALID_PARAM, "ASCII data is empty");
778 SAFE_FREE(visualization_frame);
779 return;
780 }
781
782 // Apply digital rain effect if enabled
783 char *display_frame = (char *)ascii;
784 char *rain_result = NULL;
785 if (ctx->digital_rain && !visualization_enabled) {
786 uint64_t t_rain_start = time_get_ns();
787 uint64_t current_time_ns = t_rain_start;
788 float delta_time = (float)(current_time_ns - ctx->last_frame_time_ns) / (float)NS_PER_SEC_INT;
789 ctx->last_frame_time_ns = current_time_ns;
790
791 // Update digital rain color from current filter
792 color_filter_t current_filter = GET_OPTION(color_filter);
794
795 rain_result = digital_rain_apply(ctx->digital_rain, (char *)ascii, delta_time);
796 if (rain_result) {
797 display_frame = rain_result;
798 frame_len = strnlen(rain_result, 1024 * 1024);
799
800 uint64_t t_rain_end = time_get_ns();
801 char rain_str[32];
802 time_pretty(t_rain_end - t_rain_start, -1, rain_str, sizeof(rain_str));
803 log_info("DIGITAL_RAIN (write_ascii): Effect applied (%s)", rain_str);
804 }
805 }
806
807 // Handle first frame - perform initial terminal reset and splash cleanup
808 bool was_first_frame = atomic_exchange_bool(&ctx->first_frame, false);
809 if (was_first_frame) {
810 // Stop splash screen when first frame is ready
812
813 // Perform initial terminal reset
814 if (ctx->has_tty && !ctx->render_file) {
815 (void)terminal_reset(STDOUT_FILENO);
816 (void)terminal_clear_screen();
817 (void)terminal_cursor_home(STDOUT_FILENO);
818 (void)terminal_clear_scrollback(STDOUT_FILENO);
819 (void)terminal_cursor_show();
820 if (!ctx->snapshot_mode) {
821 (void)terminal_cursor_hide();
822 }
823 (void)terminal_flush(STDOUT_FILENO);
824 }
825 }
826
827 // Output routing logic
828 bool use_tty_control = ctx->has_tty && (!ctx->snapshot_mode || terminal_is_interactive());
829
830 START_TIMER("frame_write");
831
832 // Increment shared frame counter for both client and host rendering
833 // This metric is used by debug scripts to measure FPS across all modes
834 static atomic_t g_display_frames_rendered = {0};
835 int frame_number = atomic_fetch_add_u64(&g_display_frames_rendered, 1) + 1;
836 log_dev("[FRAMES_RENDERED_TOTAL] %d", frame_number);
837
838 if (use_tty_control) {
839 // TTY mode: Buffer cursor control + frame data together for atomic frame display
840 const char *cursor_home_sequence = "\033[H\033[3J"; // 7 bytes total
841 size_t cursor_seq_len = 7;
842 size_t total_size = cursor_seq_len + frame_len;
843
844 char *frame_buffer = SAFE_MALLOC(total_size, char *);
845 if (frame_buffer) {
846 memcpy(frame_buffer, cursor_home_sequence, cursor_seq_len);
847 memcpy(frame_buffer + cursor_seq_len, display_frame, frame_len);
848
849 log_debug("FRAME_WRITE_TTY: Writing %zu bytes (cursor=%zu + frame=%zu) to stdout", total_size, cursor_seq_len,
850 frame_len);
851 ssize_t written = platform_write_all(STDOUT_FILENO, frame_buffer, total_size);
852 log_debug("FRAME_WRITE_TTY: Wrote %zd bytes (requested %zu)", written, total_size);
853
854 // Start snapshot timer on first ASCII frame rendered
855 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
858 log_info("SNAPSHOT: FIRST ASCII FRAME RENDERED (write_ascii) - Timer started");
859 }
860
861 // Tick FPS counter
862 if (ctx->fps_counter) {
864 }
865
867 }
868
869 // Flush buffers
870 log_debug("FRAME_FLUSH: Flushing stdout");
871 (void)fflush(stdout);
872 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
873 (void)terminal_flush(flush_fd);
874
875 // Render FPS counter overlay if enabled
876 if (ctx->fps_counter && GET_OPTION(fps_counter)) {
878 }
879 } else if (terminal_is_interactive()) {
880 // Piped to interactive terminal: output ASCII frames with newline
881 char *write_buf = SAFE_MALLOC(frame_len + 1, char *);
882 if (write_buf) {
883 memcpy(write_buf, display_frame, frame_len);
884 write_buf[frame_len] = '\n';
885 (void)platform_write_all(STDOUT_FILENO, write_buf, frame_len + 1);
886 SAFE_FREE(write_buf);
887 }
888
889 // Flush buffers
890 (void)fflush(stdout);
891 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
892 (void)terminal_flush(flush_fd);
893 } else {
894 // Non-interactive piped output
895 // BUT: Don't write ASCII frames to stdout if render_file is using stdout (--render-file="-")
896 if (!ctx->render_file) {
897 // No render-file, safe to write ASCII frames to stdout
898 char *write_buf = SAFE_MALLOC(frame_len + 1, char *);
899 if (write_buf) {
900 memcpy(write_buf, display_frame, frame_len);
901 write_buf[frame_len] = '\n';
902 (void)platform_write_all(STDOUT_FILENO, write_buf, frame_len + 1);
903
904 // Start snapshot timer on first ASCII frame rendered
905 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
908 log_info("SNAPSHOT: FIRST ASCII FRAME RENDERED (write_ascii piped) - Timer started");
909 }
910
911 SAFE_FREE(write_buf);
912 }
913
914 // Flush buffers (skip terminal_flush on pipes to avoid blocking in snapshot mode)
915 (void)fflush(stdout);
916 if (!GET_OPTION(snapshot_mode) || ctx->has_tty) {
917 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
918 (void)terminal_flush(flush_fd);
919 }
920 } else {
921 // render_file is using stdout: skip ASCII output to avoid mixing with video
922 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
925 log_info("SNAPSHOT: FIRST FRAME RENDERED (render_file piped) - Timer started");
926 }
927 }
928 }
929
930 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "frame_write",
931 "FRAME_WRITE: Write and flush complete (%.2f ms)");
932
933 // Clean up digital rain result if allocated
934 if (rain_result) {
935 SAFE_FREE(rain_result);
936 }
937 SAFE_FREE(visualization_frame);
938}
939
944
946 static int call_count = 0;
947 if (call_count++ < 5) {
948 log_info("session_display_encode_frame: CALLED (ctx=%p, image=%p, captured_ns=%llu, ctx->render_file=%p)",
949 (void *)ctx, (void *)image, (unsigned long long)captured_ns, ctx ? (void *)ctx->render_file : NULL);
950 }
951
952 if (!ctx || !ctx->initialized) {
953 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL or uninitialized");
954 return;
955 }
956
957 if (!image) {
958 SET_ERRNO(ERROR_INVALID_PARAM, "Image is NULL");
959 return;
960 }
961
962 // Only encode if render_file is active
963 if (!ctx->render_file) {
964 log_warn_every(10 * NS_PER_SEC_INT, "session_display_encode_frame: render_file is NULL, skipping encode");
965 return;
966 }
967
968 // Convert image to ASCII
969 char *ascii = session_display_convert_to_ascii(ctx, image);
970 if (!ascii) {
971 log_warn("session_display_encode_frame: Failed to convert image to ASCII");
972 return;
973 }
974
975 // Write the same selected visualization used by the terminal to the render-file encoder.
976 char *visualization_frame = NULL;
977 const char *render_frame = ascii;
978 if (GET_OPTION(waveform) || GET_OPTION(fft)) {
979 visualization_frame = session_display_create_visualization_frame(ctx);
980 if (visualization_frame)
981 render_frame = visualization_frame;
982 }
983
984 asciichat_error_t fe = render_file_write_frame(ctx->render_file, render_frame, captured_ns);
985 SAFE_FREE(visualization_frame);
986 if (fe != ASCIICHAT_OK) {
987 log_warn_every(5 * NS_PER_SEC_INT, "render-file: encode failed (%s)", asciichat_error_string(fe));
988 }
989
990 SAFE_FREE(ascii);
991}
992
993void session_display_write_raw(session_display_ctx_t *ctx, const char *data, size_t len) {
994 if (!ctx || !ctx->initialized || !data || len == 0) {
995 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, data=%p, len=%zu", ctx, data, len);
996 return;
997 }
998
999 int fd = -1;
1000 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
1001 fd = ctx->tty_info.fd;
1002 } else {
1003 fd = STDOUT_FILENO;
1004 }
1005
1006 // Write all data with automatic retry on transient errors
1007 (void)platform_write_all(fd, data, len);
1008
1009 // Flush immediately after write to TTY to ensure data is sent
1010 (void)terminal_flush(fd);
1011}
1012
1014 if (!ctx || !ctx->fps_counter || !ctx->initialized) {
1015 return;
1016 }
1017
1018 // Only render overlay in TTY mode
1019 if (!ctx->has_tty) {
1020 return;
1021 }
1022
1023 // Get current FPS value from counter
1024 float fps = fps_counter_get(ctx->fps_counter);
1025
1026 // Get terminal width for right-alignment
1027 int term_cols = (int)terminal_get_effective_width();
1028
1029 // Position at row 1, right side of screen
1030 // Column = term_cols - 7 (to fit "FPS:144" which is 7 chars)
1031 // Use reverse video (\033[7m) for visibility, then reset (\033[0m)
1032 char overlay[64];
1033 int overlay_len = snprintf(overlay, sizeof(overlay), "\033[1;%dH\033[7mFPS:%3.0f\033[0m", term_cols - 6, fps);
1034
1035 if (overlay_len > 0 && overlay_len < (int)sizeof(overlay)) {
1036 // Write overlay to TTY
1037 int fd = (ctx->has_tty && ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
1038 (void)platform_write_all(fd, overlay, overlay_len);
1039
1040 // Flush to ensure overlay appears immediately
1041 (void)terminal_flush(fd);
1042 }
1043}
1044
1046 if (!ctx || !ctx->initialized) {
1047 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1048 return;
1049 }
1050
1051 // Skip terminal reset in snapshot mode to preserve rendered output
1052 if (ctx->snapshot_mode) {
1053 return;
1054 }
1055
1056 // Only perform terminal operations if we have a valid TTY
1057 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
1058 (void)terminal_reset(ctx->tty_info.fd);
1059 (void)terminal_cursor_show();
1060 (void)terminal_flush(ctx->tty_info.fd);
1061 }
1062}
1063
1065 if (!ctx || !ctx->initialized) {
1066 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1067 return;
1068 }
1069
1070 // Skip terminal clear in snapshot mode to preserve rendered output
1071 if (ctx->snapshot_mode) {
1072 return;
1073 }
1074
1075 // Only perform terminal operations when we have a valid TTY (not when piping)
1076 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
1077 (void)terminal_clear_screen();
1078 (void)terminal_cursor_home(ctx->tty_info.fd);
1079 }
1080}
1081
1083 if (!ctx || !ctx->initialized) {
1084 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1085 return;
1086 }
1087
1088 int fd = ctx->has_tty ? ctx->tty_info.fd : STDOUT_FILENO;
1089 if (fd >= 0) {
1090 (void)terminal_cursor_home(fd);
1091 }
1092}
1093
1095 if (!ctx || !ctx->initialized) {
1096 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1097 return false;
1098 }
1099 return ctx->audio_playback_enabled;
1100}
1101
1102asciichat_error_t session_display_write_audio(session_display_ctx_t *ctx, const float *buffer, size_t num_samples) {
1103 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
1104 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%zu", ctx, buffer,
1105 num_samples);
1106 }
1107
1108 if (!ctx->audio_playback_enabled || !ctx->audio_ctx) {
1109 // Audio not enabled, but not an error - just skip silently
1110 return ASCIICHAT_OK;
1111 }
1112
1113 // For mirror mode with local files: write samples directly without jitter buffering
1114 // The jitter buffer is designed for network scenarios with irregular packet arrivals
1115 // For local playback, we just want raw samples flowing to the speakers
1116 audio_context_t *audio_ctx = (audio_context_t *)ctx->audio_ctx;
1117 if (audio_ctx && audio_ctx->playback_buffer) {
1118 audio_ring_buffer_t *rb = audio_ctx->playback_buffer;
1119
1120 // Simple direct write: append samples to ring buffer without network-oriented complexity
1121 uint32_t write_idx = (uint32_t)atomic_load_u64(&rb->write_index);
1122 uint32_t read_idx = (uint32_t)atomic_load_u64(&rb->read_index);
1123
1124 // Calculate available space in ring buffer
1125 uint32_t available = (read_idx - write_idx - 1) & (AUDIO_RING_BUFFER_SIZE - 1);
1126
1127 if ((uint32_t)num_samples > available) {
1128 // Buffer full - just skip this write to avoid distortion from overwriting old audio
1129 return ASCIICHAT_OK;
1130 }
1131
1132 // Direct memcpy to ring buffer, handling wrap-around
1133 uint32_t space_before_wrap = AUDIO_RING_BUFFER_SIZE - write_idx;
1134 if ((uint32_t)num_samples <= space_before_wrap) {
1135 memcpy(&rb->data[write_idx], buffer, num_samples * sizeof(float));
1136 } else {
1137 // Split write: first part to end of buffer, second part wraps to beginning
1138 uint32_t first_part = space_before_wrap;
1139 uint32_t second_part = num_samples - first_part;
1140 memcpy(&rb->data[write_idx], buffer, first_part * sizeof(float));
1141 memcpy(&rb->data[0], &buffer[first_part], second_part * sizeof(float));
1142 }
1143
1144 // Update write index atomically
1145 atomic_store_u64(&rb->write_index, (write_idx + num_samples) % AUDIO_RING_BUFFER_SIZE);
1146
1147 return ASCIICHAT_OK;
1148 }
1149
1150 return ASCIICHAT_OK;
1151}
1152
1153/* ============================================================================
1154 * Keyboard Help Functions (from keyboard_help module)
1155 * ============================================================================ */
1156
1161 if (!ctx) {
1162 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1163 return;
1164 }
1165
1166 bool current = atomic_load_bool(&ctx->keyboard_help_active);
1167 atomic_store_bool(&ctx->keyboard_help_active, !current);
1168}
1169
1173// Global reference to current display context (used by signal handlers)
1174// Set during render loop, cleared on exit
1175static atomic_ptr_t g_current_display_ctx = {0};
1176
1177// Signal-safe flag to request help screen closure from Ctrl+C handler
1178static atomic_t g_help_signal_cancel = {0};
1179
1180// Signal handler safe way to access current display context
1181static session_display_ctx_t *get_current_display_ctx(void) {
1182 return (session_display_ctx_t *)atomic_ptr_load(&g_current_display_ctx);
1183}
1184
1185// Called by render loop to register/unregister the display context
1187 atomic_ptr_store(&g_current_display_ctx, (void *)ctx);
1188}
1189
1190// Public getter for global display context
1192 return get_current_display_ctx();
1193}
1194
1195// Public setter for global display context (used by discovery participant cleanup)
1199
1200// Called from Ctrl+C signal handler to request help screen closure
1202 atomic_store_bool(&g_help_signal_cancel, true);
1203}
1204
1205// Called from render loop to check if help should be closed
1207 bool expected = true;
1208 return atomic_cas_bool(&g_help_signal_cancel, &expected, false);
1209}
1210
1212 if (!ctx) {
1213 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1214 return false;
1215 }
1216
1217 // Disable help in snapshot mode only - allow help state check even if stdin/stdout aren't TTYs
1218 if (GET_OPTION(snapshot_mode)) {
1219 return false;
1220 }
1221
1223}
1224
1225// Global accessor for signal handlers (called from Ctrl+C handler)
1226// Checks raw help state without snapshot mode check (signal handler needs to know
1227// if help is active even in snapshot mode)
1229 session_display_ctx_t *ctx = get_current_display_ctx();
1230 if (!ctx) {
1231 platform_write_all(STDERR_FILENO, "[DEBUG: ctx is NULL]\n", 20);
1232 return false;
1233 }
1234 bool active = atomic_load_bool(&ctx->keyboard_help_active);
1235 const char *msg = active ? "[DEBUG: help active]\n" : "[DEBUG: help inactive]\n";
1236 platform_write_all(STDERR_FILENO, msg, active ? 22 : 24);
1237 return active;
1238}
1239
1241 session_display_ctx_t *ctx = get_current_display_ctx();
1242 if (ctx) {
1244 }
1245}
1246
1248 log_info("session_display_set_snapshot_actual_duration: CALLED with duration=%.3f, ctx=%p, render_file=%p",
1249 actual_duration_sec, (void *)ctx, ctx ? (void *)ctx->render_file : NULL);
1250
1251 if (!ctx) {
1252 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1253 return;
1254 }
1255
1256 if (!ctx->render_file) {
1257 log_warn(" -> render_file is NULL, cannot set duration");
1258 return;
1259 }
1260
1261 log_info(" -> Passing duration %.3f to render_file encoder", actual_duration_sec);
1262 render_file_set_snapshot_actual_duration(ctx->render_file, actual_duration_sec);
1263}
Platform abstraction layer umbrella header providing unified cross-platform API.
ANSI escape sequence utilities and fast color code generation.
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
void atomic_ptr_store(atomic_ptr_t *a, void *value)
Atomically store a pointer.
Definition atomic.c:280
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void * atomic_ptr_load(atomic_ptr_t *a)
Atomically load a pointer.
Definition atomic.c:272
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
bool atomic_exchange_bool(atomic_t *a, bool new_value)
Atomically exchange a boolean and return the old value.
Definition atomic.c:303
bool atomic_cas_bool(atomic_t *a, bool *expected, bool new_value)
Atomically compare-and-swap a boolean.
Definition atomic.c:184
⚛️ Atomic operations abstraction layer with debug tracking
uint64_t g_snapshot_first_frame_rendered_ns
Centralized render loop abstraction for all display modes.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Matrix-style digital rain effect for ASCII frames.
⏱️ FPS counter for measuring output write throughput
void ansi_fast_init_256color(void)
Definition ansi.c:334
void ansi_fast_init(void)
Definition ansi.c:319
void ansi_fast_init_16color(void)
Definition ansi.c:398
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
void log_set_force_stderr(bool enabled)
Force all terminal log output to stderr.
Definition log/log.c:721
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define STOP_TIMER_AND_LOG_EVERY(log_level, interval_ns, threshold_ns, timer_name, msg_fmt,...)
Stop a timer and log the result with rate limiting.
Definition time.h:647
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define NS_PER_SEC_INT
Definition time.h:157
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:333
#define NS_PER_MS_INT
Definition time.h:156
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
audio_source_t
Audio source selected for visualization.
@ COLOR_SETTING_FALSE
Force colors OFF: disable all colors and logging colors.
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256], size_t *client_palette_len, char client_luminance_palette[256])
Initialize client palette with full configuration.
Definition palette.c:299
palette_type_t
Built-in palette type enumeration.
Definition palette.h:84
@ PALETTE_STANDARD
Standard ASCII palette: " ...',;:clodxkO0KXNWM".
Definition palette.h:88
asciichat_error_t terminal_reset(int fd)
Reset terminal to default state.
Definition misc.c:81
asciichat_error_t terminal_clear_scrollback(int fd)
Clear terminal scrollback buffer.
Definition misc.c:76
asciichat_error_t terminal_cursor_home(int fd)
Move cursor to home position (top-left)
asciichat_error_t terminal_cursor_hide(void)
Hide terminal cursor.
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
Definition system.c:224
asciichat_error_t terminal_clear_screen(void)
Clear the terminal screen.
int platform_close(int fd)
Safe file close (close replacement)
asciichat_error_t terminal_flush(int fd)
Flush terminal output.
asciichat_error_t terminal_cursor_show(void)
Show terminal cursor.
#define AUDIO_RING_BUFFER_SIZE
Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz)
Definition ringbuffer.h:128
void session_display_destroy(session_display_ctx_t *ctx)
Destroy session display context and free resources.
void session_display_set_render_fps(session_display_ctx_t *ctx, uint32_t fps)
Set the render FPS for file output encoding.
void session_display_encode_frame(session_display_ctx_t *ctx, const image_t *image, uint64_t captured_ns)
Encode frame to render-file (FFmpeg only, no terminal output)
void session_display_write_ascii(session_display_ctx_t *ctx, const char *ascii)
Write ASCII frame to terminal only (no encoding)
session_display_ctx_t * session_display_create(const session_display_config_t *config)
Create a new session display context.
void keyboard_help_toggle(session_display_ctx_t *ctx)
Toggle keyboard help on/off (implemented in display.c for struct access)
void session_display_clear(session_display_ctx_t *ctx)
Clear the terminal screen.
void * session_display_get_stdin_reader(session_display_ctx_t *ctx)
Get the stdin frame reader for ASCII-to-video rendering.
size_t session_display_get_palette_len(session_display_ctx_t *ctx)
Get the palette character count.
void session_display_set_stdin_reader(session_display_ctx_t *ctx, void *reader)
Set stdin frame reader for ASCII-to-video rendering.
bool keyboard_help_is_active_global(void)
Check if keyboard help is active (global accessor for signal handlers)
bool keyboard_help_is_active(session_display_ctx_t *ctx)
Check if keyboard help is currently active.
int splash_intro_done(void)
Signal that intro splash should end (first frame ready to render)
Definition splash.c:693
void session_display_set_snapshot_actual_duration(session_display_ctx_t *ctx, double actual_duration_sec)
Set the actual wall-clock duration for snapshot mode frame timing.
void session_display_write_raw(session_display_ctx_t *ctx, const char *data, size_t len)
Render raw bytes to the terminal without frame processing.
float fps_counter_get(fps_counter_t *counter)
Get the current FPS value.
Definition fps_counter.c:74
void fps_counter_tick(fps_counter_t *counter)
Record a frame timestamp.
Definition fps_counter.c:57
asciichat_error_t session_display_write_audio(session_display_ctx_t *ctx, const float *buffer, size_t num_samples)
Write audio samples to playback buffer.
session_display_ctx_t * session_display_get_global_context(void)
Get the global display context (used for signal handlers and special cleanup)
bool session_display_has_tty(session_display_ctx_t *ctx)
Check if display has a TTY (terminal) available.
void session_display_render_fps_overlay(session_display_ctx_t *ctx)
Render FPS counter overlay in top-right corner.
void session_display_cursor_home(session_display_ctx_t *ctx)
Move cursor to home position (top-left)
void keyboard_help_toggle_global(void)
Toggle keyboard help (global accessor for signal handlers)
const char * session_display_get_luminance_palette(session_display_ctx_t *ctx)
Get the luminance mapping palette.
void keyboard_help_signal_cancel(void)
Request help screen closure from signal handler (Ctrl+C)
void session_display_set_global_context_public(session_display_ctx_t *ctx)
Set the global display context (public setter for discovery cleanup)
void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data)
Render an ASCII frame to the terminal.
void session_display_reset_first_frame(session_display_ctx_t *ctx)
Reset the first frame flag to allow splash screen to show again on next render.
int session_display_get_tty_fd(session_display_ctx_t *ctx)
Get the TTY file descriptor.
void splash_clear_display_context(void)
Clear the cached display context to prevent use-after-free.
Definition splash.c:721
bool session_display_has_first_frame(session_display_ctx_t *ctx)
Check if the display has rendered its first frame.
void session_display_set_global_context(session_display_ctx_t *ctx)
Set the global display context (for signal handlers like Ctrl+C)
bool session_display_has_render_file(session_display_ctx_t *ctx)
Check if display has render-file configured.
void fps_counter_destroy(fps_counter_t *counter)
Destroy FPS counter and free resources.
Definition fps_counter.c:47
bool keyboard_help_check_signal_cancel(void)
Check if help screen closure was requested by signal handler.
const char * session_display_get_palette_chars(session_display_ctx_t *ctx)
Get the palette characters string.
const terminal_capabilities_t * session_display_get_caps(session_display_ctx_t *ctx)
Get detected terminal capabilities.
bool session_display_has_audio_playback(session_display_ctx_t *ctx)
Check if display has audio playback configured.
char * session_display_convert_to_ascii(session_display_ctx_t *ctx, const image_t *image)
Convert an image to ASCII art using display context and command-line options.
void session_display_reset(session_display_ctx_t *ctx)
Reset terminal to default state.
uint32_t session_display_get_render_fps(session_display_ctx_t *ctx)
Get the render FPS configured for file output.
fps_counter_t * fps_counter_create(void)
Create a new FPS counter.
Definition fps_counter.c:33
char * ascii_convert_with_capabilities(image_t *original, const ssize_t width, const ssize_t height, const terminal_capabilities_t *caps, const bool use_aspect_ratio, const bool stretch, const char *palette_chars)
Convert image to ASCII art with terminal capability awareness.
Definition ascii.c:233
char * rainbow_replace_ansi_colors(const char *ansi_string, float time_seconds)
Replace all RGB ANSI codes with rainbow color.
utf8_palette_cache_t * get_utf8_palette_cache(const char *ascii_chars)
Get UTF-8 palette cache for a character set.
asciichat_error_t ascii_write_init(int fd, bool reset_terminal)
Initialize ASCII write subsystem.
Definition ascii.c:83
int apply_color_filter(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t stride, color_filter_t filter, float time)
Apply color filter to entire image in-place.
void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter)
Set digital rain color from color filter.
void ascii_write_destroy(int fd, bool reset_terminal)
Destroy ASCII write subsystem.
Definition ascii.c:442
void digital_rain_destroy(digital_rain_t *rain)
Destroy digital rain effect context.
char * digital_rain_apply(digital_rain_t *rain, const char *frame, float delta_time)
Apply digital rain effect to ASCII frame.
digital_rain_t * digital_rain_init(int num_columns, int num_rows)
Initialize digital rain effect context.
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
🔊 Audio Capture and Playback Interface for ascii-chat
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
📝 Logging API with multiple log levels and terminal output control
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
terminal_capabilities_t apply_color_mode_override(terminal_capabilities_t caps)
Apply command-line overrides to detected capabilities.
Definition misc.c:29
tty_info_t get_current_tty(void)
Get current TTY information.
Definition misc.c:24
NEON-optimized ASCII rendering functions.
ASCII Palette Management for Video-to-ASCII Conversion.
bool g_snapshot_first_frame_rendered
unsigned short int terminal_get_effective_width(void)
Get effective terminal width with fallback priority.
bool terminal_is_interactive(void)
Check if the session is fully interactive.
bool terminal_is_stdin_tty(void)
Check if stdin is connected to a TTY.
bool terminal_is_stdout_tty(void)
Check if stdout is connected to a TTY.
bool terminal_should_force_stderr(void)
Determine if logs should be forced to stderr.
unsigned short int terminal_get_effective_height(void)
Get effective terminal height with fallback priority.
terminal_capabilities_t detect_terminal_capabilities(void)
Detect terminal capabilities.
bool terminal_has_dark_background(void)
Detect if terminal theme is dark.
Read ANSI ASCII frames from file descriptor, chunked by height.
void render_file_set_snapshot_actual_duration(render_file_ctx_t *ctx, double actual_duration_sec)
Definition renderer.c:241
asciichat_error_t render_file_write_frame(render_file_ctx_t *ctx, const char *ansi_frame, uint64_t captured_ns)
Definition renderer.c:146
void render_file_set_live_timing(render_file_ctx_t *ctx)
Definition renderer.c:139
asciichat_error_t render_file_create(const char *output_path, int cols, int rows, int fps, int theme, render_file_ctx_t **out)
Definition renderer.c:33
asciichat_error_t render_file_destroy(render_file_ctx_t *ctx)
Definition renderer.c:247
Terminal-to-pixel renderer and render-file output (audio + video)
Intro and status screen display for ascii-chat with animated rainbow effects.
struct session_display_ctx session_display_ctx_t
Internal session display context structure.
asciichat_error_t session_display_init_render_file(session_display_ctx_t *ctx, uint32_t fps)
Perform complete terminal reset.
void session_display_set_render_live_timing(session_display_ctx_t *ctx)
🖥️ Unified terminal display abstraction for session-based rendering
Atomic pointer wrapper.
Definition atomic.h:106
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Audio context for full-duplex capture and playback.
audio_ring_buffer_t * playback_buffer
OLD: Network → Worker (will become network_playback_rb)
Audio ring buffer for real-time audio streaming.
Definition ringbuffer.h:205
atomic_t read_index
Read index (consumer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:211
atomic_t write_index
Write index (producer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:209
float data[192000]
Audio sample data buffer.
Definition ringbuffer.h:207
Digital rain effect context.
FPS counter state.
Definition fps_counter.c:23
Frame buffer structure.
Image structure.
int w
Image width in pixels (must be > 0)
int h
Image height in pixels (must be > 0)
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
RGB pixel structure.
Configuration for session display context.
void * callback_data
Opaque data passed to should_exit_callback.
palette_type_t palette_type
Palette type for ASCII rendering.
void * audio_ctx
Audio context for playback (borrowed, not owned)
bool defer_render_file
Defer render-file encoder creation until media timing is known.
const char * custom_palette
Custom palette characters (required if palette_type == PALETTE_CUSTOM)
bool enable_audio_playback
Enable audio playback (mirror mode)
session_display_should_exit_fn should_exit_callback
Optional: callback to check if initialization should be cancelled (e.g., shutdown signal)
uint32_t render_fps
Video FPS for render-file encoding (0 = use default/option)
bool snapshot_mode
Enable snapshot mode (single frame capture)
terminal_color_mode_t color_mode
Color mode override (TERM_COLOR_AUTO for auto-detection)
Internal session display context structure.
char palette_chars[256]
Palette character string for rendering.
fps_counter_t * fps_counter
FPS counter for measuring output throughput.
bool has_tty
True if we have a valid TTY for interactive output.
atomic_t keyboard_help_active
Keyboard help active flag (toggled with '?') - atomic for thread-safe access.
tty_info_t tty_info
TTY information (file descriptor, path, ownership)
char luminance_palette[256]
Luminance-to-character mapping table (256 entries)
digital_rain_t * digital_rain
Digital rain effect context (NULL if disabled)
uint64_t last_frame_time_ns
Last frame timestamp for digital rain delta time calculation.
bool initialized
Context is fully initialized.
atomic_t first_frame
First frame flag for logging control.
render_file_ctx_t * render_file
Render-to-file context (NULL if disabled)
void * audio_ctx
Audio context for playback (borrowed, not owned)
terminal_capabilities_t caps
Detected terminal capabilities.
bool audio_playback_enabled
Audio playback is enabled.
palette_type_t palette_type
Configured palette type.
bool snapshot_mode
Snapshot mode enabled.
uint32_t render_fps
Video FPS for render-file encoding.
size_t palette_len
Number of characters in palette.
terminal_fd_reader_t * stdin_reader
Stdin frame reader for ASCII-to-video rendering (borrowed ref, not owned)
Complete terminal capabilities structure.
Definition terminal.h:709
terminal_color_mode_t color_level
Detected color support level (terminal_color_mode_t)
Definition terminal.h:711
render_mode_t render_mode
Preferred rendering mode (render_mode_t)
Definition terminal.h:721
bool wants_padding
Whether client wants frame padding (centering) - false for snapshot/piped modes.
Definition terminal.h:737
size_t pad_height
Number of top padding lines when wants_padding is true.
Definition terminal.h:739
TTY detection and management structure.
Definition terminal.h:753
bool owns_fd
True if we opened the FD and should close it, false otherwise.
Definition terminal.h:759
int fd
File descriptor for TTY access.
Definition terminal.h:755
🖥️ Cross-platform terminal interface for ascii-chat
color_filter_t
Monochromatic color filter enumeration.
Definition terminal.h:599
@ COLOR_FILTER_NONE
No filtering (default)
Definition terminal.h:601
@ COLOR_FILTER_RAINBOW
Rainbow (cycles through spectrum over 3.5s)
Definition terminal.h:625
render_mode_t
Render mode preferences.
Definition terminal.h:662
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:578
@ TERM_COLOR_NONE
No color support (monochrome terminal)
Definition terminal.h:582
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:584
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:586
@ TERM_COLOR_AUTO
Auto-detect color support from terminal capabilities.
Definition terminal.h:580
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:588
⏱️ High-precision timing utilities using sokol_time.h and uthash
int platform_isatty(int fd)
Check if a file descriptor is a terminal.
Definition util.c:63
SIMD-optimized ASCII conversion interface.
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)
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_MIC
@ AUDIO_VISUALIZATION_SOURCE_REMOTE
#define AUDIO_VISUALIZATION_COLOR_BRIGHTER
#define AUDIO_VISUALIZATION_COLOR_DARKER