mirror of
https://github.com/Harshit-Dhanwalkar/AsciiCam.git
synced 2026-06-21 10:58:05 +02:00
Add hot-reloadable charsets, expand output buffer sizing and macOS build support
This commit is contained in:
parent
b179ccb32c
commit
9963f01810
10 changed files with 897 additions and 163 deletions
12
C/Makefile
12
C/Makefile
|
|
@ -26,23 +26,21 @@ FILTERDIR := filters
|
|||
BUILDDIR := build
|
||||
OBJDIR := $(BUILDDIR)/obj
|
||||
|
||||
CFLAGS_COMMON := -Wall -Wextra -O2 -Iinclude -I$(LIBDIR) -fno-stack-protector -fno-builtin-memcpy -fno-builtin-memset -fno-builtin-strlen
|
||||
CFLAGS_COMMON +=
|
||||
CFLAGS_COMMON := -Wall -Wextra -O2 -Iinclude -I$(LIBDIR) -fno-stack-protector -fno-builtin -ffreestanding
|
||||
|
||||
ifeq ($(PLATFORM),linux)
|
||||
# Linux: nolibc, V4L2, SSE4.1
|
||||
CFLAGS := $(CFLAGS_COMMON) -DPLATFORM_LINUX -D__LINUX_NOLIBC__
|
||||
CFLAGS += -msse4.1
|
||||
|
||||
LDFLAGS := -nostdlib
|
||||
LDFLAGS := -nostdlib -nostdinc
|
||||
LDFLAGS += -Wl,--no-as-needed
|
||||
LDFLAGS += -Wl,-dynamic-linker,/lib64/ld-linux-x86-64.so.2
|
||||
LDFLAGS += $(shell $(CC) --print-file-name=crti.o)
|
||||
LDFLAGS += $(shell $(CC) --print-file-name=crtn.o)
|
||||
LDFLAGS += -L/usr/lib/x86_64-linux-gnu # TEST: Explicit library paths
|
||||
# LDFLAGS += -Wl,-rpath-link=/usr/lib/x86_64-linux-gnu
|
||||
LDFLAGS += -ldl -lpthread -lc
|
||||
# LDFLAGS += -ldl -lpthread
|
||||
LDFLAGS += -ldl -lpthread -lc # FIX: lc flag pulls standard libc,but removing them will -lpthread and -ldl will fail to link because they themselves depend on libc
|
||||
# TODO: custom threading library using clone() + futex to eliminate -lpthread (pthread functions) dependency
|
||||
# TODO: implement an ELF loader (or statically link plugins) to eliminate -ldl (dlopen/dlsym/dlclose are part of libdl.so also glibc) dependency
|
||||
LDFLAGS += -msse4.1
|
||||
|
||||
LIBSRCS := $(LIBDIR)/nl_alloc.c \
|
||||
|
|
|
|||
|
|
@ -5,24 +5,84 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#define ASCII_CHARS_DEFAULT " .:-=+*#%@"
|
||||
#define MAX_CHARSETS 16
|
||||
#define CHARSET_NAME_LEN 32
|
||||
#define CHARSET_RAMP_LEN 128
|
||||
|
||||
// Render mode
|
||||
typedef enum {
|
||||
RENDER_BRAILLE = 0,
|
||||
RENDER_BLOCKS,
|
||||
RENDER_ASCII_RAMP,
|
||||
RENDER_HALF_BLOCK,
|
||||
RENDER_DOTS,
|
||||
RENDER_MODE_COUNT
|
||||
} render_mode_t;
|
||||
|
||||
// Edge detection mode
|
||||
typedef enum {
|
||||
EDGE_OFF = 0,
|
||||
EDGE_SOBEL,
|
||||
EDGE_SOBEL_DIR,
|
||||
EDGE_LAPLACIAN,
|
||||
EDGE_MODE_COUNT
|
||||
} edge_mode_t;
|
||||
|
||||
typedef struct {
|
||||
char name[CHARSET_NAME_LEN];
|
||||
char ramp[CHARSET_RAMP_LEN];
|
||||
} charset_entry_t;
|
||||
|
||||
// Registry of loaded charsets, hot-reloadable from a directory of .txt files.
|
||||
typedef struct {
|
||||
charset_entry_t sets[MAX_CHARSETS];
|
||||
int count;
|
||||
int active;
|
||||
int inotify_fd;
|
||||
int inotify_wd;
|
||||
char dir_path[256];
|
||||
} charset_registry_t;
|
||||
|
||||
int charset_registry_init(charset_registry_t *reg, const char *dir);
|
||||
void charset_registry_scan(
|
||||
charset_registry_t *reg); /* (re)loads all files in dir */
|
||||
void charset_registry_check_reload(
|
||||
charset_registry_t *reg); /* inotify poll, call once per frame */
|
||||
void charset_registry_cleanup(charset_registry_t *reg);
|
||||
const char *charset_registry_active_ramp(const charset_registry_t *reg);
|
||||
|
||||
typedef struct {
|
||||
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 */
|
||||
edge_mode_t edges; /* edge detection mode */
|
||||
int dither; /* Floyd-Steinberg dithering */
|
||||
const char *charset; /* NULL = ASCII_CHARS_DEFAULT */
|
||||
int threshold_val; /* Binarization limit */
|
||||
const char *charset; /* NULL = ASCII_CHARS_DEFAULT; active ramp for
|
||||
RENDER_ASCII_RAMP */
|
||||
render_mode_t render_mode;
|
||||
|
||||
// TESTING:
|
||||
/* Pseudo-3D "pop out" parallax effect.
|
||||
* depth_pop: 0 = off, >0 = effect strength (1..100 suggested range)
|
||||
* depth_invert: 0 = brighter is "closer" (pops toward viewer), 1 = darker is
|
||||
* closer
|
||||
*/
|
||||
int depth_pop;
|
||||
int depth_invert;
|
||||
} ascii_opts_t;
|
||||
|
||||
// Convert YUYV raw data to grayscale
|
||||
// void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height);
|
||||
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width, int height);
|
||||
void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height);
|
||||
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width,
|
||||
int height);
|
||||
void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height);
|
||||
|
||||
// Output buffer sizing
|
||||
size_t ascii_out_size(int dst_w, int dst_h, int color);
|
||||
size_t ascii_out_size_for_mode(int dst_w, int dst_h, int color,
|
||||
render_mode_t mode);
|
||||
|
||||
// grayscale to ASCII output grid
|
||||
int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
|
||||
|
|
@ -32,4 +92,8 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
|
|||
// Overlay FPS box
|
||||
void overlay_fps_box(int dst_w, double fps, int color_enabled);
|
||||
|
||||
// Render mode name for UI display
|
||||
const char *render_mode_name(render_mode_t m);
|
||||
const char *edge_mode_name(edge_mode_t m);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ static inline int nl_ioctl(int fd, unsigned long req, void *arg) {
|
|||
#define stderr 2
|
||||
#define fprintf(fd, fmt, ...) \
|
||||
do { \
|
||||
char _fb[1024]; \
|
||||
char _fb[4096]; \
|
||||
int _fn = nl_snprintf(_fb, sizeof(_fb), fmt, ##__VA_ARGS__); \
|
||||
if (_fn > 0) { \
|
||||
size_t _nw = \
|
||||
|
|
|
|||
669
C/src/ascii.c
669
C/src/ascii.c
|
|
@ -3,18 +3,36 @@
|
|||
// - x86_64 Linux/macOS: SSE2 via <immintrin.h>
|
||||
// - ARM64 macOS: NEON via <arm_neon.h>
|
||||
|
||||
#include "ascii.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#include "nolibc.h"
|
||||
|
||||
#include "ascii.h"
|
||||
#include "platform.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sys/inotify.h>
|
||||
|
||||
// Helpers
|
||||
static inline uint8_t clamp_u8(int v) {
|
||||
return (v < 0) ? 0 : (v > 255) ? 255 : (uint8_t)v;
|
||||
}
|
||||
static inline int my_abs(int x) { return x < 0 ? -x : x; }
|
||||
|
||||
static inline double my_sqrt(double x) {
|
||||
if (x <= 0.0)
|
||||
return 0.0;
|
||||
double guess = x;
|
||||
// initial halve the exponent by repeated division
|
||||
while (guess > 1.0) {
|
||||
guess /= 2.0;
|
||||
}
|
||||
if (guess <= 0.0)
|
||||
guess = 1.0;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
guess = 0.5 * (guess + x / guess);
|
||||
}
|
||||
return guess;
|
||||
}
|
||||
|
||||
// YUYV to grayscale
|
||||
#if defined(ARCH_X86_64)
|
||||
#include <immintrin.h>
|
||||
|
|
@ -77,11 +95,40 @@ void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height) {
|
|||
}
|
||||
}
|
||||
|
||||
// Buffer sizing
|
||||
// Buffer sizing for multi-byte UTF-8 Braille (3 bytes per block) + color codes
|
||||
size_t ascii_out_size(int dst_w, int dst_h, int color) {
|
||||
if (color)
|
||||
return 3 + (size_t)dst_h * ((size_t)dst_w * 21 + 6) + 1;
|
||||
return 3 + (size_t)dst_h * ((size_t)dst_w + 1) + 1;
|
||||
return ascii_out_size_for_mode(dst_w, dst_h, color, RENDER_BRAILLE);
|
||||
}
|
||||
|
||||
size_t ascii_out_size_for_mode(int dst_w, int dst_h, int color,
|
||||
render_mode_t mode) {
|
||||
int braille_term_w = dst_w / 2;
|
||||
int braille_term_h = dst_h / 4;
|
||||
|
||||
switch (mode) {
|
||||
case RENDER_HALF_BLOCK: {
|
||||
int tw = dst_w;
|
||||
int th = dst_h / 2;
|
||||
size_t per_cell = color ? 41 : 3;
|
||||
return 3 + (size_t)th * ((size_t)tw * per_cell + 5) + 1;
|
||||
}
|
||||
case RENDER_BLOCKS:
|
||||
case RENDER_ASCII_RAMP:
|
||||
case RENDER_DOTS: {
|
||||
int tw = braille_term_w;
|
||||
int th = braille_term_h;
|
||||
size_t per_cell = color ? 22 : 3; // worst case: 3-byte glyph
|
||||
return 3 + (size_t)th * ((size_t)tw * per_cell + 5) + 1;
|
||||
}
|
||||
case RENDER_BRAILLE:
|
||||
default: {
|
||||
int tw = braille_term_w;
|
||||
int th = braille_term_h;
|
||||
if (color)
|
||||
return 3 + (size_t)th * ((size_t)tw * 22 + 5) + 1;
|
||||
return 3 + (size_t)th * ((size_t)tw * 3 + 1) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sobel edge detection
|
||||
|
|
@ -103,44 +150,293 @@ static void sobel(const uint8_t *in, uint8_t *out, int w, int h) {
|
|||
}
|
||||
}
|
||||
|
||||
static void sobel_dir(const uint8_t *in, uint8_t *out_mag, uint8_t *out_dir,
|
||||
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}};
|
||||
for (int y = 1; y < h - 1; y++) {
|
||||
for (int x = 1; x < w - 1; x++) {
|
||||
int gx = 0, gy = 0;
|
||||
for (int ky = -1; ky <= 1; ky++)
|
||||
for (int kx = -1; kx <= 1; kx++) {
|
||||
int p = in[(y + ky) * w + (x + kx)];
|
||||
gx += Gx[ky + 1][kx + 1] * p;
|
||||
gy += Gy[ky + 1][kx + 1] * p;
|
||||
}
|
||||
int mag = my_abs(gx) + my_abs(gy);
|
||||
out_mag[y * w + x] = (uint8_t)(mag > 255 ? 255 : mag);
|
||||
|
||||
uint8_t dir;
|
||||
int agx = my_abs(gx), agy = my_abs(gy);
|
||||
if (agx == 0 && agy == 0) {
|
||||
dir = 0;
|
||||
} else if (agy * 5 < agx * 2) {
|
||||
dir = 2;
|
||||
} else if (agx * 5 < agy * 2) {
|
||||
dir = 0;
|
||||
} else {
|
||||
dir = ((gx > 0) == (gy > 0)) ? 3 : 1;
|
||||
}
|
||||
out_dir[y * w + x] = dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void laplacian(const uint8_t *in, uint8_t *out, int w, int h) {
|
||||
for (int y = 1; y < h - 1; y++) {
|
||||
for (int x = 1; x < w - 1; x++) {
|
||||
int center = in[y * w + x];
|
||||
int lap = 4 * center - in[(y - 1) * w + x] - in[(y + 1) * w + x] -
|
||||
in[y * w + (x - 1)] - in[y * w + (x + 1)];
|
||||
out[y * w + x] = (uint8_t)(my_abs(lap) > 255 ? 255 : my_abs(lap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint8_t get_braille_bitmask(int dx, int dy) {
|
||||
static const uint8_t braille_dots[2][4] = {
|
||||
{0x01, 0x02, 0x04, 0x40}, // Left layout column: dots 1, 2, 3, 7
|
||||
{0x08, 0x10, 0x20, 0x80} // Right layout column: dots 4, 5, 6, 8
|
||||
};
|
||||
return braille_dots[dx][dy];
|
||||
}
|
||||
|
||||
// Charset registry: hot-reloadable character ramps from a directory.
|
||||
static void _trim_line(char *s) {
|
||||
// Cut at first newline/CR, then trim trailing whitespace.
|
||||
for (char *p = s; *p; p++) {
|
||||
if (*p == '\n' || *p == '\r') {
|
||||
*p = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t len = nl_strlen(s);
|
||||
while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t')) {
|
||||
s[--len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void _name_from_path(const char *path, char *out, size_t out_sz) {
|
||||
char tmp[256];
|
||||
nl_strncpy_safe(tmp, path, sizeof(tmp));
|
||||
const char *base = basename(tmp);
|
||||
nl_strncpy_safe(out, base, out_sz);
|
||||
// strip extension
|
||||
for (size_t i = 0; out[i]; i++) {
|
||||
if (out[i] == '.') {
|
||||
out[i] = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int charset_registry_init(charset_registry_t *reg, const char *dir) {
|
||||
nl_memset(reg, 0, sizeof(*reg));
|
||||
reg->inotify_fd = -1;
|
||||
reg->inotify_wd = -1;
|
||||
nl_strncpy_safe(reg->dir_path, dir, sizeof(reg->dir_path));
|
||||
|
||||
nl_strncpy_safe(reg->sets[0].name, "default", CHARSET_NAME_LEN);
|
||||
nl_strncpy_safe(reg->sets[0].ramp, ASCII_CHARS_DEFAULT, CHARSET_RAMP_LEN);
|
||||
reg->count = 1;
|
||||
reg->active = 0;
|
||||
|
||||
charset_registry_scan(reg);
|
||||
|
||||
reg->inotify_fd = inotify_init1(IN_NONBLOCK);
|
||||
if (reg->inotify_fd >= 0) {
|
||||
reg->inotify_wd =
|
||||
inotify_add_watch(reg->inotify_fd, dir,
|
||||
IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE | IN_DELETE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TEST:
|
||||
void charset_registry_scan(charset_registry_t *reg) {
|
||||
static const char *known[] = {"blocks", "ascii", "dots", "shades",
|
||||
"minimal", "emoji", "lines", NULL};
|
||||
for (int i = 0; known[i] && reg->count < MAX_CHARSETS; i++) {
|
||||
char path[256];
|
||||
nl_snprintf(path, sizeof(path), "%s/%s.txt", reg->dir_path, known[i]);
|
||||
|
||||
int fd = open(path, O_RDONLY, 0);
|
||||
if (fd < 0)
|
||||
continue;
|
||||
|
||||
char buf[CHARSET_RAMP_LEN];
|
||||
ssize_t n = read(fd, buf, sizeof(buf) - 1);
|
||||
close(fd);
|
||||
if (n <= 0)
|
||||
continue;
|
||||
buf[n] = '\0';
|
||||
_trim_line(buf);
|
||||
if (buf[0] == '\0')
|
||||
continue;
|
||||
|
||||
// Skip if charset with name is already loaded
|
||||
int dup = 0;
|
||||
for (int j = 0; j < reg->count; j++) {
|
||||
char nm[CHARSET_NAME_LEN];
|
||||
_name_from_path(path, nm, sizeof(nm));
|
||||
if (nl_strcmp(reg->sets[j].name, nm) == 0) {
|
||||
nl_strncpy_safe(reg->sets[j].ramp, buf, CHARSET_RAMP_LEN);
|
||||
dup = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dup)
|
||||
continue;
|
||||
|
||||
_name_from_path(path, reg->sets[reg->count].name, CHARSET_NAME_LEN);
|
||||
nl_strncpy_safe(reg->sets[reg->count].ramp, buf, CHARSET_RAMP_LEN);
|
||||
reg->count++;
|
||||
}
|
||||
}
|
||||
|
||||
void charset_registry_check_reload(charset_registry_t *reg) {
|
||||
if (reg->inotify_fd < 0)
|
||||
return;
|
||||
|
||||
char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
|
||||
ssize_t n = read(reg->inotify_fd, buf, sizeof(buf));
|
||||
if (n <= 0)
|
||||
return;
|
||||
|
||||
charset_registry_scan(reg);
|
||||
}
|
||||
|
||||
const char *charset_registry_active_ramp(const charset_registry_t *reg) {
|
||||
if (reg->count == 0)
|
||||
return ASCII_CHARS_DEFAULT;
|
||||
int idx = reg->active;
|
||||
if (idx < 0 || idx >= reg->count)
|
||||
idx = 0;
|
||||
return reg->sets[idx].ramp;
|
||||
}
|
||||
|
||||
void charset_registry_cleanup(charset_registry_t *reg) {
|
||||
if (reg->inotify_fd >= 0) {
|
||||
close(reg->inotify_fd);
|
||||
reg->inotify_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Grayscale to ASCII
|
||||
// Block-shade ramp
|
||||
static const char *BLOCK_SHADE_UTF8[] = {" ", "\xe2\x96\x91", "\xe2\x96\x92",
|
||||
"\xe2\x96\x93", "\xe2\x96\x88"};
|
||||
#define BLOCK_SHADE_COUNT 5
|
||||
|
||||
// Dot ramp
|
||||
static const char *DOT_UTF8[] = {" ", "\xc2\xb7", "\xe2\x80\xa2",
|
||||
"\xe2\x97\x8f"};
|
||||
#define DOT_COUNT 4
|
||||
|
||||
// Half block glyph
|
||||
static const char HALF_BLOCK_UTF8[] = "\xe2\x96\x80";
|
||||
|
||||
const char *render_mode_name(render_mode_t m) {
|
||||
switch (m) {
|
||||
case RENDER_BRAILLE:
|
||||
return "braille";
|
||||
case RENDER_BLOCKS:
|
||||
return "blocks";
|
||||
case RENDER_ASCII_RAMP:
|
||||
return "ascii";
|
||||
case RENDER_HALF_BLOCK:
|
||||
return "halfblock";
|
||||
case RENDER_DOTS:
|
||||
return "dots";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
const char *edge_mode_name(edge_mode_t m) {
|
||||
switch (m) {
|
||||
case EDGE_OFF:
|
||||
return "off";
|
||||
case EDGE_SOBEL:
|
||||
return "sobel";
|
||||
case EDGE_SOBEL_DIR:
|
||||
return "sobel-dir";
|
||||
case EDGE_LAPLACIAN:
|
||||
return "laplacian";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static int emit_glyph(char *out, size_t out_size, int out_idx,
|
||||
const char *glyph, int do_color, int r, int g, int b) {
|
||||
if (do_color) {
|
||||
int written = snprintf(out + out_idx, out_size - (size_t)out_idx,
|
||||
"\033[38;2;%d;%d;%dm%s", r, g, b, glyph);
|
||||
if (written > 0 && (size_t)(out_idx + written) < out_size)
|
||||
out_idx += written;
|
||||
} else {
|
||||
size_t glen = nl_strlen(glyph);
|
||||
if ((size_t)(out_idx + (int)glen) < out_size) {
|
||||
nl_memcpy(out + out_idx, glyph, glen);
|
||||
out_idx += (int)glen;
|
||||
}
|
||||
}
|
||||
return out_idx;
|
||||
}
|
||||
|
||||
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) {
|
||||
const char *charset =
|
||||
(opts && opts->charset) ? opts->charset : ASCII_CHARS_DEFAULT;
|
||||
int nchars = (int)strlen(charset);
|
||||
int safe_dst_w = dst_w - (dst_w % 2);
|
||||
int safe_dst_h = dst_h - (dst_h % 4);
|
||||
|
||||
int brightness = opts ? opts->brightness : 0;
|
||||
int contrast = opts ? opts->contrast : 100;
|
||||
int invert = opts ? opts->invert : 0;
|
||||
int do_color = opts && opts->color && rgb;
|
||||
int do_edges = opts ? opts->edges : 0;
|
||||
edge_mode_t edge_mode = opts ? opts->edges : EDGE_OFF;
|
||||
int do_dither = opts ? opts->dither : 0;
|
||||
int thresh_limit = opts ? opts->threshold_val : 35;
|
||||
render_mode_t render_mode = opts ? opts->render_mode : RENDER_BRAILLE;
|
||||
const char *ramp = (opts && opts->charset && opts->charset[0])
|
||||
? opts->charset
|
||||
: ASCII_CHARS_DEFAULT;
|
||||
int ramp_len = (int)nl_strlen(ramp);
|
||||
if (ramp_len < 1) {
|
||||
ramp = ASCII_CHARS_DEFAULT;
|
||||
ramp_len = (int)nl_strlen(ramp);
|
||||
}
|
||||
|
||||
double bw = (double)src_w / dst_w;
|
||||
double bh = (double)src_h / dst_h;
|
||||
int depth_pop = opts ? opts->depth_pop : 0;
|
||||
int depth_invert = opts ? opts->depth_invert : 0;
|
||||
|
||||
uint8_t *small_g = malloc((size_t)(dst_w * dst_h));
|
||||
uint8_t *small_rgb = do_color ? malloc((size_t)(dst_w * dst_h * 3)) : NULL;
|
||||
double bw = (double)src_w / safe_dst_w;
|
||||
double bh = (double)src_h / safe_dst_h;
|
||||
|
||||
if (!small_g || (do_color && !small_rgb)) {
|
||||
free(small_g);
|
||||
free(small_rgb);
|
||||
uint8_t *subpixel_g = malloc((size_t)(safe_dst_w * safe_dst_h));
|
||||
uint8_t *subpixel_rgb =
|
||||
do_color ? malloc((size_t)(safe_dst_w * safe_dst_h * 3)) : NULL;
|
||||
|
||||
if (!subpixel_g || (do_color && !subpixel_rgb)) {
|
||||
free(subpixel_g);
|
||||
free(subpixel_rgb);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int y = 0; y < dst_h; y++) {
|
||||
// Downscale and interpolate the high-res frames to subpixel boundaries
|
||||
for (int y = 0; y < safe_dst_h; y++) {
|
||||
int ys = (int)(y * bh), ye = (int)((y + 1) * bh);
|
||||
if (ye <= ys)
|
||||
ye = ys + 1;
|
||||
for (int x = 0; x < dst_w; x++) {
|
||||
for (int x = 0; x < safe_dst_w; x++) {
|
||||
int xs = (int)(x * bw), xe = (int)((x + 1) * bw);
|
||||
if (xe <= xs)
|
||||
xe = xs + 1;
|
||||
|
||||
long tg = 0, tr = 0, tgv = 0, tb = 0;
|
||||
int count = 0;
|
||||
for (int sy = ys; sy < ye && sy < src_h; sy++)
|
||||
for (int sy = ys; sy < ye && sy < src_h; sy++) {
|
||||
for (int sx = xs; sx < xe && sx < src_w; sx++) {
|
||||
tg += gray[sy * src_w + sx];
|
||||
if (do_color) {
|
||||
|
|
@ -151,6 +447,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
|
|||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (!count)
|
||||
count = 1;
|
||||
|
||||
|
|
@ -158,10 +455,10 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
|
|||
if (contrast != 100)
|
||||
gv = 128 + (gv - 128) * contrast / 100;
|
||||
gv += brightness;
|
||||
small_g[y * dst_w + x] = clamp_u8(gv);
|
||||
subpixel_g[y * safe_dst_w + x] = clamp_u8(gv);
|
||||
|
||||
if (do_color) {
|
||||
uint8_t *op = small_rgb + (y * dst_w + x) * 3;
|
||||
uint8_t *op = subpixel_rgb + (y * safe_dst_w + x) * 3;
|
||||
op[0] = clamp_u8((int)(tr / count));
|
||||
op[1] = clamp_u8((int)(tgv / count));
|
||||
op[2] = clamp_u8((int)(tb / count));
|
||||
|
|
@ -169,47 +466,120 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
|
|||
}
|
||||
}
|
||||
|
||||
if (do_edges) {
|
||||
uint8_t *eb = calloc((size_t)(dst_w * dst_h), 1);
|
||||
// TEST:
|
||||
// Pseudo-3D "pop out" parallax effect.
|
||||
if (depth_pop > 0) {
|
||||
uint8_t *warped_g = malloc((size_t)(safe_dst_w * safe_dst_h));
|
||||
uint8_t *warped_rgb =
|
||||
do_color ? malloc((size_t)(safe_dst_w * safe_dst_h * 3)) : NULL;
|
||||
if (warped_g && (!do_color || warped_rgb)) {
|
||||
double cx = safe_dst_w / 2.0;
|
||||
double cy = safe_dst_h / 2.0;
|
||||
double max_r = (cx > cy ? cx : cy);
|
||||
double strength = (depth_pop / 100.0) * (max_r * 0.35);
|
||||
|
||||
for (int y = 0; y < safe_dst_h; y++) {
|
||||
for (int x = 0; x < safe_dst_w; x++) {
|
||||
uint8_t luma = subpixel_g[y * safe_dst_w + x];
|
||||
double depth01 = depth_invert ? (255 - luma) / 255.0 : luma / 255.0;
|
||||
|
||||
double dx = x - cx, dy = y - cy;
|
||||
double r = (dx * dx + dy * dy);
|
||||
double rlen = r > 0.0001 ? my_sqrt(r) : 0.0001;
|
||||
double nx = dx / rlen, ny = dy / rlen;
|
||||
|
||||
double rnorm = rlen / (max_r > 0.0001 ? max_r : 0.0001);
|
||||
double disp = strength * depth01 * rnorm;
|
||||
|
||||
int sx = (int)(x + nx * disp);
|
||||
int sy = (int)(y + ny * disp);
|
||||
if (sx < 0)
|
||||
sx = 0;
|
||||
if (sx >= safe_dst_w)
|
||||
sx = safe_dst_w - 1;
|
||||
if (sy < 0)
|
||||
sy = 0;
|
||||
if (sy >= safe_dst_h)
|
||||
sy = safe_dst_h - 1;
|
||||
|
||||
warped_g[y * safe_dst_w + x] = subpixel_g[sy * safe_dst_w + sx];
|
||||
if (do_color) {
|
||||
const uint8_t *src_px = subpixel_rgb + (sy * safe_dst_w + sx) * 3;
|
||||
uint8_t *dst_px = warped_rgb + (y * safe_dst_w + x) * 3;
|
||||
dst_px[0] = src_px[0];
|
||||
dst_px[1] = src_px[1];
|
||||
dst_px[2] = src_px[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
nl_memcpy(subpixel_g, warped_g, (size_t)(safe_dst_w * safe_dst_h));
|
||||
if (do_color)
|
||||
nl_memcpy(subpixel_rgb, warped_rgb,
|
||||
(size_t)(safe_dst_w * safe_dst_h * 3));
|
||||
}
|
||||
free(warped_g);
|
||||
free(warped_rgb);
|
||||
}
|
||||
|
||||
// Edge detection dispatch
|
||||
uint8_t *dir_buf = NULL;
|
||||
if (edge_mode != EDGE_OFF) {
|
||||
uint8_t *eb = calloc((size_t)(safe_dst_w * safe_dst_h), 1);
|
||||
if (eb) {
|
||||
sobel(small_g, eb, dst_w, dst_h);
|
||||
nl_memcpy(small_g, eb, (size_t)(dst_w * dst_h));
|
||||
switch (edge_mode) {
|
||||
case EDGE_SOBEL:
|
||||
sobel(subpixel_g, eb, safe_dst_w, safe_dst_h);
|
||||
break;
|
||||
case EDGE_SOBEL_DIR:
|
||||
dir_buf = calloc((size_t)(safe_dst_w * safe_dst_h), 1);
|
||||
if (dir_buf)
|
||||
sobel_dir(subpixel_g, eb, dir_buf, safe_dst_w, safe_dst_h);
|
||||
else
|
||||
sobel(subpixel_g, eb, safe_dst_w, safe_dst_h);
|
||||
break;
|
||||
case EDGE_LAPLACIAN:
|
||||
laplacian(subpixel_g, eb, safe_dst_w, safe_dst_h);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
nl_memcpy(subpixel_g, eb, (size_t)(safe_dst_w * safe_dst_h));
|
||||
free(eb);
|
||||
}
|
||||
}
|
||||
|
||||
// Floyd-Steinberg dithering on the subpixel grayscale buffer
|
||||
if (do_dither) {
|
||||
int16_t *eb = malloc((size_t)(dst_w * dst_h) * sizeof(int16_t));
|
||||
if (eb) {
|
||||
for (int i = 0; i < dst_w * dst_h; i++)
|
||||
eb[i] = (int16_t)small_g[i];
|
||||
for (int y = 0; y < dst_h; y++) {
|
||||
for (int x = 0; x < dst_w; x++) {
|
||||
int old_v = eb[y * dst_w + x];
|
||||
int qi = old_v * (nchars - 1) / 255;
|
||||
if (qi < 0)
|
||||
qi = 0;
|
||||
if (qi >= nchars)
|
||||
qi = nchars - 1;
|
||||
int new_v = qi * 255 / (nchars - 1);
|
||||
eb[y * dst_w + x] = (int16_t)new_v;
|
||||
int err = old_v - new_v;
|
||||
#define FS(DY, DX, N) \
|
||||
do { \
|
||||
int ny = y + (DY), nx = x + (DX); \
|
||||
if (nx >= 0 && nx < dst_w && ny >= 0 && ny < dst_h) \
|
||||
eb[ny * dst_w + nx] += (int16_t)(err * (N) / 16); \
|
||||
} while (0)
|
||||
FS(0, 1, 7);
|
||||
FS(1, -1, 3);
|
||||
FS(1, 0, 5);
|
||||
FS(1, 1, 1);
|
||||
#undef FS
|
||||
int16_t *err = calloc((size_t)(safe_dst_w * safe_dst_h), sizeof(int16_t));
|
||||
if (err) {
|
||||
for (int i = 0; i < safe_dst_w * safe_dst_h; i++)
|
||||
err[i] = (int16_t)subpixel_g[i];
|
||||
|
||||
for (int y = 0; y < safe_dst_h; y++) {
|
||||
for (int x = 0; x < safe_dst_w; x++) {
|
||||
int idx = y * safe_dst_w + x;
|
||||
int16_t old = err[idx];
|
||||
int16_t nval = (old > (int16_t)thresh_limit) ? 255 : 0;
|
||||
int16_t qerr = old - nval;
|
||||
err[idx] = nval;
|
||||
|
||||
if (x + 1 < safe_dst_w)
|
||||
err[idx + 1] += (qerr * 7) >> 4;
|
||||
if (y + 1 < safe_dst_h) {
|
||||
if (x > 0)
|
||||
err[idx + safe_dst_w - 1] += (qerr * 3) >> 4;
|
||||
err[idx + safe_dst_w] += (qerr * 5) >> 4;
|
||||
if (x + 1 < safe_dst_w)
|
||||
err[idx + safe_dst_w + 1] += (qerr * 1) >> 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < dst_w * dst_h; i++)
|
||||
small_g[i] = clamp_u8(eb[i]);
|
||||
free(eb);
|
||||
|
||||
for (int i = 0; i < safe_dst_w * safe_dst_h; i++) {
|
||||
int16_t v = err[i];
|
||||
subpixel_g[i] = (v < 0) ? 0 : (v > 255) ? 255 : (uint8_t)v;
|
||||
}
|
||||
free(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,33 +590,169 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
|
|||
out_idx = sizeof(HOME) - 1;
|
||||
}
|
||||
|
||||
for (int y = 0; y < dst_h; y++) {
|
||||
for (int x = 0; x < dst_w; x++) {
|
||||
int gv = small_g[y * dst_w + x];
|
||||
int idx = gv * (nchars - 1) / 255;
|
||||
if (idx < 0)
|
||||
idx = 0;
|
||||
if (idx >= nchars)
|
||||
idx = nchars - 1;
|
||||
if (invert)
|
||||
idx = nchars - 1 - idx;
|
||||
char ch = charset[idx];
|
||||
if (render_mode == RENDER_HALF_BLOCK) {
|
||||
int term_w = safe_dst_w;
|
||||
int term_h = safe_dst_h / 2;
|
||||
for (int ty = 0; ty < term_h; ty++) {
|
||||
for (int tx = 0; tx < term_w; tx++) {
|
||||
int top_idx = (ty * 2) * safe_dst_w + tx;
|
||||
int bot_idx = (ty * 2 + 1) * safe_dst_w + tx;
|
||||
uint8_t top_l = subpixel_g[top_idx];
|
||||
uint8_t bot_l = subpixel_g[bot_idx];
|
||||
|
||||
if (do_color) {
|
||||
const uint8_t *tp = subpixel_rgb + top_idx * 3;
|
||||
const uint8_t *bp = subpixel_rgb + bot_idx * 3;
|
||||
int written =
|
||||
snprintf(out + out_idx, out_size - (size_t)out_idx,
|
||||
"\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm%s", tp[0], tp[1],
|
||||
tp[2], bp[0], bp[1], bp[2], HALF_BLOCK_UTF8);
|
||||
if (written > 0 && (size_t)(out_idx + written) < out_size)
|
||||
out_idx += written;
|
||||
} else {
|
||||
int active = (top_l > thresh_limit) || (bot_l > thresh_limit);
|
||||
if (invert)
|
||||
active = !active;
|
||||
const char *glyph = active ? "\xe2\x96\x88" : " ";
|
||||
out_idx = emit_glyph(out, out_size, out_idx, glyph, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
if (do_color) {
|
||||
const uint8_t *px = small_rgb + (y * dst_w + x) * 3;
|
||||
int w = snprintf(out + out_idx, out_size - (size_t)out_idx,
|
||||
"\033[38;2;%d;%d;%dm%c", px[0], px[1], px[2], ch);
|
||||
if (w > 0 && (size_t)(out_idx + w) < out_size)
|
||||
out_idx += w;
|
||||
int written =
|
||||
snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n");
|
||||
if (written > 0 && (size_t)(out_idx + written) < out_size)
|
||||
out_idx += written;
|
||||
} else {
|
||||
if ((size_t)(out_idx + 1) < out_size)
|
||||
out[out_idx++] = ch;
|
||||
out[out_idx++] = '\n';
|
||||
}
|
||||
}
|
||||
out[(size_t)out_idx < out_size ? (size_t)out_idx : out_size - 1] = '\0';
|
||||
free(subpixel_g);
|
||||
free(subpixel_rgb);
|
||||
free(dir_buf);
|
||||
return out_idx;
|
||||
}
|
||||
|
||||
// BRAILLE / BLOCKS / ASCII_RAMP / DOTS
|
||||
int term_w = safe_dst_w / 2;
|
||||
int term_h = safe_dst_h / 4;
|
||||
|
||||
for (int ty = 0; ty < term_h; ty++) {
|
||||
for (int tx = 0; tx < term_w; tx++) {
|
||||
uint8_t braille_offset = 0;
|
||||
long sum_r = 0, sum_g = 0, sum_b = 0;
|
||||
long sum_luma = 0;
|
||||
int colored_subpixels = 0;
|
||||
int active_count = 0;
|
||||
uint8_t dom_dir = 0;
|
||||
int dom_dir_votes[4] = {0, 0, 0, 0};
|
||||
|
||||
for (int dy = 0; dy < 4; dy++) {
|
||||
for (int dx = 0; dx < 2; dx++) {
|
||||
int sub_x = tx * 2 + dx;
|
||||
int sub_y = ty * 4 + dy;
|
||||
int pixel_idx = sub_y * safe_dst_w + sub_x;
|
||||
|
||||
uint8_t luma = subpixel_g[pixel_idx];
|
||||
sum_luma += luma;
|
||||
|
||||
int is_active = (luma > thresh_limit);
|
||||
if (invert)
|
||||
is_active = !is_active;
|
||||
|
||||
if (is_active) {
|
||||
braille_offset |= get_braille_bitmask(dx, dy);
|
||||
active_count++;
|
||||
if (dir_buf)
|
||||
dom_dir_votes[dir_buf[pixel_idx]]++;
|
||||
}
|
||||
|
||||
if (do_color) {
|
||||
const uint8_t *px = subpixel_rgb + pixel_idx * 3;
|
||||
sum_r += px[0];
|
||||
sum_g += px[1];
|
||||
sum_b += px[2];
|
||||
colored_subpixels++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dir_buf) {
|
||||
int best = 0;
|
||||
for (int d = 1; d < 4; d++)
|
||||
if (dom_dir_votes[d] > dom_dir_votes[best])
|
||||
best = d;
|
||||
dom_dir = (uint8_t)best;
|
||||
}
|
||||
|
||||
int avg_r = 0, avg_g = 0, avg_b = 0;
|
||||
if (do_color && colored_subpixels > 0) {
|
||||
avg_r = (int)(sum_r / colored_subpixels);
|
||||
avg_g = (int)(sum_g / colored_subpixels);
|
||||
avg_b = (int)(sum_b / colored_subpixels);
|
||||
}
|
||||
int avg_luma = (int)(sum_luma / 8);
|
||||
|
||||
switch (render_mode) {
|
||||
case RENDER_BRAILLE: {
|
||||
uint32_t unicode_val = 0x2800 + braille_offset;
|
||||
char utf8_seq[4];
|
||||
utf8_seq[0] = (char)(0xE0 | ((unicode_val >> 12) & 0x0F));
|
||||
utf8_seq[1] = (char)(0x80 | ((unicode_val >> 6) & 0x3F));
|
||||
utf8_seq[2] = (char)(0x80 | (unicode_val & 0x3F));
|
||||
utf8_seq[3] = '\0';
|
||||
out_idx = emit_glyph(out, out_size, out_idx, utf8_seq, do_color, avg_r,
|
||||
avg_g, avg_b);
|
||||
break;
|
||||
}
|
||||
case RENDER_BLOCKS: {
|
||||
int shade_idx = avg_luma * BLOCK_SHADE_COUNT / 256;
|
||||
if (shade_idx >= BLOCK_SHADE_COUNT)
|
||||
shade_idx = BLOCK_SHADE_COUNT - 1;
|
||||
if (invert)
|
||||
shade_idx = BLOCK_SHADE_COUNT - 1 - shade_idx;
|
||||
out_idx =
|
||||
emit_glyph(out, out_size, out_idx, BLOCK_SHADE_UTF8[shade_idx],
|
||||
do_color, avg_r, avg_g, avg_b);
|
||||
break;
|
||||
}
|
||||
case RENDER_DOTS: {
|
||||
int dot_idx = avg_luma * DOT_COUNT / 256;
|
||||
if (dot_idx >= DOT_COUNT)
|
||||
dot_idx = DOT_COUNT - 1;
|
||||
if (invert)
|
||||
dot_idx = DOT_COUNT - 1 - dot_idx;
|
||||
out_idx = emit_glyph(out, out_size, out_idx, DOT_UTF8[dot_idx],
|
||||
do_color, avg_r, avg_g, avg_b);
|
||||
break;
|
||||
}
|
||||
case RENDER_ASCII_RAMP:
|
||||
default: {
|
||||
char glyph[2] = {0, 0};
|
||||
if (dir_buf && active_count > 0) {
|
||||
static const char dirs[4] = {'-', '/', '|', '\\'};
|
||||
glyph[0] = dirs[dom_dir];
|
||||
} else {
|
||||
int ramp_idx = avg_luma * ramp_len / 256;
|
||||
if (ramp_idx >= ramp_len)
|
||||
ramp_idx = ramp_len - 1;
|
||||
if (invert)
|
||||
ramp_idx = ramp_len - 1 - ramp_idx;
|
||||
glyph[0] = ramp[ramp_idx];
|
||||
}
|
||||
out_idx = emit_glyph(out, out_size, out_idx, glyph, do_color, avg_r,
|
||||
avg_g, avg_b);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (do_color) {
|
||||
int w = snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n");
|
||||
if (w > 0 && (size_t)(out_idx + w) < out_size)
|
||||
out_idx += w;
|
||||
int written =
|
||||
snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n");
|
||||
if (written > 0 && (size_t)(out_idx + written) < out_size)
|
||||
out_idx += written;
|
||||
} else {
|
||||
if ((size_t)(out_idx + 1) < out_size)
|
||||
out[out_idx++] = '\n';
|
||||
|
|
@ -254,15 +760,18 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
|
|||
}
|
||||
|
||||
out[(size_t)out_idx < out_size ? (size_t)out_idx : out_size - 1] = '\0';
|
||||
free(small_g);
|
||||
free(small_rgb);
|
||||
|
||||
free(subpixel_g);
|
||||
free(subpixel_rgb);
|
||||
free(dir_buf);
|
||||
return out_idx;
|
||||
}
|
||||
|
||||
// FPS overlay
|
||||
// FPS Overlay Configuration
|
||||
void overlay_fps_box(int dst_w, double fps, int color_enabled) {
|
||||
char buf[80];
|
||||
int col = (dst_w - 13) / 2 + 1;
|
||||
int term_w = dst_w / 2;
|
||||
int col = (term_w - 13) / 2 + 1;
|
||||
if (col < 1)
|
||||
col = 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
#ifdef PLATFORM_LINUX
|
||||
|
||||
#include "nolibc.h"
|
||||
|
||||
#include "ascii.h"
|
||||
#include "capture.h"
|
||||
#include "platform.h"
|
||||
|
||||
#ifdef PLATFORM_LINUX
|
||||
|
||||
#include "nolibc.h"
|
||||
#include <linux/videodev2.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
|
@ -126,4 +127,4 @@ void webcam_cleanup(webcam_t *cam) {
|
|||
cam->impl = (webcam_impl_t *)0;
|
||||
}
|
||||
|
||||
#endif /* PLATFORM_LINUX */
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#ifdef PLATFORM_MACOS
|
||||
#include "capture.h"
|
||||
#include "platform.h"
|
||||
|
||||
#ifdef PLATFORM_MACOS
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define FRAME_BUFS 2
|
||||
|
||||
|
|
|
|||
236
C/src/main.c
236
C/src/main.c
|
|
@ -1,11 +1,11 @@
|
|||
#include "nolibc.h"
|
||||
|
||||
#include "ascii.h"
|
||||
#include "capture.h"
|
||||
#include "plugins.h"
|
||||
#include "thread_sharing.h"
|
||||
#include "timing.h"
|
||||
|
||||
#include "nolibc.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
|
@ -13,10 +13,11 @@
|
|||
// Defaults
|
||||
#define DEFAULT_ASCII_WIDTH 80
|
||||
#define DEFAULT_ASCII_HEIGHT 40
|
||||
#define DEFAULT_CAPTURE_WIDTH 160
|
||||
#define DEFAULT_CAPTURE_HEIGHT 120
|
||||
#define DEFAULT_CAPTURE_WIDTH 640
|
||||
#define DEFAULT_CAPTURE_HEIGHT 480
|
||||
#define DEFAULT_FPS 20
|
||||
#define MAX_PLUGINS 8
|
||||
#define DEFAULT_CHARSET_DIR "./charsets"
|
||||
|
||||
// Signal handling
|
||||
volatile sig_atomic_t keep_running = 1;
|
||||
|
|
@ -55,18 +56,55 @@ static void print_usage(const char *prog) {
|
|||
" -H <height> ASCII output rows (default: %d) \n"
|
||||
" -s <chars> custom charset string (default: \"%s\") \n"
|
||||
" -p <path> filter plugin .so path \n"
|
||||
" -m <mode> render mode: braille|blocks|ascii|halfblock|dots\n"
|
||||
" -k <dir> charset directory (hot-reloadable .txt ramps) \n"
|
||||
"\n"
|
||||
"Image adjustments:\n"
|
||||
" -b <val> brightness offset -128..128 (default: 0)\n"
|
||||
" -c <val> contrast in percent >0; 100=none (default: 100)\n"
|
||||
" -i invert mapping \n"
|
||||
" -e enable Sobel edge detection \n"
|
||||
" -E <mode> edge mode off|sobel|sobel-dir|laplacian\n"
|
||||
" -C ANSI truecolor output \n"
|
||||
" -D Floyd-Steinberg dithering \n",
|
||||
" -D Floyd-Steinberg dithering \n"
|
||||
" -P <0-100> depth-pop 3D parallax strength (0=off) \n"
|
||||
"\n"
|
||||
"Live keybindings:\n"
|
||||
" m / M cycle render mode forward / backward \n"
|
||||
" x / X cycle edge detection mode forward / backward \n"
|
||||
" n / N cycle loaded charset forward / backward \n"
|
||||
" p / o increase / decrease depth-pop strength \n"
|
||||
" up/down select plugin [ ] +-1 { } +-10 r reset \n"
|
||||
" q quit \n",
|
||||
prog, DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT, DEFAULT_FPS,
|
||||
DEFAULT_ASCII_WIDTH, DEFAULT_ASCII_HEIGHT, ASCII_CHARS_DEFAULT);
|
||||
}
|
||||
|
||||
static render_mode_t parse_render_mode(const char *s) {
|
||||
if (nl_strcmp(s, "braille") == 0)
|
||||
return RENDER_BRAILLE;
|
||||
if (nl_strcmp(s, "blocks") == 0)
|
||||
return RENDER_BLOCKS;
|
||||
if (nl_strcmp(s, "ascii") == 0)
|
||||
return RENDER_ASCII_RAMP;
|
||||
if (nl_strcmp(s, "halfblock") == 0)
|
||||
return RENDER_HALF_BLOCK;
|
||||
if (nl_strcmp(s, "dots") == 0)
|
||||
return RENDER_DOTS;
|
||||
return RENDER_BRAILLE;
|
||||
}
|
||||
|
||||
static edge_mode_t parse_edge_mode(const char *s) {
|
||||
if (nl_strcmp(s, "off") == 0)
|
||||
return EDGE_OFF;
|
||||
if (nl_strcmp(s, "sobel") == 0)
|
||||
return EDGE_SOBEL;
|
||||
if (nl_strcmp(s, "sobel-dir") == 0)
|
||||
return EDGE_SOBEL_DIR;
|
||||
if (nl_strcmp(s, "laplacian") == 0)
|
||||
return EDGE_LAPLACIAN;
|
||||
return EDGE_OFF;
|
||||
}
|
||||
|
||||
// termios
|
||||
void term_raw_mode(void) {
|
||||
tcgetattr(STDIN_FILENO, &orig_terminal); // save stdin state
|
||||
|
|
@ -81,7 +119,8 @@ void term_restore(void) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_terminal); }
|
|||
|
||||
static void overlay_panel(int ascii_h, double fps, plugin_loader_t *plugins,
|
||||
int *plugin_params, int count, int selected,
|
||||
int color) {
|
||||
int color, const ascii_opts_t *opts,
|
||||
const charset_registry_t *charsets) {
|
||||
char buf[1024];
|
||||
int n, base_row = ascii_h + 1; // 1-indexed panel row
|
||||
|
||||
|
|
@ -103,7 +142,32 @@ static void overlay_panel(int ascii_h, double fps, plugin_loader_t *plugins,
|
|||
if (n > 0 && n < (int)sizeof(buf))
|
||||
(void)write(STDOUT_FILENO, buf, (size_t)n);
|
||||
|
||||
// Mode/edge/charset/depth-pop status row
|
||||
const char *cset_name = (charsets && charsets->count > 0 &&
|
||||
opts->render_mode == RENDER_ASCII_RAMP)
|
||||
? charsets->sets[charsets->active].name
|
||||
: "-";
|
||||
n = nl_snprintf(buf, sizeof(buf), "\033[%d;1H\033[K", base_row + 1);
|
||||
if (n > 0)
|
||||
(void)write(STDOUT_FILENO, buf, (size_t)n);
|
||||
if (color) {
|
||||
n = nl_snprintf(buf, sizeof(buf),
|
||||
"\033[38;2;0;180;220m mode: %s (m/M) edges: %s (x/X) "
|
||||
"charset: %s (n/N) depth-pop: %d%s (+/-, v)\033[0m\033[K",
|
||||
render_mode_name(opts->render_mode),
|
||||
edge_mode_name(opts->edges), cset_name, opts->depth_pop,
|
||||
opts->depth_invert ? " [inv]" : "");
|
||||
} else {
|
||||
n = nl_snprintf(buf, sizeof(buf),
|
||||
" mode: %s edges: %s charset: %s depth-pop: %d%s\033[K",
|
||||
render_mode_name(opts->render_mode),
|
||||
edge_mode_name(opts->edges), cset_name, opts->depth_pop,
|
||||
opts->depth_invert ? " [inv]" : "");
|
||||
}
|
||||
if (n > 0 && n < (int)sizeof(buf))
|
||||
(void)write(STDOUT_FILENO, buf, (size_t)n);
|
||||
|
||||
n = nl_snprintf(buf, sizeof(buf), "\033[%d;1H\033[K", base_row + 2);
|
||||
if (n > 0)
|
||||
(void)write(STDOUT_FILENO, buf, (size_t)n);
|
||||
|
||||
|
|
@ -163,15 +227,20 @@ int main(int argc, char *argv[]) {
|
|||
int cap_w = DEFAULT_CAPTURE_WIDTH;
|
||||
int cap_h = DEFAULT_CAPTURE_HEIGHT;
|
||||
int fps = DEFAULT_FPS;
|
||||
const char *charset_dir = DEFAULT_CHARSET_DIR;
|
||||
|
||||
ascii_opts_t opts = {
|
||||
.brightness = 0,
|
||||
.contrast = 100,
|
||||
.invert = 0,
|
||||
.color = 0,
|
||||
.edges = 0,
|
||||
.edges = EDGE_OFF,
|
||||
.dither = 0,
|
||||
.threshold_val = 35,
|
||||
.charset = NULL,
|
||||
.render_mode = RENDER_BRAILLE,
|
||||
.depth_pop = 0,
|
||||
.depth_invert = 0,
|
||||
};
|
||||
|
||||
// Plugins
|
||||
|
|
@ -180,7 +249,7 @@ int main(int argc, char *argv[]) {
|
|||
|
||||
// CLI parsing
|
||||
int opt;
|
||||
while ((opt = nl_getopt(argc, argv, "d:W:H:w:h:f:b:c:iCDes:p:")) != -1)
|
||||
while ((opt = nl_getopt(argc, argv, "d:W:H:w:h:f:b:c:iCDs:p:m:E:k:P:")) != -1)
|
||||
switch (opt) {
|
||||
case 'd':
|
||||
device = optarg;
|
||||
|
|
@ -224,8 +293,21 @@ int main(int argc, char *argv[]) {
|
|||
case 'C':
|
||||
opts.color = 1;
|
||||
break;
|
||||
case 'e':
|
||||
opts.edges = 1;
|
||||
case 'E':
|
||||
opts.edges = parse_edge_mode(optarg);
|
||||
break;
|
||||
case 'm':
|
||||
opts.render_mode = parse_render_mode(optarg);
|
||||
break;
|
||||
case 'k':
|
||||
charset_dir = optarg;
|
||||
break;
|
||||
case 'P':
|
||||
opts.depth_pop = my_atoi(optarg);
|
||||
if (opts.depth_pop < 0)
|
||||
opts.depth_pop = 0;
|
||||
if (opts.depth_pop > 100)
|
||||
opts.depth_pop = 100;
|
||||
break;
|
||||
case 'D':
|
||||
opts.dither = 1;
|
||||
|
|
@ -275,10 +357,11 @@ int main(int argc, char *argv[]) {
|
|||
}
|
||||
fprintf(stderr,
|
||||
"Device: %s | capture %dx%d | ASCII %dx%d | %d fps | %d "
|
||||
"plugin(s)%s%s%s%s\n",
|
||||
"plugin(s) | mode: %s%s%s%s\n",
|
||||
device, cam.width, cam.height, ascii_w, ascii_h, fps, plugin_count,
|
||||
opts.color ? " | color" : "", opts.edges ? " | edges" : "",
|
||||
opts.dither ? " | dither" : "", opts.invert ? " | inverted" : "");
|
||||
render_mode_name(opts.render_mode), opts.color ? " | color" : "",
|
||||
opts.edges != EDGE_OFF ? " | edges" : "",
|
||||
opts.dither ? " | dither" : "");
|
||||
|
||||
// Pixel buffers allocation
|
||||
int cam_pixels = cam.width * cam.height;
|
||||
|
|
@ -293,7 +376,13 @@ int main(int argc, char *argv[]) {
|
|||
}
|
||||
|
||||
// Allocate output string buffer
|
||||
size_t out_size = ascii_out_size(ascii_w, ascii_h, opts.color);
|
||||
size_t out_size = 0;
|
||||
for (render_mode_t rm = 0; rm < RENDER_MODE_COUNT; rm++) {
|
||||
size_t s =
|
||||
ascii_out_size_for_mode(ascii_w * 2, ascii_h * 4, opts.color, rm);
|
||||
if (s > out_size)
|
||||
out_size = s;
|
||||
}
|
||||
char *out_buf = malloc(out_size);
|
||||
|
||||
if (!out_buf) {
|
||||
|
|
@ -304,6 +393,11 @@ int main(int argc, char *argv[]) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
// Charset registry, hot-reloadable ramps from charset_dir
|
||||
charset_registry_t charsets;
|
||||
charset_registry_init(&charsets, charset_dir);
|
||||
opts.charset = charset_registry_active_ramp(&charsets);
|
||||
|
||||
// // Thead sharing
|
||||
// shared_frame_t sf = {0};
|
||||
// sf.buf[0] = malloc(cam_pixels);
|
||||
|
|
@ -366,33 +460,68 @@ int main(int argc, char *argv[]) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// adjust plugin param if selected
|
||||
int *p = (plugin_count > 0) ? &plugin_params[selected] : NULL;
|
||||
// Adjust plugin param if selected
|
||||
int *pp = (plugin_count > 0) ? &plugin_params[selected] : NULL;
|
||||
switch (ch) {
|
||||
case 'q':
|
||||
case 'Q':
|
||||
keep_running = 0;
|
||||
break;
|
||||
case ']':
|
||||
if (p && *p < 255)
|
||||
(*p)++;
|
||||
if (pp && *pp < 255)
|
||||
(*pp)++;
|
||||
break;
|
||||
case '[':
|
||||
if (p && *p > 0)
|
||||
(*p)--;
|
||||
if (pp && *pp > 0)
|
||||
(*pp)--;
|
||||
break;
|
||||
case '}':
|
||||
if (p)
|
||||
*p = (*p + 10 > 255) ? 255 : *p + 10;
|
||||
if (pp)
|
||||
*pp = (*pp + 10 > 255) ? 255 : *pp + 10;
|
||||
break;
|
||||
case '{':
|
||||
if (p)
|
||||
*p = (*p - 10 < 0) ? 0 : *p - 10;
|
||||
if (pp)
|
||||
*pp = (*pp - 10 < 0) ? 0 : *pp - 10;
|
||||
break;
|
||||
case 'r':
|
||||
case 'R':
|
||||
if (p)
|
||||
*p = 128;
|
||||
if (pp)
|
||||
*pp = 128;
|
||||
break;
|
||||
case 'm':
|
||||
opts.render_mode = (opts.render_mode + 1) % RENDER_MODE_COUNT;
|
||||
break;
|
||||
case 'M':
|
||||
opts.render_mode =
|
||||
(opts.render_mode - 1 + RENDER_MODE_COUNT) % RENDER_MODE_COUNT;
|
||||
break;
|
||||
case 'x':
|
||||
opts.edges = (opts.edges + 1) % EDGE_MODE_COUNT;
|
||||
break;
|
||||
case 'X':
|
||||
opts.edges = (opts.edges - 1 + EDGE_MODE_COUNT) % EDGE_MODE_COUNT;
|
||||
break;
|
||||
case 'n':
|
||||
if (charsets.count > 0) {
|
||||
charsets.active = (charsets.active + 1) % charsets.count;
|
||||
opts.charset = charset_registry_active_ramp(&charsets);
|
||||
}
|
||||
break;
|
||||
case 'N':
|
||||
if (charsets.count > 0) {
|
||||
charsets.active =
|
||||
(charsets.active - 1 + charsets.count) % charsets.count;
|
||||
opts.charset = charset_registry_active_ramp(&charsets);
|
||||
}
|
||||
break;
|
||||
case '+':
|
||||
opts.depth_pop = (opts.depth_pop + 5 > 100) ? 100 : opts.depth_pop + 5;
|
||||
break;
|
||||
case '-':
|
||||
opts.depth_pop = (opts.depth_pop - 5 < 0) ? 0 : opts.depth_pop - 5;
|
||||
break;
|
||||
case 'v':
|
||||
opts.depth_invert = !opts.depth_invert;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -403,6 +532,10 @@ int main(int argc, char *argv[]) {
|
|||
for (int i = 0; i < plugin_count; i++)
|
||||
plugin_check_reload(&plugins[i]);
|
||||
|
||||
// Hot-reload check for charset ramps
|
||||
charset_registry_check_reload(&charsets);
|
||||
opts.charset = charset_registry_active_ramp(&charsets);
|
||||
|
||||
// Frame capture
|
||||
if (webcam_wait_frame(&cam, 1000) < 0)
|
||||
continue; // timeout, retry
|
||||
|
|
@ -419,18 +552,56 @@ int main(int argc, char *argv[]) {
|
|||
&plugin_params[i]);
|
||||
}
|
||||
|
||||
if (opts.color && rgb)
|
||||
// NOTE: cam.buffer is the V4L2 mmap region (Linux only)
|
||||
// On macOS, capture_macos.c delivers luma only; cam.buffer is NULL
|
||||
// TODO: Add color support for macOS
|
||||
// Color mode is therefore a Linux-only feature for now.
|
||||
if (opts.color && rgb && cam.buffer && cam.buffer != MAP_FAILED)
|
||||
yuyv_to_rgb((const uint8_t *)cam.buffer, rgb, cam.width, cam.height);
|
||||
|
||||
int len = grayscale_to_ascii(gray, rgb, cam.width, cam.height, ascii_w,
|
||||
ascii_h, out_buf, out_size, &opts);
|
||||
// Calculate proper subpixel dimensions
|
||||
int subpixel_w = ascii_w;
|
||||
int subpixel_h = ascii_h;
|
||||
|
||||
switch (opts.render_mode) {
|
||||
case RENDER_BRAILLE:
|
||||
subpixel_w = ascii_w * 2;
|
||||
subpixel_h = ascii_h * 4;
|
||||
break;
|
||||
case RENDER_HALF_BLOCK:
|
||||
subpixel_w = ascii_w * 1;
|
||||
subpixel_h = ascii_h * 2;
|
||||
break;
|
||||
default:
|
||||
// RENDER_BLOCKS, RENDER_ASCII_RAMP, RENDER_DOTS are 1x1 per cell
|
||||
subpixel_w = ascii_w * 2;
|
||||
subpixel_h = ascii_h * 4;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t out_size = ascii_out_size_for_mode(subpixel_w, subpixel_h,
|
||||
opts.color, opts.render_mode);
|
||||
char *out_buf = malloc(out_size);
|
||||
if (!out_buf) {
|
||||
perror("Failed to allocate text output buffer");
|
||||
break;
|
||||
}
|
||||
|
||||
// Process frame mapping using dynamically calculated bounds
|
||||
int len = grayscale_to_ascii(gray, rgb, cam.width, cam.height, subpixel_w,
|
||||
subpixel_h, out_buf, out_size, &opts);
|
||||
if (len > (int)out_size) {
|
||||
fprintf(stderr, "WARNING: len=%d > out_size=%zu\n", len, out_size);
|
||||
}
|
||||
|
||||
if (len > 0) {
|
||||
(void)write(STDOUT_FILENO, out_buf, (size_t)len);
|
||||
overlay_panel(ascii_h, current_fps, plugins, plugin_params, plugin_count,
|
||||
selected, opts.color);
|
||||
selected, opts.color, &opts, &charsets);
|
||||
}
|
||||
|
||||
free(out_buf);
|
||||
|
||||
if (webcam_requeue_buffer(&cam) < 0) {
|
||||
perror("requeue_buffer");
|
||||
break;
|
||||
|
|
@ -442,7 +613,7 @@ int main(int argc, char *argv[]) {
|
|||
// Cleanup
|
||||
term_restore();
|
||||
// \033[2J = erase screen, \033[H = cursor home, \033[?25h = show cursor
|
||||
static const char CLEANUP[] = "\033[2J\033[H\033[0m\033[?25h\n";
|
||||
static const char CLEANUP[] = "\033[2J\033[H\033[0m\033[?25h";
|
||||
(void)write(STDOUT_FILENO, CLEANUP, sizeof(CLEANUP) - 1);
|
||||
fprintf(stderr, "Stopped.\n");
|
||||
|
||||
|
|
@ -451,6 +622,7 @@ int main(int argc, char *argv[]) {
|
|||
free(out_buf);
|
||||
for (int i = 0; i < plugin_count; i++)
|
||||
plugin_cleanup(&plugins[i]);
|
||||
charset_registry_cleanup(&charsets);
|
||||
webcam_cleanup(&cam);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
#include "nolibc.h"
|
||||
|
||||
#include "plugins.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <sys/inotify.h>
|
||||
|
||||
#include "nolibc.h"
|
||||
|
||||
static int copy_file(const char *src, const char *dst) {
|
||||
int fd_src = open(src, O_RDONLY);
|
||||
if (fd_src < 0) {
|
||||
|
|
@ -67,17 +67,9 @@ int plugin_load(plugin_loader_t *pl, const char *path) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
void *sym = dlsym(pl->dl_handle, "plugin_get");
|
||||
if (!sym) {
|
||||
fprintf(stderr, "[plugin] dlsym: %s\n", dlerror());
|
||||
dlclose(pl->dl_handle);
|
||||
pl->dl_handle = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
filter_plugin_t *(*get_plugin)(void) = dlsym(pl->dl_handle, "plugin_get");
|
||||
if (!get_plugin) {
|
||||
fprintf(stderr, "[plugin] dlsym: %s\n", dlerror());
|
||||
fprintf(stderr, "[plugin] dlsym plugin_get: %s\n", dlerror());
|
||||
dlclose(pl->dl_handle);
|
||||
pl->dl_handle = NULL;
|
||||
unlink(pl->tmp_path);
|
||||
|
|
@ -88,9 +80,6 @@ int plugin_load(plugin_loader_t *pl, const char *path) {
|
|||
pl->plugin = get_plugin();
|
||||
snprintf(pl->status_msg, sizeof(pl->status_msg), "loaded: %s",
|
||||
pl->plugin->name);
|
||||
|
||||
// FIX: Temporary file leak in plugin_load() when dlsym fails
|
||||
// unlink(pl->tmp_path)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
/*
|
||||
Still uses pthread functions, TODO: replace them with raw futex syscalls and clone()
|
||||
Still uses pthread functions, TODO: replace them with raw futex syscalls and
|
||||
clone()
|
||||
*/
|
||||
|
||||
#include "nolibc.h"
|
||||
|
||||
#include "ascii.h"
|
||||
#include "capture.h"
|
||||
|
|
@ -9,8 +11,6 @@ Still uses pthread functions, TODO: replace them with raw futex syscalls and clo
|
|||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "nolibc.h"
|
||||
|
||||
// Producer thread for capturing frames
|
||||
void *capture_thread(void *arg) {
|
||||
shared_frame_t *sf = arg;
|
||||
|
|
@ -53,11 +53,13 @@ void *capture_thread(void *arg) {
|
|||
void *render_thread(void *arg) {
|
||||
shared_frame_t *sf = arg;
|
||||
|
||||
// Allocate dynamic buffers
|
||||
size_t out_size = ascii_out_size(sf->ascii_w, sf->ascii_h, sf->opts.color);
|
||||
char *out_buf = malloc(out_size);
|
||||
uint8_t *local_rgb =
|
||||
sf->opts.color ? malloc(sf->width * sf->height * 3) : NULL;
|
||||
int braille_w = sf->ascii_w * 2;
|
||||
int braille_h = sf->ascii_h * 4;
|
||||
size_t out_size = ascii_out_size(braille_w, braille_h, sf->opts.color);
|
||||
char *out_buf = nl_malloc(out_size);
|
||||
|
||||
// TODO: extend shared_frame_t to carry an rgb double-buffer alongside gray
|
||||
uint8_t *local_rgb = NULL;
|
||||
|
||||
if (!out_buf) {
|
||||
perror("render_thread malloc");
|
||||
|
|
@ -78,9 +80,9 @@ void *render_thread(void *arg) {
|
|||
pthread_mutex_unlock(&sf->lock);
|
||||
|
||||
// Process frame outside locked state
|
||||
int len = grayscale_to_ascii(sf->buf[read_idx], local_rgb, sf->width,
|
||||
sf->height, sf->ascii_w, sf->ascii_h, out_buf,
|
||||
out_size, &sf->opts);
|
||||
int len =
|
||||
grayscale_to_ascii(sf->buf[read_idx], local_rgb, sf->width, sf->height,
|
||||
braille_w, braille_h, out_buf, out_size, &sf->opts);
|
||||
|
||||
if (len > 0) {
|
||||
write(STDOUT_FILENO, "\033[H", 3); // cursor to top-left
|
||||
|
|
@ -92,11 +94,10 @@ void *render_thread(void *arg) {
|
|||
|
||||
pthread_mutex_unlock(&sf->lock);
|
||||
|
||||
// Cleanup memory
|
||||
free(out_buf);
|
||||
if (local_rgb) {
|
||||
free(local_rgb);
|
||||
}
|
||||
// local_rgb is NULL in this path (see comment above); free is a no-op but
|
||||
// safe
|
||||
free(local_rgb);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#include "nolibc.h"
|
||||
|
||||
#include "timing.h"
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#include "nolibc.h"
|
||||
|
||||
static long frame_duration_ns = 0; // nanoseconds per frame
|
||||
|
||||
void timing_init(int fps) { frame_duration_ns = 1000000000L / fps; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue