mirror of
https://github.com/Harshit-Dhanwalkar/AsciiCam.git
synced 2026-06-12 10:35:13 +02:00
Add macos support
This commit is contained in:
parent
49b58febba
commit
14cc7dcc2e
10 changed files with 616 additions and 250 deletions
189
C/Makefile
189
C/Makefile
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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
20
C/include/platform.h
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
202
C/src/ascii.c
202
C/src/ascii.c
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (non‑blocking 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
297
C/src/capture_macos.c
Normal 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 */
|
||||
11
C/src/main.c
11
C/src/main.c
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue