Add macos support

This commit is contained in:
Harshit-Dhanwalkar 2026-06-10 20:30:10 +05:30
parent 49b58febba
commit 14cc7dcc2e
10 changed files with 616 additions and 250 deletions

View file

@ -1,81 +1,156 @@
CC = gcc
CFLAGS = -Wall -Wextra -O2 -Iinclude -Ilib
# AsciiCam cross-platform build
# Supports:
# Linux x86_64 (gcc, nolibc, V4L2)
# macOS ARM64 (clang, system libc, AVFoundation)
# macOS x86_64 (clang, system libc, AVFoundation)
UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
ifeq ($(UNAME_S),Linux)
CFLAGS += -fno-stack-protector -D__LINUX_NOLIBC__
# LDFLAGS = -lm
LDFLAGS += -nostdlib # drops crt startup files and default libs
# LDFLAGS += -nodefaultlibs # drops default libs but keeps crt startup files
LDFLAGS += -Wl,--no-as-needed
LDLIBS := -ldl -lpthread -lc
# LDFLAGS += -Wl,-rpath,/lib/x86_64-linux-gnu
# LDFLAGS += /lib/x86_64-linux-gnu/libdl.so.2
LDFLAGS += -Wl,-dynamic-linker,/lib64/ld-linux-x86-64.so.2
LDFLAGS += /usr/lib/x86_64-linux-gnu/crti.o
LDFLAGS += /usr/lib/x86_64-linux-gnu/crtn.o
LDFLAGS += -ldl # Dynamic loading symbols
LDFLAGS += -lpthread # Multi-threaded capture-render, pthreads producer/consumer
LDFLAGS += -lc
LDFLAGS += -msse4.1 # SIMD - SSE2 for the YUYV to gray conversion
# LDFLAGS += -static
LIBSRCS = $(wildcard lib/*.c)
PLATFORM := linux
CC := gcc
LD := gcc
else ifeq ($(UNAME_S),Darwin)
CFLAGS +=
LDFLAGS +=
LDLIBS :=
LIBSRCS =
PLATFORM := macos
CC := clang
LD := clang
OBJC := clang
else
$(error Unsupported platform: $(UNAME_S))
endif
SRCDIR = src
INCDIR = include
LIBDIR = lib
BUILDDIR = build
OBJDIR = $(BUILDDIR)/obj
SRCDIR := src
LIBDIR := lib
FILTERDIR := filters
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
LIBSRCS = $(wildcard $(LIBDIR)/*.c)
SOURCES = $(wildcard $(SRCDIR)/*.c) $(LIBSRCS)
OBJECTS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES))
TARGET = $(BUILDDIR)/webcam_ascii
PLUGIN_SRCS = $(wildcard filters/*.c)
PLUGIN_C_SRCS = $(filter-out filters/%.h, $(PLUGIN_SRCS))
PLUGIN_TARGETS = $(patsubst filters/%.c,$(BUILDDIR)/%.so,$(PLUGIN_C_SRCS))
CFLAGS_COMMON := -Wall -Wextra -O2 -Iinclude -I$(LIBDIR) -fno-stack-protector -fno-builtin-memcpy -fno-builtin-memset -fno-builtin-strlen
CFLAGS_COMMON +=
# Security Testing Targets
TEST_TARGET = $(BUILDDIR)/security_tests
TEST_SRC = ../tests/security_tests.c
FILTER_SRCS = filters/edge_detect.c filters/invert.c filters/threshold.c
ifeq ($(PLATFORM),linux)
# Linux: nolibc, V4L2, SSE4.1
CFLAGS := $(CFLAGS_COMMON) -DPLATFORM_LINUX -D__LINUX_NOLIBC__
CFLAGS += -msse4.1
.PHONY: all clean plugins test
LDFLAGS := -nostdlib
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 += -msse4.1
LIBSRCS := $(LIBDIR)/nl_alloc.c \
$(LIBDIR)/nl_errno.c \
$(LIBDIR)/nl_getopt.c \
$(LIBDIR)/nl_printf.c \
$(LIBDIR)/nl_start.c
PLAT_SRC := $(SRCDIR)/capture_linux.c
# Plugin shared objects
SO_EXT := so
SO_FLAGS := -fPIC -shared
else ifeq ($(PLATFORM),macos)
# macOS: system libc + AVFoundation, supports ARM64 + x86_64
CFLAGS := $(CFLAGS_COMMON) -DPLATFORM_MACOS
OBJCFLAGS:= $(CFLAGS) -x objective-c \
-fobjc-arc \
-fmodules
ifeq ($(UNAME_M),arm64)
CFLAGS += -arch arm64
LDFLAGS := -arch arm64
else
CFLAGS += -arch x86_64 -msse4.1
LDFLAGS := -arch x86_64
endif
LDFLAGS += -framework AVFoundation \
-framework CoreMedia \
-framework CoreVideo \
-framework Foundation \
-lpthread
# TODO: Update nolibc for macos support instead of system system libc, currently :
# No nolibc on macOS, uses system libc
LIBSRCS :=
PLAT_SRC := $(SRCDIR)/capture_macos.m
# Plugin shared objects
SO_EXT := dylib
SO_FLAGS := -dynamiclib
endif
CORE_SRCS := $(SRCDIR)/ascii.c \
$(SRCDIR)/main.c \
$(SRCDIR)/plugins.c \
$(SRCDIR)/thread_sharing.c \
$(SRCDIR)/timing.c
ALL_C_SRCS := $(CORE_SRCS) $(PLAT_SRC) $(LIBSRCS)
OBJECTS := $(patsubst $(SRCDIR)/%.c, $(OBJDIR)/%.o, $(CORE_SRCS))
OBJECTS += $(patsubst $(SRCDIR)/%.c, $(OBJDIR)/%.o, $(filter $(SRCDIR)/%.c, $(PLAT_SRC)))
OBJECTS += $(patsubst $(SRCDIR)/%.m, $(OBJDIR)/%.o, $(filter $(SRCDIR)/%.m, $(PLAT_SRC)))
OBJECTS += $(patsubst $(LIBDIR)/%.c, $(OBJDIR)/lib_%.o, $(LIBSRCS))
TARGET := $(BUILDDIR)/webcam_ascii
# Plugins
PLUGIN_SRCS := $(wildcard $(FILTERDIR)/*.c)
PLUGIN_TARGETS := $(patsubst $(FILTERDIR)/%.c, $(BUILDDIR)/%.$(SO_EXT), $(PLUGIN_SRCS))
.PHONY: all clean plugins info
all: $(TARGET) plugins
# Main binary
$(TARGET): $(OBJECTS)
$(CC) $^ -o $@ $(LDFLAGS)
$(TARGET): $(OBJECTS) | $(BUILDDIR)
$(LD) $^ -o $@ $(LDFLAGS)
@echo " LD $@ ($(PLATFORM)/$(UNAME_M))"
# Compile .c
$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
# Compile .m (Objective-C for macOS capture)
$(OBJDIR)/%.o: $(SRCDIR)/%.m | $(OBJDIR)
$(OBJC) $(OBJCFLAGS) -c $< -o $@
# Compile nolibc (Linux only)
$(OBJDIR)/lib_%.o: $(LIBDIR)/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
# Plugins
plugins: $(PLUGIN_TARGETS)
$(BUILDDIR)/%.$(SO_EXT): $(FILTERDIR)/%.c | $(BUILDDIR)
ifeq ($(PLATFORM),linux)
$(CC) $(CFLAGS) $(SO_FLAGS) $< -o $@.tmp && mv $@.tmp $@
else
$(CC) $(CFLAGS) $(SO_FLAGS) $< -o $@ \
-undefined dynamic_lookup
endif
$(OBJDIR):
mkdir -p $(OBJDIR)
# Plugins compilation
plugins: $(PLUGIN_TARGETS)
$(BUILDDIR)/%.so: filters/%.c | $(BUILDDIR)
$(CC) $(CFLAGS) -fPIC -shared $< -o $@.tmp
mv $@.tmp $@
# Automated Unit Tests Execution
test: $(BUILDDIR)
$(CC) -I.. $(CFLAGS) -DTESTING $(TEST_SRC) $(FILTER_SRCS) -o $(TEST_TARGET) -lcheck -lsubunit -lm -lrt -lpthread
@echo " RUNNING ALL PLUGINS INVARIANT TEST "
./$(TEST_TARGET)
$(BUILDDIR):
mkdir -p $(BUILDDIR)
# Cleanup
clean:
rm -rf $(BUILDDIR)
info:
@echo "Platform : $(PLATFORM)"
@echo "Arch : $(UNAME_M)"
@echo "CC : $(CC)"
@echo "CFLAGS : $(CFLAGS)"
@echo "LDFLAGS : $(LDFLAGS)"
@echo "Objects : $(OBJECTS)"

View file

@ -1,18 +1,22 @@
#ifndef CAPTURE_H
#define CAPTURE_H
#include <stddef.h>
#include <stdint.h>
#include <linux/videodev2.h>
typedef struct webcam_impl webcam_impl_t;
typedef struct {
int fd;
int width;
int height;
void *buffer;
struct v4l2_buffer buf_info;
int fd; /* Linux: V4L2 fd. macOS: -1 (unused externally) */
int width;
int height;
void *buffer;
webcam_impl_t *impl;
} webcam_t;
// Initialize webcam
// On Linux: device = "/dev/video0"
// On macOS: device = NULL (uses system default camera) or a device name string
int webcam_init(webcam_t *cam, const char *device, int width, int height);
// Wait for frame to be ready

