diff --git a/C/include/ascii.h b/C/include/ascii.h index 3f192a4..1f88e5d 100644 --- a/C/include/ascii.h +++ b/C/include/ascii.h @@ -7,13 +7,13 @@ #define ASCII_CHARS_DEFAULT " .:-=+*#%@" typedef struct { - int brightness; - int contrast; - int invert; - int color; - int edges; - int dither; - const char *charset; + int brightness; /* additive offset: -128..128 */ + int contrast; /* percent, 100 = no change */ + int invert; /* flip brightness->charset mapping */ + int color; /* ANSI truecolor output */ + int edges; /* Sobel edge detection */ + int dither; /* Floyd-Steinberg dithering */ + const char *charset; /* NULL = ASCII_CHARS_DEFAULT */ } ascii_opts_t; // Convert YUYV raw data to grayscale @@ -28,4 +28,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, int src_h, int dst_w, int dst_h, char *out, size_t out_size, const ascii_opts_t *opts); +// Overlay FPS box +void overlay_fps_box(int dst_w, double fps, int color_enabled); + #endif diff --git a/C/include/thread_sharing.h b/C/include/thread_sharing.h index 7a800af..9c223e0 100644 --- a/C/include/thread_sharing.h +++ b/C/include/thread_sharing.h @@ -1,17 +1,22 @@ +#ifndef THREAD_SHARING_H + #include #include #include "ascii.h" typedef struct { - uint8_t *buf[2]; // Duble buffer - int width, height; - int ascii_w, ascii_h; - int ready_idx; - int has_frame; + uint8_t *buf[2]; // Double buffer + int width, height; + int ascii_w, ascii_h; + int ready_idx; + int has_frame; pthread_mutex_t lock; - pthread_cond_t cond; - volatile int stop; - - ascii_opts_t opts; + pthread_cond_t cond; + volatile int stop; + ascii_opts_t opts; } shared_frame_t; +void *capture_thread(void *arg); +void *render_thread (void *arg); + +#endif diff --git a/C/include/timing.h b/C/include/timing.h index 5c57539..0d5d20b 100644 --- a/C/include/timing.h +++ b/C/include/timing.h @@ -3,9 +3,19 @@ #include +typedef struct { + long samples[16]; // frame duration (ns) ring buffer + int head; + int count; +} fps_counter_t; + // Initialize framerate control void timing_init(int fps); void timing_sleep(struct timespec *start_time); +void fps_push(fps_counter_t *fc, long elapsed_ns); + +double fps_get(const fps_counter_t *fc); + #endif diff --git a/C/src/ascii.c b/C/src/ascii.c index 27cafb0..162f9c3 100644 --- a/C/src/ascii.c +++ b/C/src/ascii.c @@ -5,6 +5,7 @@ #include #include #include +#include // Helpers static inline uint8_t clamp_u8(int v) { @@ -53,8 +54,16 @@ size_t ascii_out_size(int dst_w, int dst_h, int color) { // Sobel edge detection (kernel convolution) static void sobel(const uint8_t *in, uint8_t *out, int w, int h) { - static const int Gx[3][3] = {{-1, 0, 1}, {-2, 0, -2}, {-1, 0, 1}}; - static const int Gy[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}}; + static const int Gx[3][3] = { + {-1, 0, 1}, + {-2, 0, 2}, + {-1, 0, 1} + }; + static const int Gy[3][3] = { + {-1, -2, -1}, + {0, 0, 0}, + {1, 2, 1} + }; for (int y = 1; y < h - 1; y++) { for (int x = 1; x < w - 1; x++) { @@ -90,7 +99,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, int do_edges = opts ? opts->edges : 0; int do_dither = opts ? opts->dither : 0; - // Blocking Widht and height pixels in source pixels + // Blocking width and height pixels in source pixels double bw = (double)src_w / dst_w; double bh = (double)src_h / dst_h; @@ -117,7 +126,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, xe = xs + 1; long tg = 0, tr = 0, tgv = 0, tb = 0; - int cnt = 0; + int count = 0; for (int sy = ys; sy < ye && sy < src_h; sy++) { for (int sx = xs; sx < xe && sx < src_w; sx++) { @@ -128,14 +137,14 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, tgv += px[1]; tb += px[2]; } - cnt++; + count++; } } - if (cnt == 0) - cnt = 1; + if (count == 0) + count = 1; // Brightness and contrast - int gv = (int)(tg / cnt); + int gv = (int)(tg / count); if (contrast != 100) gv = 128 + (gv - 128) * contrast / 100; gv += brightness; @@ -143,16 +152,16 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, if (do_color) { uint8_t *out_px = small_rgb + (y * dst_w + x) * 3; - out_px[0] = clamp_u8((int)(tr / cnt)); - out_px[1] = clamp_u8((int)(tgv / cnt)); - out_px[2] = clamp_u8((int)(tb / cnt)); + out_px[0] = clamp_u8((int)(tr / count)); + out_px[1] = clamp_u8((int)(tgv / count)); + out_px[2] = clamp_u8((int)(tb / count)); } } } // Sobel edge detection if (do_edges) { - // Temporary buffer for detected edges results + // Temporary buffer for detected edges results uint8_t *edge_buf = calloc(dst_w * dst_h, sizeof(uint8_t)); if (edge_buf) { sobel(small_g, edge_buf, dst_w, dst_h); @@ -208,9 +217,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, // Render into caller's buffer int out_idx = 0; - - // Cursor repositions without erasing - static const char HOME[] = "\033[H"; + static const char HOME[] = "\033[H"; // Cursor repositions without erasing if (out_size > sizeof(HOME)) { memcpy(out, HOME, sizeof(HOME) - 1); out_idx = sizeof(HOME) - 1; @@ -243,7 +250,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, if (do_color) { int w = snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n"); - if (w > 0) + if (w > 0 && (size_t)(out_idx + w) < out_size) out_idx += w; } else { if ((size_t)(out_idx + 1) < out_size) @@ -251,12 +258,29 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, } } - if ((size_t)out_idx < out_size) - out[out_idx] = '\0'; - else - out[out_size - 1] = '\0'; + out[(size_t)out_idx < out_size ? out_idx : out_size - 1] = '\0'; free(small_g); free(small_rgb); return out_idx; } + + +// FPS overlay +void overlay_fps_box(int dst_w, double fps, int color_enabled) { + char buf[80]; + int col = (dst_w - 13) / 2 + 1; + if (col < 1) col = 1; + + int n; + if (color_enabled) { + n = snprintf(buf, sizeof(buf), + "\033[1;%dH\033[38;2;0;255;0m\033[48;2;30;30;30m" + "[ FPS: %4.1f ]\033[0m", + col, fps); + } else { + n = snprintf(buf, sizeof(buf), "\033[1;%dH[ FPS: %4.1f ]", col, fps); + } + if (n > 0 && n < (int)sizeof(buf)) + write(STDOUT_FILENO, buf, (size_t)n); +} diff --git a/C/src/main.c b/C/src/main.c index 16bbc7e..ab1ee67 100644 --- a/C/src/main.c +++ b/C/src/main.c @@ -15,11 +15,11 @@ #include // Defaults -#define DEFAULT_ASCII_WIDTH 80 -#define DEFAULT_ASCII_HEIGHT 40 -#define DEFAULT_CAPTURE_WIDTH 160 +#define DEFAULT_ASCII_WIDTH 80 +#define DEFAULT_ASCII_HEIGHT 40 +#define DEFAULT_CAPTURE_WIDTH 160 #define DEFAULT_CAPTURE_HEIGHT 120 -#define DEFAULT_FPS 20 +#define DEFAULT_FPS 20 // Signal handling volatile sig_atomic_t keep_running = 1; @@ -55,20 +55,22 @@ static void print_usage(const char *prog) { " -C colour output (ANSI truecolor)\n" " -D Floyd-Steinberg dithering\n", prog, DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT, DEFAULT_FPS, - DEFAULT_ASCII_WIDTH, DEFAULT_ASCII_HEIGHT, ASCII_CHARS_DEFAULT); + DEFAULT_ASCII_WIDTH, DEFAULT_ASCII_HEIGHT, ASCII_CHARS_DEFAULT); // FIX: undeclared identifier ASCII_CHARS_DEFAULT } // termios void term_raw_mode(void) { - tcgetattr(STDOUT_FILENO, &orig_terminal); + tcgetattr(STDOUT_FILENO, &orig_terminal); // save stdin state struct termios raw = orig_terminal; - raw.c_lflag &= ~(ICANON | ECHO); // no line buffering or no echo - raw.c_cc[VMIN] = 0; // non-blocking read + raw.c_lflag &= ~(ICANON | ECHO); // no line buffering or no echo + raw.c_cc[VMIN] = 0; // non-blocking read raw.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); } -void term_restore(void) { tcsetattr(STDOUT_FILENO, TCSAFLUSH, &orig_terminal); } +void term_restore(void) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_terminal); } + +fps_counter_t fps_calc = {0}; // Global FPS counter // FIX: // Main int main(int argc, char *argv[]) { @@ -77,13 +79,13 @@ int main(int argc, char *argv[]) { // Config char *device = "/dev/video0"; - int ascii_w = DEFAULT_ASCII_WIDTH; - int ascii_h = DEFAULT_ASCII_HEIGHT; - int cap_w = DEFAULT_CAPTURE_WIDTH; - int cap_h = DEFAULT_CAPTURE_HEIGHT; - int fps = DEFAULT_FPS; + int ascii_w = DEFAULT_ASCII_WIDTH; + int ascii_h = DEFAULT_ASCII_HEIGHT; + int cap_w = DEFAULT_CAPTURE_WIDTH; + int cap_h = DEFAULT_CAPTURE_HEIGHT; + int fps = DEFAULT_FPS; - ascii_opts_t opts = { + ascii_opts_t opts = { // FIX: undcleared identifier 'ascii_opts_t' .brightness = 0, .contrast = 100, .invert = 0, @@ -162,10 +164,12 @@ int main(int argc, char *argv[]) { perror("webcam_init"); return 1; } - fprintf(stderr, "Device: %s | capture %dx%d | ASCII %dx%d | %d fps%s%s%s\n", + fprintf(stderr, "Device: %s | capture %dx%d | ASCII %dx%d | %d fps%s%s%s%s\n", device, cam.width, cam.height, ascii_w, ascii_h, fps, - opts.color ? " | color" : "", opts.edges ? " | edges" : "", - opts.dither ? " | dither" : "", opts.invert ? " | inverted" : ""); + opts.color ? " | color" : "", + opts.edges ? " | edges" : "", + opts.dither ? " | dither" : "", + opts.invert ? " | inverted": ""); // Allocate pixel buffers int cam_pixels = cam.width * cam.height; @@ -191,16 +195,27 @@ int main(int argc, char *argv[]) { } // Initial full clear - write(STDOUT_FILENO, "\033[2J\033[H\033[?25l", 11); + write(STDOUT_FILENO, "\033[2J\033[H\033[?25l", 13); term_raw_mode(); // Main loop struct timespec frame_start; + clock_gettime(CLOCK_MONOTONIC, &frame_start); + struct timespec last_frame_time = frame_start; char input_char; while (keep_running) { clock_gettime(CLOCK_MONOTONIC, &frame_start); + long frame_diff_ns = + (frame_start.tv_sec - last_frame_time.tv_sec) * 1000000000L + // Seconds + (frame_start.tv_nsec - last_frame_time.tv_nsec); // Nano seconds + + if (frame_diff_ns > 0) + fps_push(&fps_calc, frame_diff_ns); + last_frame_time = frame_start; + double current_fps = fps_get(&fps_calc); + // Check for 'q' key non-blocking if (read(STDIN_FILENO, &input_char, 1) == 1) { if (input_char == 'q' || input_char == 'Q') { @@ -222,9 +237,10 @@ int main(int argc, char *argv[]) { int len = grayscale_to_ascii(gray, rgb, cam.width, cam.height, ascii_w, ascii_h, out_buf, out_size, &opts); + if (len > 0) { - write(STDOUT_FILENO, "\033[H", 3); write(STDOUT_FILENO, out_buf, (size_t)len); + overlay_fps_box(ascii_w, current_fps, opts.color); } if (webcam_requeue_buffer(&cam) < 0) { @@ -237,7 +253,7 @@ int main(int argc, char *argv[]) { // Cleanup term_restore(); - write(STDOUT_FILENO, "\033[0m\033[?25h\n", 10); + write(STDOUT_FILENO, "\033[0m\033[?25h\n", 11); fprintf(stderr, "Stopped.\n"); free(gray); diff --git a/C/src/timing.c b/C/src/timing.c index d28c094..6861940 100644 --- a/C/src/timing.c +++ b/C/src/timing.c @@ -2,21 +2,39 @@ #include #include -static long frame_duration_ns = 0; // nanoseconds per frame +static long frame_duration_ns = 0; // nanoseconds per frame -void timing_init(int fps) { - frame_duration_ns = 1000000000L / fps; -} +void timing_init(int fps) { frame_duration_ns = 1000000000L / fps; } void timing_sleep(struct timespec *start_time) { - struct timespec end_time; - clock_gettime(CLOCK_MONOTONIC, &end_time); + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); - long elapsed_ns = (end_time.tv_sec - start_time->tv_sec) * 1000000000L + - (end_time.tv_nsec - start_time->tv_nsec); - long sleep_ns = frame_duration_ns - elapsed_ns; - if (sleep_ns > 0) { - struct timespec ts = { sleep_ns / 1000000000L, sleep_ns % 1000000000L }; - while (nanosleep(&ts, &ts) == -1 && errno == EINTR); - } + long elapsed_ns = (end_time.tv_sec - start_time->tv_sec) * 1000000000L + + (end_time.tv_nsec - start_time->tv_nsec); + long sleep_ns = frame_duration_ns - elapsed_ns; + if (sleep_ns > 0) { + struct timespec ts = {sleep_ns / 1000000000L, sleep_ns % 1000000000L}; + while (nanosleep(&ts, &ts) == -1 && errno == EINTR) + ; + } +} + +void fps_push(fps_counter_t *fc, long ns) { + fc->samples[fc->head % 16] = ns; + fc->head++; + if (fc->count < 16) + fc->count++; +} + +double fps_get(const fps_counter_t *fc) { + if (fc->count == 0) + return 0; + + long sum = 0; + for (int i = 0; i < fc->count; i++) { + sum += fc->samples[i]; + } + + return 1e9 * fc->count / sum; }