diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..291c17b --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +CC = gcc +CFLAGS = -Wall -Wextra -O2 -Iinclude +LDFLAGS = -lm + +SRCDIR = src +INCDIR = include +OBJDIR = obj +BINDIR = . + +SOURCES = $(wildcard $(SRCDIR)/*.c) +OBJECTS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES)) +TARGET = $(BINDIR)/webcam_ascii + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(CC) $^ -o $@ $(LDFLAGS) + +$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR) + $(CC) $(CFLAGS) -c $< -o $@ + +$(OBJDIR): + mkdir -p $(OBJDIR) + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/README.md b/README.md index fd5f70e..ba33641 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # AsciiCam Ascii video output from your webcam in your terminal. -![Demo](assets/output.gif) +![Demo](assets/demo.gif) ## TODO - [x] Adjust width and height of capturing frame. -- [ ] Add feature to record and save it in popular video formats like mp4 and gif. +- [ ] Add feature to record and save it in popular video formats like `.mp4`, `.mov` and `.gif`. diff --git a/assests/output.gif b/assests/output.gif deleted file mode 100644 index 0e8d2c3..0000000 Binary files a/assests/output.gif and /dev/null differ diff --git a/assets/output.gif b/assets/output.gif new file mode 100644 index 0000000..7937ba0 Binary files /dev/null and b/assets/output.gif differ diff --git a/include/ascii.h b/include/ascii.h new file mode 100644 index 0000000..b51edc8 --- /dev/null +++ b/include/ascii.h @@ -0,0 +1,15 @@ +#ifndef ASCII_H +#define ASCII_H + +#include + +#define ASCII_CHARS " .:-=+*#%@" + +// Convert YUYV raw data to grayscale +void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height); + +// grayscale to ASCII output grid +char *grayscale_to_ascii(const uint8_t *gray, int src_w, int src_h, + int dst_w, int dst_h); + +#endif diff --git a/include/capture.h b/include/capture.h new file mode 100644 index 0000000..f948b33 --- /dev/null +++ b/include/capture.h @@ -0,0 +1,30 @@ +#ifndef CAPTURE_H +#define CAPTURE_H + +#include +#include + +typedef struct { + int fd; + int width; + int height; + void *buffer; + struct v4l2_buffer buf_info; +} webcam_t; + +// Initialize webcam +int webcam_init(webcam_t *cam, const char *device, int width, int height); + +// Wait for frame to be ready +int webcam_wait_frame(webcam_t *cam, int timeout_ms); + +// Capture frame, dequeue buffer, fill grayscale output buffer +int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer); + +// Re‑queue buffer +int webcam_requeue_buffer(webcam_t *cam); + +// Stop streaming and clean up resources +void webcam_cleanup(webcam_t *cam); + +#endif diff --git a/include/timing.h b/include/timing.h new file mode 100644 index 0000000..5c57539 --- /dev/null +++ b/include/timing.h @@ -0,0 +1,11 @@ +#ifndef TIMING_H +#define TIMING_H + +#include + +// Initialize framerate control +void timing_init(int fps); + +void timing_sleep(struct timespec *start_time); + +#endif diff --git a/src/ascii.c b/src/ascii.c new file mode 100644 index 0000000..ae47652 --- /dev/null +++ b/src/ascii.c @@ -0,0 +1,47 @@ +#include "ascii.h" +#include +#include +#include + +void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height) { + for (int i = 0, j = 0; i < width * height * 2; i += 2, j++) { + gray[j] = yuyv[i]; + } +} + +char *grayscale_to_ascii(const uint8_t *gray, int src_w, int src_h, + int dst_w, int dst_h) { + // Allocate output string: each row has dst_w chars + newline, plus null terminator + char *output = malloc(dst_w * dst_h + dst_h + 1); + if (!output) return NULL; + + double block_w = (double)src_w / dst_w; + double block_h = (double)src_h / dst_h; + const char *ascii = ASCII_CHARS; + int ascii_len = strlen(ascii); + + int out_idx = 0; + for (int y = 0; y < dst_h; y++) { + for (int x = 0; x < dst_w; x++) { + long total = 0; + int count = 0; + int y_start = (int)(y * block_h); + int y_end = (int)((y + 1) * block_h); + int x_start = (int)(x * block_w); + int x_end = (int)((x + 1) * block_w); + + for (int sy = y_start; sy < y_end && sy < src_h; sy++) { + for (int sx = x_start; sx < x_end && sx < src_w; sx++) { + total += gray[sy * src_w + sx]; + count++; + } + } + unsigned char avg = (count > 0) ? (total / count) : 0; + int idx = avg * (ascii_len - 1) / 255; + output[out_idx++] = ascii[idx]; + } + output[out_idx++] = '\n'; + } + output[out_idx] = '\0'; + return output; +} diff --git a/src/capture.c b/src/capture.c new file mode 100644 index 0000000..dcbc479 --- /dev/null +++ b/src/capture.c @@ -0,0 +1,133 @@ +#include "capture.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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); + 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.pixelformat = V4L2_PIX_FMT_YUYV; + fmt.fmt.pix.field = V4L2_FIELD_NONE; + + if (ioctl(cam->fd, VIDIOC_S_FMT, &fmt) < 0) { + close(cam->fd); + return -1; + } + + // Read back actual resolution + cam->width = fmt.fmt.pix.width; + cam->height = fmt.fmt.pix.height; + + // Request one mmap buffer + struct v4l2_requestbuffers req = {0}; + req.count = 1; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; + + if (ioctl(cam->fd, VIDIOC_REQBUFS, &req) < 0) { + close(cam->fd); + return -1; + } + + // Query buffer info + struct v4l2_buffer buf = {0}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = 0; + + if (ioctl(cam->fd, VIDIOC_QUERYBUF, &buf) < 0) { + close(cam->fd); + return -1; + } + + // mmap + cam->buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, + MAP_SHARED, cam->fd, 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; + + // 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); + close(cam->fd); + return -1; + } + + return 0; +} + +int webcam_wait_frame(webcam_t *cam, int timeout_ms) { + fd_set fds; + struct timeval tv; + FD_ZERO(&fds); + FD_SET(cam->fd, &fds); + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + + int ret = select(cam->fd + 1, &fds, NULL, NULL, &tv); + if (ret <= 0) return -1; // timeout or error + return 0; +} + +int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer) { + // Dequeue buffer + struct v4l2_buffer buf = cam->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]; + } + + // Store updated buffer info for requeue + cam->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; +} + +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); + close(cam->fd); + } + cam->fd = -1; + cam->buffer = MAP_FAILED; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..8f68c79 --- /dev/null +++ b/src/main.c @@ -0,0 +1,110 @@ +#include "capture.h" +#include "ascii.h" +#include "timing.h" +#include +#include +#include +#include +#include +#include +#include + +// Defaults +#define DEFAULT_ASCII_WIDTH 80 +#define DEFAULT_ASCII_HEIGHT 40 +#define DEFAULT_CAPTURE_WIDTH 160 +#define DEFAULT_CAPTURE_HEIGHT 120 +#define DEFAULT_FPS 20 + +volatile sig_atomic_t keep_running = 1; +void handle_signal(int sig) { (void)sig; keep_running = 0; } + +void print_usage(char *prog) { + fprintf(stderr, + "Usage: %s [-d ] [-W ] [-H ] [-f ]\n", + prog); + fprintf(stderr, " -d : video device (default: /dev/video0)\n"); + fprintf(stderr, " -W : ASCII output width (default: %d)\n", DEFAULT_ASCII_WIDTH); + fprintf(stderr, " -H : ASCII output height (default: %d)\n", DEFAULT_ASCII_HEIGHT); + fprintf(stderr, " -f : target framerate (default: %d)\n", DEFAULT_FPS); +} + +int main(int argc, char *argv[]) { + signal(SIGINT, handle_signal); + + // Config + char *device = "/dev/video0"; + int ascii_w = DEFAULT_ASCII_WIDTH; + int ascii_h = DEFAULT_ASCII_HEIGHT; + int fps = DEFAULT_FPS; + + int opt; + while ((opt = getopt(argc, argv, "d:W:H:f:")) != -1) { + switch (opt) { + case 'd': device = optarg; break; + case 'W': ascii_w = atoi(optarg); if (ascii_w <= 0) ascii_w = DEFAULT_ASCII_WIDTH; break; + case 'H': ascii_h = atoi(optarg); if (ascii_h <= 0) ascii_h = DEFAULT_ASCII_HEIGHT; break; + case 'f': fps = atoi(optarg); if (fps <= 0) fps = DEFAULT_FPS; break; + default: print_usage(argv[0]); return 1; + } + } + + // Capture resolution (fixed for simplicity, could also be made configurable) + int cap_w = DEFAULT_CAPTURE_WIDTH; + int cap_h = DEFAULT_CAPTURE_HEIGHT; + + timing_init(fps); + + webcam_t cam = { .fd = -1, .buffer = MAP_FAILED }; + if (webcam_init(&cam, device, cap_w, cap_h) < 0) { + perror("webcam_init"); + return 1; + } + printf("Webcam opened: %s, capture resolution %dx%d\n", device, cam.width, cam.height); + + uint8_t *gray = malloc(cam.width * cam.height); + if (!gray) { + perror("malloc gray"); + webcam_cleanup(&cam); + return 1; + } + + printf("Starting ASCII stream (%dx%d), target %d fps. Press Ctrl+C to stop.\n", + ascii_w, ascii_h, fps); + + struct timespec start_time; + while (keep_running) { + clock_gettime(CLOCK_MONOTONIC, &start_time); + + if (webcam_wait_frame(&cam, 1000) < 0) { + // timeout or error, just continue + continue; + } + + if (webcam_capture_frame(&cam, gray) < 0) { + perror("capture_frame"); + break; + } + + char *ascii_art = grayscale_to_ascii(gray, cam.width, cam.height, + ascii_w, ascii_h); + if (ascii_art) { + printf("\033[2J\033[H"); // clear screen, home cursor + fputs(ascii_art, stdout); + fflush(stdout); + free(ascii_art); + } + + if (webcam_requeue_buffer(&cam) < 0) { + perror("requeue_buffer"); + break; + } + + timing_sleep(&start_time); + } + + printf("\nStopping...\n"); + free(gray); + webcam_cleanup(&cam); + return 0; +} diff --git a/src/timing.c b/src/timing.c new file mode 100644 index 0000000..d28c094 --- /dev/null +++ b/src/timing.c @@ -0,0 +1,22 @@ +#include "timing.h" +#include +#include + +static long frame_duration_ns = 0; // nanoseconds per frame + +void timing_init(int fps) { + frame_duration_ns = 1000000000L / fps; +} + +void timing_sleep(struct timespec *start_time) { + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + long elapsed_ns = (end_time.tv_sec - start_time->tv_sec) * 1000000000L + + (end_time.tv_nsec - start_time->tv_nsec); + long sleep_ns = frame_duration_ns - elapsed_ns; + if (sleep_ns > 0) { + struct timespec ts = { sleep_ns / 1000000000L, sleep_ns % 1000000000L }; + while (nanosleep(&ts, &ts) == -1 && errno == EINTR); + } +} diff --git a/webcam.c b/webcam.c deleted file mode 100644 index 2a9258f..0000000 --- a/webcam.c +++ /dev/null @@ -1,286 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Default configuration values -#define DEFAULT_ASCII_WIDTH 80 -#define DEFAULT_ASCII_HEIGHT 40 -#define DEFAULT_CAPTURE_WIDTH 160 -#define DEFAULT_CAPTURE_HEIGHT 120 -#define DEFAULT_FPS 20 - -#define ASCII_CHARS " .:-=+*#%@" -#define CLEAR_SCREEN "\033[2J\033[H" - -volatile sig_atomic_t keep_running = 1; - -void handle_signal(int sig) { keep_running = 0; } - -void print_usage(char *prog_name) { - fprintf(stderr, - "Usage: %s [-d ] [-W ] [-H ] [-f ]\n", - prog_name); - fprintf(stderr, " -d : Video device path (default: /dev/video0)\n"); - fprintf(stderr, " -W : ASCII output width (default: %d)\n", - DEFAULT_ASCII_WIDTH); - fprintf(stderr, " -H : ASCII output height (default: %d)\n", - DEFAULT_ASCII_HEIGHT); - fprintf(stderr, " -f : Target framerate (default: %d)\n", - DEFAULT_FPS); -} - -int main(int argc, char *argv[]) { - signal(SIGINT, handle_signal); - - // Configuration Variables - char *device_path = "/dev/video0"; - int ascii_width = DEFAULT_ASCII_WIDTH; - int ascii_height = DEFAULT_ASCII_HEIGHT; - int target_fps = DEFAULT_FPS; - long frame_duration_ns = 1000000000L / target_fps; - int capture_width = DEFAULT_CAPTURE_WIDTH; - int capture_height = DEFAULT_CAPTURE_HEIGHT; - - // Resource Tracking for Cleanup - int fd = -1; - void *buffer = MAP_FAILED; - unsigned char *gray_buffer = NULL; - struct v4l2_buffer buf = {0}; - enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - - // Command-Line Argument Parsing - int c; - while ((c = getopt(argc, argv, "d:W:H:f:")) != -1) { - switch (c) { - case 'd': - device_path = optarg; - break; - case 'W': - ascii_width = atoi(optarg); - if (ascii_width <= 0) - ascii_width = DEFAULT_ASCII_WIDTH; - break; - case 'H': - ascii_height = atoi(optarg); - if (ascii_height <= 0) - ascii_height = DEFAULT_ASCII_HEIGHT; - break; - case 'f': - target_fps = atoi(optarg); - if (target_fps <= 0) - target_fps = DEFAULT_FPS; - frame_duration_ns = 1000000000L / target_fps; - break; - default: - print_usage(argv[0]); - return 1; - } - } - - // V4L2 Setup - fd = open(device_path, O_RDWR | O_NONBLOCK); - if (fd < 0) { - perror("Error opening video device"); - goto cleanup; - } - printf("Webcam opened successfully: %s\n", device_path); - - // Check capabilities (omitted for brevity, assume capture capability) - - // Set format - struct v4l2_format fmt = {0}; - fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - fmt.fmt.pix.width = capture_width; - fmt.fmt.pix.height = capture_height; - fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; - fmt.fmt.pix.field = V4L2_FIELD_NONE; - - if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { - perror("Setting format failed"); - goto cleanup; - } - - // Format Negotiation (Read back the actual accepted size) - capture_width = fmt.fmt.pix.width; - capture_height = fmt.fmt.pix.height; - printf("Capture resolution set to: %dx%d\n", capture_width, capture_height); - - // Request buffers - struct v4l2_requestbuffers req = {0}; - req.count = 1; - req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - req.memory = V4L2_MEMORY_MMAP; - - if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) { - perror("Requesting buffers failed"); - goto cleanup; - } - - // Map the buffer - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_MMAP; - buf.index = 0; - - if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) { - perror("Querying buffer failed"); - goto cleanup; - } - - buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, - buf.m.offset); - if (buffer == MAP_FAILED) { - perror("Memory mapping failed"); - goto cleanup; - } - - // Queue the buffer - if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { - perror("Queue buffer failed"); - goto cleanup; - } - - // Start streaming - if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) { - perror("Start streaming failed"); - goto cleanup; - } - - printf("Starting ASCII webcam stream... Press Ctrl+C to stop\n"); - - // Create buffer for grayscale conversion - gray_buffer = malloc(capture_width * capture_height); - if (gray_buffer == NULL) { - perror("Memory allocation failed for gray_buffer"); - goto cleanup; - } - - // Main Streaming Loop - struct timespec start_time, end_time; - - while (keep_running) { - clock_gettime(CLOCK_MONOTONIC, &start_time); - - fd_set fds; - struct timeval tv = {0}; - - FD_ZERO(&fds); - FD_SET(fd, &fds); - tv.tv_sec = 1; // Shorter select timeout - - int r = select(fd + 1, &fds, NULL, NULL, &tv); - if (r < 0) { - if (errno == EINTR) - continue; // Handle interrupted select - perror("Select failed"); - break; - } - - if (r == 0) { - fprintf(stderr, "Select timeout on frame availability\n"); - continue; - } - - // Dequeue buffer - if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) { - perror("Dequeue buffer failed"); - break; - } - - // Convert YUYV to grayscale (Y component only) - uint8_t *yuyv_data = (uint8_t *)buffer; - for (int i = 0, j = 0; i < capture_width * capture_height * 2; - i += 2, j++) { - gray_buffer[j] = yuyv_data[i]; - } - - // Clear screen and move cursor to top-left - printf(CLEAR_SCREEN); - - // Averaging Resampling and ASCII Conversion - for (int y = 0; y < ascii_height; y++) { - for (int x = 0; x < ascii_width; x++) { - double block_width = (double)capture_width / ascii_width; - double block_height = (double)capture_height / ascii_height; - long long total_brightness = 0; - int pixel_count = 0; - - // Iterate over the source block that corresponds to one ASCII character - for (int src_y = (int)(y * block_height); - src_y < (int)((y + 1) * block_height); src_y++) { - for (int src_x = (int)(x * block_width); - src_x < (int)((x + 1) * block_width); src_x++) { - - // Bounds check - if (src_x < capture_width && src_y < capture_height) { - int idx = src_y * capture_width + src_x; - total_brightness += gray_buffer[idx]; - pixel_count++; - } - } - } - - unsigned char avg_pixel = 0; - if (pixel_count > 0) { - avg_pixel = total_brightness / pixel_count; - } - - // Map brightness (0-255) to ASCII character set - int ascii_len = strlen(ASCII_CHARS); - int ascii_idx = avg_pixel * (ascii_len - 1) / 255; - putchar(ASCII_CHARS[ascii_idx]); - } - putchar('\n'); - } - fflush(stdout); - - // Re-queue buffer - if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { - perror("Re-queue buffer failed"); - break; - } - - // Precise Frame Rate Control - clock_gettime(CLOCK_MONOTONIC, &end_time); - long elapsed_ns = (end_time.tv_sec - start_time.tv_sec) * 1000000000L + - (end_time.tv_nsec - start_time.tv_nsec); - long sleep_ns = frame_duration_ns - elapsed_ns; - - if (sleep_ns > 0) { - struct timespec ts = {0, 0}; - ts.tv_sec = sleep_ns / 1000000000L; - ts.tv_nsec = sleep_ns % 1000000000L; - // Use nanosleep with loop to handle interruptions - while (nanosleep(&ts, &ts) == -1 && errno == EINTR) - ; - } - } - - printf("\nStopping...\n"); - // Stop streaming - ioctl(fd, VIDIOC_STREAMOFF, &type); - -// Centralized Cleanup -cleanup: - if (gray_buffer != NULL) { - free(gray_buffer); - } - if (buffer != MAP_FAILED) { - // buf.length is still valid from the initial VIDIOC_QUERYBUF call - munmap(buffer, buf.length); - } - if (fd >= 0) { - close(fd); - } - - return (fd < 0 || buffer == MAP_FAILED || gray_buffer == NULL) ? 1 : 0; -} diff --git a/webcam_ascii b/webcam_ascii new file mode 100755 index 0000000..ca56ee7 Binary files /dev/null and b/webcam_ascii differ