20
C/include/platform.h Normal file
View file

@ -0,0 +1,20 @@
#ifndef PLATFORM_H
#define PLATFORM_H
#if defined(__linux__)
#define PLATFORM_LINUX 1
#elif defined(__APPLE__) && defined(__MACH__)
#define PLATFORM_MACOS 1
#else
#error "Unsupported platform (only Linux and macOS are supported)"
#endif
#if defined(__x86_64__) || defined(_M_X64)
#define ARCH_X86_64 1
#elif defined(__aarch64__) || defined(_M_ARM64)
#define ARCH_ARM64 1
#else
#error "Unsupported architecture (only x86_64 and ARM64 are supported)"
#endif
#endif

View file

@ -6,11 +6,15 @@
inotify
*/
#include <termios.h>
#include <time.h>
#ifdef __LINUX_NOLIBC__
#ifndef MAP_FAILED
#define MAP_FAILED ((void *)-1)
#endif
#include "nl_syscall.h"
/* Basic I/O */
// Basic I/O
static inline ssize_t nl_write(int fd, const void *buf, size_t n) {
return (ssize_t)__sc3(SYS_write, fd, (long)buf, (long)n);
}
@ -28,7 +32,20 @@ static inline int nl_ioctl(int fd, unsigned long req, void *arg) {
return (int)__sc3(SYS_ioctl, fd, (long)req, (long)arg);
}
/* mmap / munmap */
// fprintf / stderr
#define stderr 2
#define fprintf(fd, fmt, ...) \
do { \
char _fb[1024]; \
int _fn = nl_snprintf(_fb, sizeof(_fb), fmt, ##__VA_ARGS__); \
if (_fn > 0) { \
size_t _nw = \
(_fn < (int)sizeof(_fb) - 1) ? (size_t)_fn : sizeof(_fb) - 1; \
nl_write((int)(long)(fd), _fb, _nw); \
} \
} while (0)
// mmap / munmap
static inline void *nl_mmap(void *addr, size_t len, int prot, int flags, int fd,
long off) {
return (void *)__sc6(SYS_mmap, (long)addr, (long)len, prot, flags, fd, off);
@ -37,7 +54,7 @@ static inline int nl_munmap(void *addr, size_t len) {
return (int)__sc2(SYS_munmap, (long)addr, (long)len);
}
/* select */
// select
struct nl_timeval {
long tv_sec;
long tv_usec;
@ -55,7 +72,7 @@ static inline int nl_select(int nfds, nl_fd_set *r, nl_fd_set *w, nl_fd_set *e,
return (int)__sc6(SYS_select, nfds, (long)r, (long)w, (long)e, (long)tv, 0);
}
/* clock / sleep */
// clock / sleep
static inline int nl_clock_gettime(clockid_t id, struct timespec *ts) {
return (int)__sc2(SYS_clock_gettime, id, (long)ts);
}
@ -78,7 +95,7 @@ static inline void nl_exit(int code) {
__builtin_unreachable();
}
/* inotify */
// inotify
static inline int nl_inotify_init1(int flags) {
return (int)__sc1(SYS_inotify_init1, flags);
}
@ -87,12 +104,16 @@ static inline int nl_inotify_add_watch(int fd, const char *path,
return (int)__sc3(SYS_inotify_add_watch, fd, (long)path, mask);
}
/* termios via ioctl */
// termios via ioctl
#include <termios.h>
#define NL_TCGETS 0x5401
#define NL_TCSETS 0x5402
#define NL_TCSETSF 0x5404
#define TCSANOW 0
#define TCSADRAIN 1
#define TCSAFLUSH 2
static inline int nl_tcgetattr(int fd, struct termios *t) {
return nl_ioctl(fd, NL_TCGETS, t);
}
@ -101,7 +122,7 @@ static inline int nl_tcsetattr(int fd, int action, const struct termios *t) {
return nl_ioctl(fd, req, (void *)t);
}
/* Macro redirects */
// Macro redirects
#define write(fd, buf, n) nl_write(fd, buf, n)
#define read(fd, buf, n) nl_read(fd, buf, n)
#define _open2(p, f) nl_open(p, f, 0)

View file

@ -1,5 +1,4 @@
#include "nl_printf.h"
// #include "nolibc.h"
#include "nolibc.h"
#include <stdarg.h>
#include <stddef.h>
@ -163,3 +162,7 @@ int nl_snprintf(char *buf, size_t size, const char *fmt, ...) {
va_end(ap);
return r;
}
void nl_eprint(const char *msg) {
nl_write(2, msg, nl_strlen(msg));
}

View file

@ -1,17 +1,13 @@
#ifndef NL_PRINTF_H
#define NL_PRINTF_H
#include "nl_io.h"
#include "nl_string.h"
#include <stdarg.h>
#include <stddef.h>
int nl_vsnprintf(char *buf, size_t size, const char *fmt, va_list ap);
int nl_snprintf(char *buf, size_t size, const char *fmt, ...);
static inline void nl_eprint(const char *msg) {
nl_write(2, msg, nl_strlen(msg));
}
void nl_eprint(const char *msg);
static inline int nl_fmt_fps(char *buf, size_t sz, double fps) {
int whole = (int)fps;
@ -25,15 +21,4 @@ static inline int nl_fmt_fps(char *buf, size_t sz, double fps) {
#define snprintf nl_snprintf
#define stderr 2
#define fprintf(fd, fmt, ...) \
do { \
char _fb[1024]; \
int _fn = nl_snprintf(_fb, sizeof(_fb), fmt, ##__VA_ARGS__); \
if (_fn > 0) { \
size_t _nwrite = \
(_fn < (int)sizeof(_fb) - 1) ? (size_t)_fn : sizeof(_fb) - 1; \
nl_write((int)(long)(fd), _fb, _nwrite); \
} \
} while (0)
#endif

View file

@ -1,8 +1,13 @@
#include "ascii.h"
// NOTE:
// SIMD paths:
// - 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 "platform.h"
// Helpers
static inline uint8_t clamp_u8(int v) {
@ -10,101 +15,95 @@ static inline uint8_t clamp_u8(int v) {
}
static inline int my_abs(int x) { return x < 0 ? -x : x; }
// Image conversion
#ifdef __x86_64__
// YUYV to grayscale
#if defined(ARCH_X86_64)
#include <immintrin.h>
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width,
int height) {
int total_pixels = width * height;
// Mask to extract every even byte (Y samples)
__m128i mask = _mm_set1_epi16(0x00FF);
int i = 0;
for (; i + 16 <= total_pixels; i += 16) {
// Load 32 bytes of YUYV (= 16 pixels)
__m128i lo = _mm_loadu_si128((__m128i *)(yuyv + i * 2));
__m128i hi = _mm_loadu_si128((__m128i *)(yuyv + i * 2 + 16));
// low byte of each 16-bit word (the Y sample)
lo = _mm_and_si128(lo, mask);
hi = _mm_and_si128(hi, mask);
// Pack 16-bit
__m128i result = _mm_packus_epi16(lo, hi);
_mm_storeu_si128((__m128i *)(gray + i), result);
}
for (; i < total_pixels; i++)
gray[i] = yuyv[i * 2];
}
#else
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width,
int height) {
int total = width * height;
for (int i = 0; i < total; i++) {
gray[i] = yuyv[i * 2];
__m128i mask = _mm_set1_epi16(0x00FF);
int i = 0;
for (; i + 16 <= total; i += 16) {
__m128i lo = _mm_loadu_si128((__m128i *)(yuyv + i * 2));
__m128i hi = _mm_loadu_si128((__m128i *)(yuyv + i * 2 + 16));
lo = _mm_and_si128(lo, mask);
hi = _mm_and_si128(hi, mask);
_mm_storeu_si128((__m128i *)(gray + i), _mm_packus_epi16(lo, hi));
}
for (; i < total; i++)
gray[i] = yuyv[i * 2];
}
#elif defined(ARCH_ARM64)
#include <arm_neon.h>
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width,
int height) {
int total = width * height;
// NEON: process 8 YUYV pairs (= 16 px) per iteration
int i = 0;
for (; i + 16 <= total; i += 16) {
// Load 32 bytes: [Y0 U0 Y1 V0 Y2 U1 Y3 V1 ...]
uint8x16x2_t yuv = vld2q_u8(yuyv + i * 2);
// yuv.val[0] = all Y bytes (even bytes = luma)
vst1q_u8(gray + i, yuv.val[0]);
}
for (; i < total; i++)
gray[i] = yuyv[i * 2];
}
#else
// fallback
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width,
int height) {
int total = width * height;
for (int i = 0; i < total; i++)
gray[i] = yuyv[i * 2];
}
#endif
void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height) {
int pairs = (width * height) / 2;
for (int i = 0; i < pairs; i++) {
int y0 = yuyv[i * 4 + 0];
int u = yuyv[i * 4 + 1];
int y1 = yuyv[i * 4 + 2];
int v = yuyv[i * 4 + 3];
int d = u - 128;
int e = v - 128;
int y0 = yuyv[i * 4 + 0], u = yuyv[i * 4 + 1];
int y1 = yuyv[i * 4 + 2], v = yuyv[i * 4 + 3];
int d = u - 128, e = v - 128;
for (int p = 0; p < 2; p++) {
int c = ((p == 0) ? y0 : y1) - 16;
uint8_t *px = rgb + (i * 2 + p) * 3;
px[0] = clamp_u8((298 * c + 409 * e + 128) >> 8); // R
px[1] = clamp_u8((298 * c - 100 * d - 208 * e + 128) >> 8); // G
px[2] = clamp_u8((298 * c + 516 * d + 128) >> 8); // B
px[0] = clamp_u8((298 * c + 409 * e + 128) >> 8);
px[1] = clamp_u8((298 * c - 100 * d - 208 * e + 128) >> 8);
px[2] = clamp_u8((298 * c + 516 * d + 128) >> 8);
}
}
}
// Buffer sizing
size_t ascii_out_size(int dst_w, int dst_h, int color) {
/* prefix: 3 bytes + 1 Char */
if (color) {
/* Per cell: "\033[38;2;255;255;255m" (20) + char (1) = 21
* Per row end: "\033[0m\n" (6) */
if (color)
return 3 + (size_t)dst_h * ((size_t)dst_w * 21 + 6) + 1;
} else {
/* Per cell: 1 byte; per row: + newline */
return 3 + (size_t)dst_h * ((size_t)dst_w + 1) + 1;
}
return 3 + (size_t)dst_h * ((size_t)dst_w + 1) + 1;
}
// Sobel edge detection (kernel convolution)
// Sobel edge detection
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}};
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 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;
}
}
// TEST: test both L1 and L2 normalisations
int mag = my_abs(gx) + my_abs(gy); // L1 normalisation
// int mag = sqrt(gx * gx + gy * gy); // L2 normalisation
int mag = my_abs(gx) + my_abs(gy);
out[y * w + x] = (uint8_t)(mag > 255 ? 255 : mag);
}
}
}
// Grayscale to ascii
// Grayscale to ASCII
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) {
@ -114,17 +113,15 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
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 != NULL);
int do_color = opts && opts->color && rgb;
int do_edges = opts ? opts->edges : 0;
int do_dither = opts ? opts->dither : 0;
// Blocking width and height pixels in source pixels
double bw = (double)src_w / dst_w;
double bh = (double)src_h / dst_h;
// Downsample to (dst_w x dst_h)
uint8_t *small_g = malloc(dst_w * dst_h);
uint8_t *small_rgb = do_color ? malloc(dst_w * dst_h * 3) : NULL;
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;
if (!small_g || (do_color && !small_rgb)) {
free(small_g);
@ -133,21 +130,17 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
}
for (int y = 0; y < dst_h; y++) {
int ys = (int)(y * bh);
int ye = (int)((y + 1) * bh);
int ys = (int)(y * bh), ye = (int)((y + 1) * bh);
if (ye <= ys)
ye = ys + 1;
for (int x = 0; x < dst_w; x++) {
int xs = (int)(x * bw);
int xe = (int)((x + 1) * bw);
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) {
@ -158,11 +151,9 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
}
count++;
}
}
if (count == 0)
if (!count)
count = 1;
// Brightness and contrast
int gv = (int)(tg / count);
if (contrast != 100)
gv = 128 + (gv - 128) * contrast / 100;
@ -170,75 +161,62 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
small_g[y * dst_w + x] = clamp_u8(gv);
if (do_color) {
uint8_t *out_px = small_rgb + (y * dst_w + x) * 3;
out_px[0] = clamp_u8((int)(tr / count));
out_px[1] = clamp_u8((int)(tgv / count));
out_px[2] = clamp_u8((int)(tb / count));
uint8_t *op = small_rgb + (y * 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));
}
}
}
// Sobel edge detection
if (do_edges) {
// 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);
// overwite grayscale image with edge map
memcpy(small_g, edge_buf, dst_w * dst_h);
free(edge_buf);
uint8_t *eb = calloc((size_t)(dst_w * dst_h), 1);
if (eb) {
sobel(small_g, eb, dst_w, dst_h);
nl_memcpy(small_g, eb, (size_t)(dst_w * dst_h));
free(eb);
}
}
// Floyd-Steinberg dithering
if (do_dither) {
int16_t *eb = malloc(dst_w * dst_h * sizeof(int16_t));
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];
// Quantise to nearest charset level
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_ADD(DY, DX, NUM) \
#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 * (NUM) / 16); \
eb[ny * dst_w + nx] += (int16_t)(err * (N) / 16); \
} while (0)
FS_ADD(0, 1, 7);
FS_ADD(1, -1, 3);
FS_ADD(1, 0, 5);
FS_ADD(1, 1, 1);
#undef FS_ADD
FS(0, 1, 7);
FS(1, -1, 3);
FS(1, 0, 5);
FS(1, 1, 1);
#undef FS
}
}
for (int i = 0; i < dst_w * dst_h; i++)
small_g[i] = clamp_u8(eb[i]);
free(eb);
}
/* HACK: If malloc fails silently fall back to no dithering */
}
// Render into caller's buffer
int out_idx = 0;
static const char HOME[] = "\033[H"; // Cursor repositions without erasing
static const char HOME[] = "\033[H";
if (out_size > sizeof(HOME)) {
memcpy(out, HOME, sizeof(HOME) - 1);
nl_memcpy(out, HOME, sizeof(HOME) - 1);
out_idx = sizeof(HOME) - 1;
}
@ -252,7 +230,6 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
idx = nchars - 1;
if (invert)
idx = nchars - 1 - idx;
char ch = charset[idx];
if (do_color) {
@ -266,7 +243,6 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
out[out_idx++] = ch;
}
}
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)
@ -278,7 +254,6 @@ 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);
return out_idx;
@ -291,19 +266,18 @@ void overlay_fps_box(int dst_w, double fps, int color_enabled) {
if (col < 1)
col = 1;
char fpsbuf[10];
nl_fmt_fps(fpsbuf, sizeof(fpsbuf), fps);
int n;
if (color_enabled) {
char fpsbuf[10];
nl_fmt_fps(fpsbuf, sizeof(fpsbuf), fps);
if (color_enabled)
n = snprintf(
buf, sizeof(buf),
"\033[1;%dH\033[38;2;0;255;0m\033[48;2;30;30;30m[ FPS: %s ]\033[0m",
col, fpsbuf);
} else {
char fpsbuf[10];
nl_fmt_fps(fpsbuf, sizeof(fpsbuf), fps);
else
n = snprintf(buf, sizeof(buf), "\033[1;%dH[ FPS: %s ]", col, fpsbuf);
}
if (n > 0 && n < (int)sizeof(buf))
(void)write(STDOUT_FILENO, buf, (size_t)n);
}

View file

@ -1,22 +1,34 @@
#include "capture.h"
#include "ascii.h"
#include "capture.h"
#include "platform.h"
#ifdef PLATFORM_LINUX
#include "nolibc.h"
#include <linux/videodev2.h>
#include <stdint.h>
#include "nolibc.h"
typedef struct webcam_impl webcam_impl_t;
struct webcam_impl {
struct v4l2_buffer buf_info;
};
static webcam_impl_t _impl_storage;
int webcam_init(webcam_t *cam, const char *device, int width, int height) {
// Open device (nonblocking for select usage)
cam->fd = open(device, O_RDWR | O_NONBLOCK, 0);
cam->impl = &_impl_storage;
cam->buffer = MAP_FAILED;
// Open device non-blocking (for select)
cam->fd = open(device ? device : "/dev/video0", O_RDWR | O_NONBLOCK, 0);
if (cam->fd < 0)
return -1;
// Set format
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = width;
fmt.fmt.pix.height = height;
fmt.fmt.pix.width = (unsigned)width;
fmt.fmt.pix.height = (unsigned)height;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
@ -25,11 +37,9 @@ int webcam_init(webcam_t *cam, const char *device, int width, int height) {
return -1;
}
// Read back actual resolution
cam->width = fmt.fmt.pix.width;
cam->height = fmt.fmt.pix.height;
cam->width = (int)fmt.fmt.pix.width;
cam->height = (int)fmt.fmt.pix.height;
// Request one mmap buffer
struct v4l2_requestbuffers req = {0};
req.count = 1;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
@ -40,7 +50,6 @@ int webcam_init(webcam_t *cam, const char *device, int width, int height) {
return -1;
}
// Query buffer info
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
@ -51,25 +60,21 @@ int webcam_init(webcam_t *cam, const char *device, int width, int height) {
return -1;
}
// mmap
cam->buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED,
cam->fd, buf.m.offset);
cam->fd, (long)buf.m.offset);
if (cam->buffer == MAP_FAILED) {
close(cam->fd);
return -1;
}
// Store buffer info for later munmap and requeue
cam->buf_info = buf;
cam->impl->buf_info = buf;
// Queue the buffer
if (ioctl(cam->fd, VIDIOC_QBUF, &buf) < 0) {
munmap(cam->buffer, buf.length);
close(cam->fd);
return -1;
}
// Start streaming
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(cam->fd, VIDIOC_STREAMON, &type) < 0) {
munmap(cam->buffer, buf.length);
@ -89,47 +94,36 @@ int webcam_wait_frame(webcam_t *cam, int timeout_ms) {
tv.tv_usec = (timeout_ms % 1000) * 1000;
int ret = nl_select(cam->fd + 1, &fds, (nl_fd_set *)0, (nl_fd_set *)0, &tv);
if (ret <= 0)
return -1; // timeout or error
return 0;
return (ret <= 0) ? -1 : 0;
}
int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer) {
// Dequeue buffer
struct v4l2_buffer buf = cam->buf_info;
struct v4l2_buffer buf = cam->impl->buf_info;
if (ioctl(cam->fd, VIDIOC_DQBUF, &buf) < 0)
return -1;
// Convert YUYV -> grayscale (Y component)
// uint8_t *yuyv = (uint8_t *)cam->buffer;
// for (int i = 0, j = 0; i < cam->width * cam->height * 2; i += 2, j++) {
// gray_buffer[j] = yuyv[i];
// }
int w = cam->width;
int h = cam->height;
yuyv_to_gray_simd((uint8_t *)cam->buffer, gray_buffer, w, h);
yuyv_to_gray_simd((uint8_t *)cam->buffer, gray_buffer, cam->width,
cam->height);
// Store updated buffer info for requeue
cam->buf_info = buf;
cam->impl->buf_info = buf;
return 0;
}
int webcam_requeue_buffer(webcam_t *cam) {
if (ioctl(cam->fd, VIDIOC_QBUF, &cam->buf_info) < 0)
return -1;
return 0;
return (ioctl(cam->fd, VIDIOC_QBUF, &cam->impl->buf_info) < 0) ? -1 : 0;
}
void webcam_cleanup(webcam_t *cam) {
if (cam->fd >= 0) {
// Stop streaming
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(cam->fd, VIDIOC_STREAMOFF, &type);
// Unmap and close
if (cam->buffer != MAP_FAILED)
munmap(cam->buffer, cam->buf_info.length);
munmap(cam->buffer, cam->impl->buf_info.length);
close(cam->fd);
}
cam->fd = -1;
cam->buffer = MAP_FAILED;
cam->impl = (webcam_impl_t *)0;
}
#endif /* PLATFORM_LINUX */

297
C/src/capture_macos.c Normal file
View file

@ -0,0 +1,297 @@
#include "capture.h"
#include "platform.h"
#ifdef PLATFORM_MACOS
#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
struct webcam_impl {
// AVFoundation objects
AVCaptureSession *session;
AVCaptureDeviceInput *input;
AVCaptureVideoDataOutput *output;
id<AVCaptureVideoDataOutputSampleBufferDelegate> delegate;
dispatch_queue_t queue;
// Frame ring buffer
uint8_t *gray_buf[FRAME_BUFS];
int buf_w, buf_h;
int write_idx;
int ready_idx;
int has_frame;
pthread_mutex_t lock;
pthread_cond_t cond;
int stopped;
};
@interface FrameDelegate
: NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
@property(assign) struct webcam_impl *impl;
@end
@implementation FrameDelegate
- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
struct webcam_impl *im = self.impl;
if (!im || im->stopped)
return;
CVPixelBufferRef pixbuf = CMSampleBufferGetImageBuffer(sampleBuffer);
if (!pixbuf)
return;
CVPixelBufferLockBaseAddress(pixbuf, kCVPixelBufferLock_ReadOnly);
// Request NV12 (YUV 4:2:0 biplanar).
// Plane 0 is pure luma (Y)
size_t width = CVPixelBufferGetWidth(pixbuf);
size_t height = CVPixelBufferGetHeight(pixbuf);
uint8_t *y_plane = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixbuf, 0);
size_t y_stride = CVPixelBufferGetBytesPerRowOfPlane(pixbuf, 0);
pthread_mutex_lock(&im->lock);
int wi = im->write_idx;
uint8_t *dst = im->gray_buf[wi];
// Copy luma plane row-by-row
for (size_t row = 0; row < height; row++) {
memcpy(dst + row * width, y_plane + row * y_stride, width);
}
im->ready_idx = wi;
im->has_frame = 1;
im->write_idx = wi ^ 1; // swap to other slot
pthread_cond_signal(&im->cond);
pthread_mutex_unlock(&im->lock);
CVPixelBufferUnlockBaseAddress(pixbuf, kCVPixelBufferLock_ReadOnly);
}
@end
int webcam_init(webcam_t *cam, const char *device, int width, int height) {
struct webcam_impl *im = calloc(1, sizeof(struct webcam_impl));
if (!im)
return -1;
pthread_mutex_init(&im->lock, NULL);
pthread_cond_init(&im->cond, NULL);
// Allocate two grayscale frame buffers
im->buf_w = width;
im->buf_h = height;
for (int i = 0; i < FRAME_BUFS; i++) {
im->gray_buf[i] = calloc((size_t)(width * height), 1);
if (!im->gray_buf[i])
goto fail;
}
// Find the camera device
AVCaptureDevice *dev = nil;
if (device) {
// Match by localizedName
NSString *devName = [NSString stringWithUTF8String:device];
NSArray<AVCaptureDevice *> *devices =
[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *d in devices) {
if ([d.localizedName isEqualToString:devName]) {
dev = d;
break;
}
}
}
if (!dev) {
// Fall back to system default
if (@available(macOS 10.15, *)) {
AVCaptureDeviceDiscoverySession *ds = [AVCaptureDeviceDiscoverySession
discoverySessionWithDeviceTypes:@[
AVCaptureDeviceTypeBuiltInWideAngleCamera
]
mediaType:AVMediaTypeVideo
position:AVCaptureDevicePositionUnspecified];
dev = ds.devices.firstObject;
} else {
dev = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
}
}
if (!dev) {
goto fail;
}
// Configure format
AVCaptureDeviceFormat *best_fmt = nil;
float best_diff = 1e9f;
for (AVCaptureDeviceFormat *fmt in dev.formats) {
CMFormatDescriptionRef desc = fmt.formatDescription;
CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(desc);
float diff = (float)((dim.width - width) * (dim.width - width) +
(dim.height - height) * (dim.height - height));
if (diff < best_diff) {
best_diff = diff;
best_fmt = fmt;
}
}
if (best_fmt) {
CMVideoDimensions dim =
CMVideoFormatDescriptionGetDimensions(best_fmt.formatDescription);
cam->width = dim.width;
cam->height = dim.height;
im->buf_w = cam->width;
im->buf_h = cam->height;
if (cam->width != width || cam->height != height) {
for (int i = 0; i < FRAME_BUFS; i++) {
free(im->gray_buf[i]);
im->gray_buf[i] = calloc((size_t)(cam->width * cam->height), 1);
if (!im->gray_buf[i])
goto fail;
}
}
if ([dev lockForConfiguration:nil]) {
dev.activeFormat = best_fmt;
[dev unlockForConfiguration];
}
} else {
cam->width = width;
cam->height = height;
}
im->session = [[AVCaptureSession alloc] init];
[im->session beginConfiguration];
im->session.sessionPreset = AVCaptureSessionPresetInputPriority;
NSError *err = nil;
im->input = [AVCaptureDeviceInput deviceInputWithDevice:dev error:&err];
if (!im->input || err)
goto fail_session;
if (![im->session canAddInput:im->input])
goto fail_session;
[im->session addInput:im->input];
im->output = [[AVCaptureVideoDataOutput alloc] init];
im->output.videoSettings = @{
(NSString *)kCVPixelBufferPixelFormatTypeKey :
@(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
};
im->output.alwaysDiscardsLateVideoFrames = YES;
im->queue = dispatch_queue_create("asciicam.capture", DISPATCH_QUEUE_SERIAL);
FrameDelegate *delegate = [[FrameDelegate alloc] init];
delegate.impl = im;
im->delegate = delegate;
[im->output setSampleBufferDelegate:delegate queue:im->queue];
if (![im->session canAddOutput:im->output])
goto fail_session;
[im->session addOutput:im->output];
[im->session commitConfiguration];
[im->session startRunning];
cam->impl = im;
cam->fd = -1;
cam->buffer = NULL;
return 0;
fail_session:
[im->session commitConfiguration];
fail:
for (int i = 0; i < FRAME_BUFS; i++)
free(im->gray_buf[i]);
pthread_mutex_destroy(&im->lock);
pthread_cond_destroy(&im->cond);
free(im);
return -1;
}
int webcam_wait_frame(webcam_t *cam, int timeout_ms) {
struct webcam_impl *im = cam->impl;
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout_ms / 1000;
ts.tv_nsec += (timeout_ms % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) {
ts.tv_sec++;
ts.tv_nsec -= 1000000000L;
}
pthread_mutex_lock(&im->lock);
while (!im->has_frame && !im->stopped) {
if (pthread_cond_timedwait(&im->cond, &im->lock, &ts) != 0) {
pthread_mutex_unlock(&im->lock);
return -1; // timeout
}
}
int ok = im->has_frame;
pthread_mutex_unlock(&im->lock);
return ok ? 0 : -1;
}
int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer) {
struct webcam_impl *im = cam->impl;
pthread_mutex_lock(&im->lock);
if (!im->has_frame) {
pthread_mutex_unlock(&im->lock);
return -1;
}
int ri = im->ready_idx;
im->has_frame = 0;
pthread_mutex_unlock(&im->lock);
// Copy the luma buffer out
memcpy(gray_buffer, im->gray_buf[ri], (size_t)(im->buf_w * im->buf_h));
return 0;
}
int webcam_requeue_buffer(webcam_t *cam) {
(void)cam;
return 0; // AVFoundation manages buffers
}
void webcam_cleanup(webcam_t *cam) {
struct webcam_impl *im = cam->impl;
if (!im)
return;
pthread_mutex_lock(&im->lock);
im->stopped = 1;
pthread_cond_broadcast(&im->cond);
pthread_mutex_unlock(&im->lock);
if (im->session) {
[im->session stopRunning];
im->session = nil;
im->input = nil;
im->output = nil;
im->delegate = nil;
}
for (int i = 0; i < FRAME_BUFS; i++)
free(im->gray_buf[i]);
pthread_mutex_destroy(&im->lock);
pthread_cond_destroy(&im->cond);
free(im);
cam->impl = NULL;
cam->fd = -1;
cam->buffer = NULL;
}
#endif /* PLATFORM_MACOS */

View file

@ -4,19 +4,12 @@
#include "thread_sharing.h"
#include "timing.h"
#include "nolibc.h"
#include <pthread.h>
#include <stdint.h>
#include <time.h>
#include "nolibc.h"
typedef int sig_atomic_t;
#define MAP_FAILED ((void *)-1)
#define PROT_READ 1
#define PROT_WRITE 2
#define MAP_SHARED 1
// Defaults
#define DEFAULT_ASCII_WIDTH 80
#define DEFAULT_ASCII_HEIGHT 40