diff --git a/crt_core.c b/crt_core.c new file mode 100644 index 0000000..3226631 --- /dev/null +++ b/crt_core.c @@ -0,0 +1,419 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ +#include "crt_core.h" + +#include +#include + +/* ensure negative values for x get properly modulo'd */ +#define POSMOD(x, n) (((x) % (n) + (n)) % (n)) + +static int sigpsin15[18] = { /* significant points on sine wave (15-bit) */ + 0x0000, + 0x0c88,0x18f8,0x2528,0x30f8,0x3c50,0x4718,0x5130,0x5a80, + 0x62f0,0x6a68,0x70e0,0x7640,0x7a78,0x7d88,0x7f60,0x8000, + 0x7f60 +}; + +static int +sintabil8(int n) +{ + int f, i, a, b; + + /* looks scary but if you don't change T14_2PI + * it won't cause out of bounds memory reads + */ + f = n >> 0 & 0xff; + i = n >> 8 & 0xff; + a = sigpsin15[i]; + b = sigpsin15[i + 1]; + return (a + ((b - a) * f >> 8)); +} + +/* 14-bit interpolated sine/cosine */ +extern void +crt_sincos14(int *s, int *c, int n) +{ + int h; + + n &= T14_MASK; + h = n & ((T14_2PI >> 1) - 1); + + if (h > ((T14_2PI >> 2) - 1)) { + *c = -sintabil8(h - (T14_2PI >> 2)); + *s = sintabil8((T14_2PI >> 1) - h); + } else { + *c = sintabil8((T14_2PI >> 2) - h); + *s = sintabil8(h); + } + if (n > ((T14_2PI >> 1) - 1)) { + *c = -*c; + *s = -*s; + } +} + +/*****************************************************************************/ +/********************************* FILTERS ***********************************/ +/*****************************************************************************/ + +#define HISTLEN 3 +#define HISTOLD (HISTLEN - 1) /* oldest entry */ +#define HISTNEW 0 /* newest entry */ + +#define EQ_P 16 /* if changed, the gains will need to be adjusted */ +#define EQ_R (1 << (EQ_P - 1)) /* rounding */ +/* three band equalizer */ +static struct EQF { + int lf, hf; /* fractions */ + int g[3]; /* gains */ + int fL[4]; + int fH[4]; + int h[HISTLEN]; /* history */ +} eqY, eqI, eqQ; + +/* f_lo - low cutoff frequency + * f_hi - high cutoff frequency + * rate - sampling rate + * g_lo, g_mid, g_hi - gains + */ +static void +init_eq(struct EQF *f, + int f_lo, int f_hi, int rate, + int g_lo, int g_mid, int g_hi) +{ + int sn, cs; + + memset(f, 0, sizeof(struct EQF)); + + f->g[0] = g_lo; + f->g[1] = g_mid; + f->g[2] = g_hi; + + crt_sincos14(&sn, &cs, T14_PI * f_lo / rate); + if (EQ_P >= 15) { + f->lf = 2 * (sn << (EQ_P - 15)); + } else { + f->lf = 2 * (sn >> (15 - EQ_P)); + } + crt_sincos14(&sn, &cs, T14_PI * f_hi / rate); + if (EQ_P >= 15) { + f->hf = 2 * (sn << (EQ_P - 15)); + } else { + f->hf = 2 * (sn >> (15 - EQ_P)); + } +} + +static void +reset_eq(struct EQF *f) +{ + memset(f->fL, 0, sizeof(f->fL)); + memset(f->fH, 0, sizeof(f->fH)); + memset(f->h, 0, sizeof(f->h)); +} + +static int +eqf(struct EQF *f, int s) +{ + int i, r[3]; + + f->fL[0] += (f->lf * (s - f->fL[0]) + EQ_R) >> EQ_P; + f->fH[0] += (f->hf * (s - f->fH[0]) + EQ_R) >> EQ_P; + + for (i = 1; i < 4; i++) { + f->fL[i] += (f->lf * (f->fL[i - 1] - f->fL[i]) + EQ_R) >> EQ_P; + f->fH[i] += (f->hf * (f->fH[i - 1] - f->fH[i]) + EQ_R) >> EQ_P; + } + + r[0] = f->fL[3]; + r[1] = f->fH[3] - f->fL[3]; + r[2] = f->h[HISTOLD] - f->fH[3]; + + for (i = 0; i < 3; i++) { + r[i] = (r[i] * f->g[i]) >> EQ_P; + } + + for (i = HISTOLD; i > 0; i--) { + f->h[i] = f->h[i - 1]; + } + f->h[HISTNEW] = s; + + return (r[0] + r[1] + r[2]); +} + +/*****************************************************************************/ +/***************************** PUBLIC FUNCTIONS ******************************/ +/*****************************************************************************/ + +extern void +crt_resize(struct CRT *v, int w, int h, int *out) +{ + v->outw = w; + v->outh = h; + v->out = out; +} + +extern void +crt_reset(struct CRT *v) +{ + v->hue = 0; + v->saturation = 10; + v->brightness = 0; + v->contrast = 180; + v->black_point = 0; + v->white_point = 100; + v->hsync = 0; + v->vsync = 0; +} + +extern void +crt_init(struct CRT *v, int w, int h, int *out) +{ + memset(v, 0, sizeof(struct CRT)); + crt_resize(v, w, h, out); + crt_reset(v); + v->rn = 194; + + /* kilohertz to line sample conversion */ +#define kHz2L(kHz) (CRT_HRES * (kHz * 100) / L_FREQ) + + /* band gains are pre-scaled as 16-bit fixed point + * if you change the EQ_P define, you'll need to update these gains too + */ + init_eq(&eqY, kHz2L(1500), kHz2L(3000), CRT_HRES, 65536, 8192, 9175); + init_eq(&eqI, kHz2L(80), kHz2L(1150), CRT_HRES, 65536, 65536, 1311); + init_eq(&eqQ, kHz2L(80), kHz2L(1000), CRT_HRES, 65536, 65536, 0); +} + +/* search windows, in samples */ +#define HSYNC_WINDOW 6 +#define VSYNC_WINDOW 6 + +extern void +crt_demodulate(struct CRT *v, int noise) +{ + struct { + int y, i, q; + } out[AV_LEN + 1], *yiqA, *yiqB; + int i, j, line, rn; + signed char *sig; + int s = 0; + int field, ratio; + int *ccr; /* color carrier signal */ + int huesn, huecs; + int xnudge = -3, ynudge = 3; + int bright = v->brightness - (BLACK_LEVEL + v->black_point); +#if CRT_DO_BLOOM + int prev_e; /* filtered beam energy per scan line */ + int max_e; /* approx maximum energy in a scan line */ +#endif + + crt_sincos14(&huesn, &huecs, ((v->hue % 360) + 33) * 8192 / 180); + huesn >>= 11; /* make 4-bit */ + huecs >>= 11; + + rn = v->rn; + for (i = 0; i < CRT_INPUT_SIZE; i++) { + rn = (214019 * rn + 140327895); + + /* signal + noise */ + s = v->analog[i] + (((((rn >> 16) & 0xff) - 0x7f) * noise) >> 8); + if (s > 127) { s = 127; } + if (s < -127) { s = -127; } + v->inp[i] = s; + } + v->rn = rn; + + /* Look for vertical sync. + * + * This is done by integrating the signal and + * seeing if it exceeds a threshold. The threshold of + * the vertical sync pulse is much higher because the + * vsync pulse is a lot longer than the hsync pulse. + * The signal needs to be integrated to lessen + * the noise in the signal. + */ + for (i = -VSYNC_WINDOW; i < VSYNC_WINDOW; i++) { + line = POSMOD(v->vsync + i, CRT_VRES); + sig = v->inp + line * CRT_HRES; + s = 0; + for (j = 0; j < CRT_HRES; j++) { + s += sig[j]; + /* increase the multiplier to make the vsync + * more stable when there is a lot of noise + */ + if (s <= (94 * SYNC_LEVEL)) { + goto vsync_found; + } + } + } +vsync_found: +#if CRT_DO_VSYNC + v->vsync = line; /* vsync found (or gave up) at this line */ +#else + v->vsync = -3; +#endif + /* if vsync signal was in second half of line, odd field */ + field = (j > (CRT_HRES / 2)); +#if CRT_DO_BLOOM + max_e = (128 + (noise / 2)) * AV_LEN; + prev_e = (16384 / 8); +#endif + /* ratio of output height to active video lines in the signal */ + ratio = (v->outh << 16) / CRT_LINES; + ratio = (ratio + 32768) >> 16; + + field = (field * (ratio / 2)); + + for (line = CRT_TOP; line < CRT_BOT; line++) { + unsigned pos, ln; + int scanL, scanR, dx; + int L, R; + int *cL, *cR; + int wave[4]; + int dci, dcq; /* decoded I, Q */ + int xpos, ypos; + int beg, end; + int phasealign; +#if CRT_DO_BLOOM + int line_w; +#endif + + beg = (line - CRT_TOP + 0) * v->outh / CRT_LINES + field; + end = (line - CRT_TOP + 1) * v->outh / CRT_LINES + field; + + if (beg >= v->outh) { continue; } + if (end > v->outh) { end = v->outh; } + + /* Look for horizontal sync. + * See comment above regarding vertical sync. + */ + ln = (POSMOD(line + v->vsync, CRT_VRES)) * CRT_HRES; + sig = v->inp + ln + v->hsync; + s = 0; + for (i = -HSYNC_WINDOW; i < HSYNC_WINDOW; i++) { + s += sig[SYNC_BEG + i]; + if (s <= (4 * SYNC_LEVEL)) { + break; + } + } +#if CRT_DO_HSYNC + v->hsync = POSMOD(i + v->hsync, CRT_HRES); +#else + v->hsync = 0; +#endif + + xpos = POSMOD(AV_BEG + v->hsync + xnudge, CRT_HRES); + ypos = POSMOD(line + v->vsync + ynudge, CRT_VRES); + pos = xpos + ypos * CRT_HRES; + + ccr = v->ccf[ypos & 3]; + sig = v->inp + ln + (v->hsync & ~3); /* burst @ 1/CB_FREQ sample rate */ + for (i = CB_BEG; i < CB_BEG + (CB_CYCLES * CRT_CB_FREQ); i++) { + int p, n; + p = ccr[i & 3] * 127 / 128; /* fraction of the previous */ + n = sig[i]; /* mixed with the new sample */ + ccr[i & 3] = p + n; + } + + phasealign = POSMOD(v->hsync, 4); + + /* amplitude of carrier = saturation, phase difference = hue */ + dci = ccr[(phasealign + 1) & 3] - ccr[(phasealign + 3) & 3]; + dcq = ccr[(phasealign + 2) & 3] - ccr[(phasealign + 0) & 3]; + + /* rotate them by the hue adjustment angle */ + wave[0] = ((dci * huecs - dcq * huesn) >> 4) * v->saturation; + wave[1] = ((dcq * huecs + dci * huesn) >> 4) * v->saturation; + wave[2] = -wave[0]; + wave[3] = -wave[1]; + + sig = v->inp + pos; +#if CRT_DO_BLOOM + s = 0; + for (i = 0; i < AV_LEN; i++) { + s += sig[i]; /* sum up the scan line */ + } + /* bloom emulation */ + prev_e = (prev_e * 123 / 128) + ((((max_e >> 1) - s) << 10) / max_e); + line_w = (AV_LEN * 112 / 128) + (prev_e >> 9); + + dx = (line_w << 12) / v->outw; + scanL = ((AV_LEN / 2) - (line_w >> 1) + 8) << 12; + scanR = (AV_LEN - 1) << 12; + + L = (scanL >> 12); + R = (scanR >> 12); +#else + dx = ((AV_LEN - 1) << 12) / v->outw; + scanL = 0; + scanR = (AV_LEN - 1) << 12; + L = 0; + R = AV_LEN; +#endif + reset_eq(&eqY); + reset_eq(&eqI); + reset_eq(&eqQ); + + for (i = L; i < R; i++) { + out[i].y = eqf(&eqY, sig[i] + bright) << 4; + out[i].i = eqf(&eqI, sig[i] * wave[(i + 0) & 3] >> 9) >> 3; + out[i].q = eqf(&eqQ, sig[i] * wave[(i + 3) & 3] >> 9) >> 3; + } + + cL = v->out + beg * v->outw; + cR = cL + v->outw; + + for (pos = scanL; pos < scanR && cL < cR; pos += dx) { + int y, i, q; + int r, g, b; + int aa, bb; + + R = pos & 0xfff; + L = 0xfff - R; + s = pos >> 12; + + yiqA = out + s; + yiqB = out + s + 1; + + /* interpolate between samples if needed */ + y = ((yiqA->y * L) >> 2) + ((yiqB->y * R) >> 2); + i = ((yiqA->i * L) >> 14) + ((yiqB->i * R) >> 14); + q = ((yiqA->q * L) >> 14) + ((yiqB->q * R) >> 14); + + /* YIQ to RGB */ + r = (((y + 3879 * i + 2556 * q) >> 12) * v->contrast) >> 8; + g = (((y - 1126 * i - 2605 * q) >> 12) * v->contrast) >> 8; + b = (((y - 4530 * i + 7021 * q) >> 12) * v->contrast) >> 8; + + if (r < 0) r = 0; + if (g < 0) g = 0; + if (b < 0) b = 0; + if (r > 255) r = 255; + if (g > 255) g = 255; + if (b > 255) b = 255; + + if (v->blend) { + aa = (r << 16 | g << 8 | b); + bb = *cL; + /* blend with previous color there */ + *cL++ = (((aa & 0xfefeff) >> 1) + ((bb & 0xfefeff) >> 1)); + } else { + *cL++ = (r << 16 | g << 8 | b); + } + } + + /* duplicate extra lines */ + ln = v->outw * sizeof(int); + for (s = beg + 1; s < (end - v->scanlines); s++) { + memcpy(v->out + s * v->outw, v->out + (s - 1) * v->outw, ln); + } + } +} diff --git a/crt_core.h b/crt_core.h new file mode 100644 index 0000000..816d55c --- /dev/null +++ b/crt_core.h @@ -0,0 +1,101 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ +#ifndef _CRT_CORE_H_ +#define _CRT_CORE_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* crt_core.h + * + * The demodulator. This is also where you can define which system to emulate. + * + */ +#define CRT_SYSTEM_NTSC 0 /* standard NTSC */ +#define CRT_SYSTEM_NES 1 /* decode 6 or 9-bit NES pixels */ + +/* the system to be compiled */ +#define CRT_SYSTEM CRT_SYSTEM_NTSC + +#if (CRT_SYSTEM == CRT_SYSTEM_NES) +#include "crt_nes.h" +#elif (CRT_SYSTEM == CRT_SYSTEM_NTSC) +#include "crt_ntsc.h" +#else +#error No system defined +#endif + +/* do bloom emulation (side effect: makes screen have black borders) */ +#define CRT_DO_BLOOM 0 /* does not work for NES */ +#define CRT_DO_VSYNC 1 /* look for VSYNC */ +#define CRT_DO_HSYNC 1 /* look for HSYNC */ + +struct CRT { + signed char analog[CRT_INPUT_SIZE]; + signed char inp[CRT_INPUT_SIZE]; /* CRT input, can be noisy */ + + int outw, outh; /* output width/height */ + int *out; /* output image */ + + int hue, brightness, contrast, saturation; /* common monitor settings */ + int black_point, white_point; /* user-adjustable */ + int scanlines; /* leave gaps between lines if necessary */ + int blend; /* blend new field onto previous image */ + + /* internal data */ + int ccf[4][4]; /* faster color carrier convergence */ + int hsync, vsync; /* keep track of sync over frames */ + int rn; /* seed for the 'random' noise */ +}; + +/* Initializes the library. Sets up filters. + * w - width of the output image + * h - height of the output image + * out - pointer to output image data 32-bit RGB packed as 0xXXRRGGBB + */ +extern void crt_init(struct CRT *v, int w, int h, int *out); + +/* Updates the output image parameters + * w - width of the output image + * h - height of the output image + * out - pointer to output image data 32-bit RGB packed as 0xXXRRGGBB + */ +extern void crt_resize(struct CRT *v, int w, int h, int *out); + +/* Resets the CRT settings back to their defaults */ +extern void crt_reset(struct CRT *v); + +/* Modulates RGB image into an analog NTSC signal + * s - struct containing settings to apply to this field + */ +extern void crt_modulate(struct CRT *v, struct NTSC_SETTINGS *s); + +/* Demodulates the NTSC signal generated by crt_modulate() + * noise - the amount of noise added to the signal (0 - inf) + */ +extern void crt_demodulate(struct CRT *v, int noise); + +/*****************************************************************************/ +/*************************** FIXED POINT SIN/COS *****************************/ +/*****************************************************************************/ + +#define T14_2PI 16384 +#define T14_MASK (T14_2PI - 1) +#define T14_PI (T14_2PI / 2) + +extern void crt_sincos14(int *s, int *c, int n); + +#ifdef __cplusplus +} +#endif + +#endif /* _CRT_SINCOS_H_ */ diff --git a/crt_main.c b/crt_main.c new file mode 100644 index 0000000..0800260 --- /dev/null +++ b/crt_main.c @@ -0,0 +1,521 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ + +#include +#include +#include +#include +#include "ppm_rw.h" +#include "bmp_rw.h" +#include "crt_core.h" + +#define CMD_LINE_VERSION 1 +/* there is no NES command line version */ +#if ((CRT_SYSTEM == CRT_SYSTEM_NES) && CMD_LINE_VERSION) +#error NES mode does not have a command line version +#endif +static int +cmpsuf(char *s, char *suf, int nc) +{ + return strcmp(s + strlen(s) - nc, suf); +} + +#if CMD_LINE_VERSION + +#define DRV_HEADER "NTSC/CRT by EMMIR 2018-2023\n" + +static int dooverwrite = 1; +static int docolor = 1; +static int field = 0; +static int progressive = 0; +static int raw = 0; +static int hue = 0; + +static int +stoint(char *s, int *err) +{ + char *tail; + long val; + + errno = 0; + *err = 0; + val = strtol(s, &tail, 10); + if (errno == ERANGE) { + printf("integer out of integer range\n"); + *err = 1; + } else if (errno != 0) { + printf("bad string: %s\n", strerror(errno)); + *err = 1; + } else if (*tail != '\0') { + printf("integer contained non-numeric characters\n"); + *err = 1; + } + return val; +} + +static void +usage(char *p) +{ + printf(DRV_HEADER); + printf("usage: %s -m|o|f|p|r|h outwidth outheight noise artifact_hue infile outfile\n", p); + printf("sample usage: %s -op 640 480 24 0 in.ppm out.ppm\n", p); + printf("sample usage: %s - 832 624 0 90 in.ppm out.ppm\n", p); + printf("-- NOTE: the - after the program name is required\n"); + printf("\tartifact_hue is [0, 359]\n"); + printf("------------------------------------------------------------\n"); + printf("\tm : monochrome\n"); + printf("\to : do not prompt when overwriting files\n"); + printf("\tf : odd field (only meaningful in progressive mode)\n"); + printf("\tp : progressive scan (rather than interlaced)\n"); + printf("\tr : raw image (needed for images that use artifact colors)\n"); + printf("\th : print help\n"); + printf("\n"); + printf("by default, the image will be full color, interlaced, and scaled to the output dimensions\n"); +} + +static int +process_args(int argc, char **argv) +{ + char *flags; + + flags = argv[1]; + if (*flags == '-') { + flags++; + } + for (; *flags != '\0'; flags++) { + switch (*flags) { + case 'm': docolor = 0; break; + case 'o': dooverwrite = 0; break; + case 'f': field = 1; break; + case 'p': progressive = 1; break; + case 'r': raw = 1; break; + case 'h': usage(argv[0]); return 0; + default: + fprintf(stderr, "Unrecognized flag '%c'\n", *flags); + return 0; + } + } + return 1; +} + +static int +fileexist(char *n) +{ + FILE *fp = fopen(n, "r"); + if (fp) { + fclose(fp); + return 1; + } + return 0; +} + +static int +promptoverwrite(char *fn) +{ + if (dooverwrite && fileexist(fn)) { + do { + char c = 0; + printf("\n--- file (%s) already exists, overwrite? (y/n)\n", fn); + scanf(" %c", &c); + if (c == 'y' || c == 'Y') { + return 1; + } + if (c == 'n' || c == 'N') { + return 0; + } + } while (1); + } + return 1; +} + +int +main(int argc, char **argv) +{ + struct NTSC_SETTINGS ntsc; + struct CRT crt; + int *img; + int imgw, imgh; + int *output = NULL; + int outw = 832; + int outh = 624; + int noise = 24; + char *input_file; + char *output_file; + int err = 0; + + if (argc < 8) { + usage(argv[0]); + return EXIT_FAILURE; + } + + if (!process_args(argc, argv)) { + return EXIT_FAILURE; + } + + printf(DRV_HEADER); + + outw = stoint(argv[2], &err); + if (err) { + return EXIT_FAILURE; + } + + outh = stoint(argv[3], &err); + if (err) { + return EXIT_FAILURE; + } + + noise = stoint(argv[4], &err); + if (err) { + return EXIT_FAILURE; + } + + if (noise < 0) noise = 0; + + hue = stoint(argv[5], &err); + if (err) { + return EXIT_FAILURE; + } + hue %= 360; + + output = calloc(outw * outh, sizeof(int)); + if (output == NULL) { + printf("out of memory\n"); + return EXIT_FAILURE; + } + + input_file = argv[6]; + output_file = argv[7]; + + if (cmpsuf(input_file, ".ppm", 4) == 0) { + if (!ppm_read24(input_file, &img, &imgw, &imgh, calloc)) { + printf("unable to read image\n"); + return EXIT_FAILURE; + } + } else { + if (!bmp_read24(input_file, &img, &imgw, &imgh, calloc)) { + printf("unable to read image\n"); + return EXIT_FAILURE; + } + } + printf("loaded %d %d\n", imgw, imgh); + + if (!promptoverwrite(output_file)) { + return EXIT_FAILURE; + } + + crt_init(&crt, outw, outh, output); + + ntsc.rgb = img; + ntsc.w = imgw; + ntsc.h = imgh; + ntsc.as_color = docolor; + ntsc.field = field & 1; + ntsc.raw = raw; + ntsc.hue = hue; + ntsc.frame = 0; + + printf("converting to %dx%d...\n", outw, outh); + err = 0; + + /* accumulate 4 frames */ + while (err < 4) { + crt_modulate(&crt, &ntsc); + crt_demodulate(&crt, noise); + if (!progressive) { + ntsc.field ^= 1; + crt_modulate(&crt, &ntsc); + crt_demodulate(&crt, noise); + if ((err & 1) == 0) { + /* a frame is two fields */ + ntsc.frame++; + ntsc.frame &= 1; + } + } + err++; + } + + if (cmpsuf(output_file, ".ppm", 4) == 0) { + if (!ppm_write24(output_file, output, outw, outh)) { + printf("unable to write image\n"); + return EXIT_FAILURE; + } + } else { + if (!bmp_write24(output_file, output, outw, outh)) { + printf("unable to write image\n"); + return EXIT_FAILURE; + } + } + printf("done\n"); + return EXIT_SUCCESS; +} +#else +#include "fw.h" +#if 0 +#define XMAX 624 +#define YMAX 832 +#else +#define XMAX 832 +#define YMAX 624 +#endif +static int *video = NULL; +static VIDINFO *info; + +static struct CRT crt; + +static int *img; +static int imgw; +static int imgh; + +static int color = 1; +static int noise = 12; +static int field = 0; +static int progressive = 0; +static int raw = 0; +static int fno = 0; /* frame number */ +static int hue = 0; +static int fadephos = 1; /* fade phosphors each frame */ + +static void +updatecb(void) +{ + if (pkb_key_pressed(FW_KEY_ESCAPE)) { + sys_shutdown(); + } + + if (pkb_key_held('q')) { + crt.black_point += 1; + printf("crt.black_point %d\n", crt.black_point); + } + if (pkb_key_held('a')) { + crt.black_point -= 1; + printf("crt.black_point %d\n", crt.black_point); + } + + if (pkb_key_held('w')) { + crt.white_point += 1; + printf("crt.white_point %d\n", crt.white_point); + } + if (pkb_key_held('s')) { + crt.white_point -= 1; + printf("crt.white_point %d\n", crt.white_point); + } + + if (pkb_key_held(FW_KEY_ARROW_UP)) { + crt.brightness += 1; + printf("%d\n", crt.brightness); + } + if (pkb_key_held(FW_KEY_ARROW_DOWN)) { + crt.brightness -= 1; + printf("%d\n", crt.brightness); + } + if (pkb_key_held(FW_KEY_ARROW_LEFT)) { + crt.contrast -= 1; + printf("%d\n", crt.contrast); + } + if (pkb_key_held(FW_KEY_ARROW_RIGHT)) { + crt.contrast += 1; + printf("%d\n", crt.contrast); + } + if (pkb_key_held('1')) { + crt.saturation -= 1; + printf("%d\n", crt.saturation); + } + if (pkb_key_held('2')) { + crt.saturation += 1; + printf("%d\n", crt.saturation); + } + if (pkb_key_held('3')) { + noise -= 1; + if (noise < 0) { + noise = 0; + } + printf("%d\n", noise); + } + if (pkb_key_held('4')) { + noise += 1; + printf("%d\n", noise); + } + if (pkb_key_held('5')) { + hue--; + if (hue < 0) { + hue = 359; + } + printf("%d\n", hue); + } + if (pkb_key_held('6')) { + hue++; + if (hue > 359) { + hue = 0; + } + printf("%d\n", hue); + } + if (pkb_key_held('7')) { + crt.hue -= 1; + printf("%d\n", crt.hue); + } + if (pkb_key_held('8')) { + crt.hue += 1; + printf("%d\n", crt.hue); + } + + if (pkb_key_pressed(FW_KEY_SPACE)) { + color ^= 1; + } + + if (pkb_key_pressed('m')) { + fadephos ^= 1; + printf("fadephos: %d\n", fadephos); + } + if (pkb_key_pressed('r')) { + crt_reset(&crt); + } + if (pkb_key_pressed('g')) { + crt.scanlines ^= 1; + printf("crt.scanlines: %d\n", crt.scanlines); + } + if (pkb_key_pressed('b')) { + crt.blend ^= 1; + printf("crt.blend: %d\n", crt.blend); + } + if (pkb_key_pressed('f')) { + field ^= 1; + printf("field: %d\n", field); + } + if (pkb_key_pressed('e')) { + progressive ^= 1; + printf("progressive: %d\n", progressive); + } + if (pkb_key_pressed('t')) { + /* Analog array must be cleared since it normally doesn't get zeroed each frame + * so active video portions that were written to in non-raw mode will not lose + * their values resulting in the previous image being + * displayed where the new, smaller image is not + */ + memset(crt.analog, 0, sizeof(crt.analog)); + raw ^= 1; + printf("raw: %d\n", raw); + } +} + +static void +fade_phosphors(void) +{ + int i, *v; + unsigned int c; + + v = video; + + for (i = 0; i < info->width * info->height; i++) { + c = v[i] & 0xffffff; + v[i] = (c >> 1 & 0x7f7f7f) + + (c >> 2 & 0x3f3f3f) + + (c >> 3 & 0x1f1f1f) + + (c >> 4 & 0x0f0f0f); + } +} + +static void +displaycb(void) +{ + static struct NTSC_SETTINGS ntsc; + + if (fadephos) { + fade_phosphors(); + } else { + memset(video, 0, info->width * info->height * sizeof(int)); + } + /* not necessary to clear if you're rendering on a constant region of the display */ + /* memset(crt.analog, 0, sizeof(crt.analog)); */ +#if (CRT_SYSTEM == CRT_SYSTEM_NES) + ntsc.data = ppu_output_256x240; + ntsc.border_color = 0x22; + ntsc.w = 256; + ntsc.h = 240; + ntsc.dot_crawl_offset = fno++ % 3; + ntsc.hue = hue; +#else + ntsc.rgb = img; + ntsc.w = imgw; + ntsc.h = imgh; + ntsc.as_color = color; + ntsc.field = field & 1; + ntsc.raw = raw; + ntsc.hue = hue; + if (field == 0) { + /* a frame is two fields */ + fno++; + fno &= 1; + } +#endif + crt_modulate(&crt, &ntsc); + crt_demodulate(&crt, noise); + if (!progressive) { + field ^= 1; + } + vid_blit(); + vid_sync(); +} + +int +main(int argc, char **argv) +{ + int werr; + char *input_file; + + sys_init(); + sys_updatefunc(updatecb); + sys_displayfunc(displaycb); + sys_keybfunc(pkb_keyboard); + sys_keybupfunc(pkb_keyboardup); + + clk_mode(FW_CLK_MODE_HIRES); + pkb_reset(); + sys_sethz(60); + sys_capfps(1); + + werr = vid_open("crt", XMAX, YMAX, 1, FW_VFLAG_VIDFAST); + if (werr != FW_VERR_OK) { + FW_error("unable to create window\n"); + return EXIT_FAILURE; + } + + info = vid_getinfo(); + video = info->video; + + crt_init(&crt, info->width, info->height, video); + + if (argc == 1) { + fprintf(stderr, "Please specify PPM or BMP image input file.\n"); + return EXIT_FAILURE; + } + input_file = argv[1]; + + if (cmpsuf(input_file, ".ppm", 4) == 0) { + if (!ppm_read24(input_file, &img, &imgw, &imgh, calloc)) { + fprintf(stderr, "unable to read image\n"); + return EXIT_FAILURE; + } + } else { + if (!bmp_read24(input_file, &img, &imgw, &imgh, calloc)) { + fprintf(stderr, "unable to read image\n"); + return EXIT_FAILURE; + } + } + + printf("loaded %d %d\n", imgw, imgh); + + sys_start(); + + sys_shutdown(); + return EXIT_SUCCESS; +} + +#endif diff --git a/crt_nes.c b/crt_nes.c new file mode 100644 index 0000000..eda4ac1 --- /dev/null +++ b/crt_nes.c @@ -0,0 +1,174 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * modifications for Mesen by Persune + * https://github.com/LMP88959/NTSC-CRT + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ + +#include "crt_core.h" + +#if (CRT_SYSTEM == CRT_SYSTEM_NES) +#include +#include + +/* generate the square wave for a given 9-bit pixel and phase */ +static int +square_sample(int p, int phase) +{ + static int active[6] = { + 0300, 0100, + 0500, 0400, + 0600, 0200 + }; + int bri, hue, v; + + hue = (p & 0x0f); + + /* last two columns are black */ + if (hue >= 0x0e) { + return 0; + } + + bri = ((p & 0x30) >> 4) * 300; + + switch (hue) { + case 0: + v = bri + 410; + break; + case 0x0d: + v = bri - 300; + break; + default: + v = (((hue + phase) % 12) < 6) ? (bri + 410) : (bri - 300); + break; + } + + if (v > 1024) { + v = 1024; + } + /* red 0100, green 0200, blue 0400 */ + if ((p & 0700) & active[(phase >> 1) % 6]) { + return (v >> 1) + (v >> 2); + } + + return v; +} + +extern void +crt_modulate(struct CRT *v, struct NTSC_SETTINGS *s) +{ + int x, y, xo, yo; + int destw = AV_LEN; + int desth = CRT_LINES; + int n, phase; + int po, lo; + int iccf[4]; + int ccburst[4]; /* color phase for burst */ + int sn, cs; + + for (x = 0; x < 4; x++) { + n = s->hue + x * 90; + crt_sincos14(&sn, &cs, (n + 33) * 8192 / 180); + ccburst[x] = sn >> 10; + } + xo = AV_BEG; + yo = CRT_TOP; + + /* align signal */ + xo = (xo & ~3); + + /* this mess of offsetting logic was reached through trial and error */ + lo = (s->dot_crawl_offset % 3); /* line offset to match color burst */ + po = lo + 1; + if (lo == 1) { + lo = 3; + } + phase = 3 + po * 3; + + for (n = 0; n < CRT_VRES; n++) { + int t; /* time */ + signed char *line = &v->analog[n * CRT_HRES]; + + t = LINE_BEG; + + /* vertical sync scanlines */ + if (n >= 259 && n <= CRT_VRES) { + while (t < SYNC_BEG) line[t++] = BLANK_LEVEL; /* FP */ + while (t < PPUpx2pos(327)) line[t++] = SYNC_LEVEL; /* sync separator */ + while (t < CRT_HRES) line[t++] = BLANK_LEVEL; /* blank */ + } else { + int cb; + /* prerender/postrender/video scanlines */ + while (t < SYNC_BEG) line[t++] = BLANK_LEVEL; /* FP */ + while (t < BW_BEG) line[t++] = SYNC_LEVEL; /* SYNC */ + while (t < CB_BEG) line[t++] = BLANK_LEVEL; /* BW + CB + BP */ + /* CB_CYCLES of color burst at 3.579545 Mhz */ + for (t = CB_BEG; t < CB_BEG + (CB_CYCLES * CRT_CB_FREQ); t++) { + cb = ccburst[(t + po + n) & 3]; + line[t] = (BLANK_LEVEL + (cb * BURST_LEVEL)) >> 5; + iccf[(t + n) & 3] = line[t]; + } + while (t < LAV_BEG) line[t++] = BLANK_LEVEL; + phase += t * 3; + if (n >= CRT_TOP && n <= (CRT_BOT + 2)) { + while (t < CRT_HRES) { + int ire, p; + p = s->border_color; + if (t == LAV_BEG) p = 0xf0; + ire = BLACK_LEVEL + v->black_point; + ire += square_sample(p, phase + 0); + ire += square_sample(p, phase + 1); + ire += square_sample(p, phase + 2); + ire += square_sample(p, phase + 3); + ire = (ire * (WHITE_LEVEL * v->white_point / 100)) >> 12; + line[t++] = ire; + phase += 3; + } + } else { + while (t < CRT_HRES) line[t++] = BLANK_LEVEL; + phase += (CRT_HRES - LAV_BEG) * 3; + } + phase %= 12; + } + } + + phase = 6; + + for (y = (lo - 3); y < desth; y++) { + int sy = (y * s->h) / desth; + if (sy >= s->h) sy = s->h; + if (sy < 0) sy = 0; + + sy *= s->w; + phase += (xo * 3); + for (x = 0; x < destw; x++) { + int ire, p; + + p = s->data[((x * s->w) / destw) + sy]; + ire = BLACK_LEVEL + v->black_point; + ire += square_sample(p, phase + 0); + ire += square_sample(p, phase + 1); + ire += square_sample(p, phase + 2); + ire += square_sample(p, phase + 3); + ire = (ire * (WHITE_LEVEL * v->white_point / 100)) >> 12; + v->analog[(x + xo) + (y + yo) * CRT_HRES] = ire; + phase += 3; + } + /* mod here so we don't overflow down the line */ + phase = (phase + ((CRT_HRES - destw) * 3)) % 12; + } + + for (x = 0; x < 4; x++) { + for (n = 0; n < 4; n++) { + /* don't know why, but it works */ + v->ccf[n][x] = iccf[(x + n + 1) & 3] << 7; + } + } +} +#endif diff --git a/crt_nes.h b/crt_nes.h new file mode 100644 index 0000000..cf9c655 --- /dev/null +++ b/crt_nes.h @@ -0,0 +1,131 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * modifications for Mesen by Persune + * https://github.com/LMP88959/NTSC-CRT + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ + +#ifndef _CRT_NES_H_ +#define _CRT_NES_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* crt_nes.h + * + * An interface to convert NES PPU output to an analog NTSC signal. + * + */ + +/* 0 = vertical chroma (228 chroma clocks per line) */ +/* 1 = checkered chroma (227.5 chroma clocks per line) */ +/* 2 = sawtooth chroma (227.3 chroma clocks per line) */ +#define CRT_CHROMA_PATTERN 2 + +/* chroma clocks (subcarrier cycles) per line */ +#if (CRT_CHROMA_PATTERN == 1) +#define CRT_CC_LINE 2275 +#elif (CRT_CHROMA_PATTERN == 2) +#define CRT_CC_LINE 2273 +#else +/* this will give the 'rainbow' effect in the famous waterfall scene */ +#define CRT_CC_LINE 2280 +#endif + +/* NOTE, in general, increasing CRT_CB_FREQ reduces blur and bleed */ +#define CRT_CB_FREQ 4 /* carrier frequency relative to sample rate */ + +/* https://www.nesdev.org/wiki/NTSC_video#Scanline_Timing */ +#define CRT_HRES (CRT_CC_LINE * CRT_CB_FREQ / 10) /* horizontal res */ +#define CRT_VRES 262 /* vertical resolution */ +#define CRT_INPUT_SIZE (CRT_HRES * CRT_VRES) + +#define CRT_TOP 15 /* first line with active video */ +#define CRT_BOT 255 /* final line with active video */ +#define CRT_LINES (CRT_BOT - CRT_TOP) /* number of active video lines */ + +/* NES composite signal is measured in terms of PPU pixels, or cycles + * https://www.nesdev.org/wiki/NTSC_video#Scanline_Timing + * + * FULL HORIZONTAL LINE SIGNAL + * (341 PPU px; one cycle skipped on odd rendered frames) + * |---------------------------------------------------------------------------| + * HBLANK (58 PPU px) ACTIVE VIDEO (283 PPU px) + * |-------------------||------------------------------------------------------| + * + * + * WITHIN HBLANK PERIOD: + * + * FP (9 PPU px) SYNC (25 PPU px) BW (4 PPU px) CB (15 PPU px) BP (5 PPU px) + * |--------------||---------------||------------||-------------||-------------| + * BLANK SYNC BLANK BLANK BLANK + * + * + * WITHIN ACTIVE VIDEO PERIOD: + * + * LB (15 PPU px) AV (256 PPU px) RB (11 PPU px) + * |--------------||--------------------------------------------||-------------| + * BORDER VIDEO BORDER + * + */ +#define LINE_BEG 0 +#define FP_PPUpx 9 /* front porch */ +#define SYNC_PPUpx 25 /* sync tip */ +#define BW_PPUpx 4 /* breezeway */ +#define CB_PPUpx 15 /* color burst */ +#define BP_PPUpx 5 /* back porch */ +#define PS_PPUpx 1 /* pulse */ +#define LB_PPUpx 15 /* left border */ +#define AV_PPUpx 256 /* active video */ +#define RB_PPUpx 11 /* right border */ +#define HB_PPUpx (FP_PPUpx + SYNC_PPUpx + BW_PPUpx + CB_PPUpx + BP_PPUpx) /* h blank */ +/* line duration should be ~63500 ns */ +#define LINE_PPUpx (FP_PPUpx + SYNC_PPUpx + BW_PPUpx + CB_PPUpx + BP_PPUpx + PS_PPUpx + LB_PPUpx + AV_PPUpx + RB_PPUpx) + +/* convert pixel offset to its corresponding point on the sampled line */ +#define PPUpx2pos(PPUpx) ((PPUpx) * CRT_HRES / LINE_PPUpx) +/* starting points for all the different pulses */ +#define FP_BEG PPUpx2pos(0) /* front porch point */ +#define SYNC_BEG PPUpx2pos(FP_PPUpx) /* sync tip point */ +#define BW_BEG PPUpx2pos(FP_PPUpx + SYNC_PPUpx) /* breezeway point */ +#define CB_BEG PPUpx2pos(FP_PPUpx + SYNC_PPUpx + BW_PPUpx) /* color burst point */ +#define BP_BEG PPUpx2pos(FP_PPUpx + SYNC_PPUpx + BW_PPUpx + CB_PPUpx) /* back porch point */ +#define LAV_BEG PPUpx2pos(HB_PPUpx) /* full active video point */ +#define AV_BEG PPUpx2pos(HB_PPUpx + PS_PPUpx + LB_PPUpx) /* PPU active video point */ +#define AV_LEN PPUpx2pos(AV_PPUpx) /* active video length */ + +/* somewhere between 7 and 12 cycles */ +#define CB_CYCLES 10 + +/* line frequency */ +#define L_FREQ 1431818 /* full line */ + +/* IRE units (100 = 1.0V, -40 = 0.0V) */ +/* https://www.nesdev.org/wiki/NTSC_video#Terminated_measurement */ +#define WHITE_LEVEL 110 +#define BURST_LEVEL 30 +#define BLACK_LEVEL 0 +#define BLANK_LEVEL 0 +#define SYNC_LEVEL -37 + +struct NTSC_SETTINGS { + const unsigned short *data; /* 6 or 9-bit NES 'pixels' */ + int w, h; /* width and height of image */ + unsigned int border_color; /* either BG or black */ + int dot_crawl_offset; /* 0, 1, or 2 */ + /* NOTE: NES mode is always progressive */ + int hue; /* 0-359 */ +}; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/crt_ntsc.c b/crt_ntsc.c new file mode 100644 index 0000000..4c97429 --- /dev/null +++ b/crt_ntsc.c @@ -0,0 +1,292 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ + +#include "crt_core.h" + +#if (CRT_SYSTEM == CRT_SYSTEM_NTSC) +#include +#include + +#if (CRT_CHROMA_PATTERN == 1) +/* 227.5 subcarrier cycles per line means every other line has reversed phase */ +#define CC_PHASE(ln) (((ln) & 1) ? -1 : 1) +#else +#define CC_PHASE(ln) (1) +#endif + +#define EXP_P 11 +#define EXP_ONE (1 << EXP_P) +#define EXP_MASK (EXP_ONE - 1) +#define EXP_PI 6434 +#define EXP_MUL(x, y) (((x) * (y)) >> EXP_P) +#define EXP_DIV(x, y) (((x) << EXP_P) / (y)) + +static int e11[] = { + EXP_ONE, + 5567, /* e */ + 15133, /* e^2 */ + 41135, /* e^3 */ + 111817 /* e^4 */ +}; + +/* fixed point e^x */ +static int +expx(int n) +{ + int neg, idx, res; + int nxt, acc, del; + int i; + + if (n == 0) { + return EXP_ONE; + } + neg = n < 0; + if (neg) { + n = -n; + } + idx = n >> EXP_P; + res = EXP_ONE; + for (i = 0; i < idx / 4; i++) { + res = EXP_MUL(res, e11[4]); + } + idx &= 3; + if (idx > 0) { + res = EXP_MUL(res, e11[idx]); + } + + n &= EXP_MASK; + nxt = EXP_ONE; + acc = 0; + del = 1; + for (i = 1; i < 17; i++) { + acc += nxt / del; + nxt = EXP_MUL(nxt, n); + del *= i; + if (del > nxt || nxt <= 0 || del <= 0) { + break; + } + } + res = EXP_MUL(res, acc); + + if (neg) { + res = EXP_DIV(EXP_ONE, res); + } + return res; +} + +/*****************************************************************************/ +/********************************* FILTERS ***********************************/ +/*****************************************************************************/ + +/* infinite impulse response low pass filter for bandlimiting YIQ */ +static struct IIRLP { + int c; + int h; /* history */ +} iirY, iirI, iirQ; + +/* freq - total bandwidth + * limit - max frequency + */ +static void +init_iir(struct IIRLP *f, int freq, int limit) +{ + int rate; /* cycles/pixel rate */ + + memset(f, 0, sizeof(struct IIRLP)); + rate = (freq << 9) / limit; + f->c = EXP_ONE - expx(-((EXP_PI << 9) / rate)); +} + +static void +reset_iir(struct IIRLP *f) +{ + f->h = 0; +} + +/* hi-pass for debugging */ +#define HIPASS 0 + +static int +iirf(struct IIRLP *f, int s) +{ + f->h += EXP_MUL(s - f->h, f->c); +#if HIPASS + return s - f->h; +#else + return f->h; +#endif +} + +extern void +crt_modulate(struct CRT *v, struct NTSC_SETTINGS *s) +{ + int x, y, xo, yo; + int destw = AV_LEN; + int desth = ((CRT_LINES * 64500) >> 16); + int iccf[4]; + int ccmod[4]; /* color phase for mod */ + int ccburst[4]; /* color phase for burst */ + int sn, cs, n, ph; + int inv_phase = 0; + static int iir_inited = 0; + if (!iir_inited) { + init_iir(&iirY, L_FREQ, Y_FREQ); + init_iir(&iirI, L_FREQ, I_FREQ); + init_iir(&iirQ, L_FREQ, Q_FREQ); + iir_inited = 1; + } +#if CRT_DO_BLOOM + if (s->raw) { + destw = s->w; + desth = s->h; + if (destw > ((AV_LEN * 55500) >> 16)) { + destw = ((AV_LEN * 55500) >> 16); + } + if (desth > ((CRT_LINES * 63500) >> 16)) { + desth = ((CRT_LINES * 63500) >> 16); + } + } else { + destw = (AV_LEN * 55500) >> 16; + desth = (CRT_LINES * 63500) >> 16; + } +#else + if (s->raw) { + destw = s->w; + desth = s->h; + if (destw > AV_LEN) { + destw = AV_LEN; + } + if (desth > ((CRT_LINES * 64500) >> 16)) { + desth = ((CRT_LINES * 64500) >> 16); + } + } +#endif + if (s->as_color) { + for (x = 0; x < 4; x++) { + n = s->hue + x * 90; + crt_sincos14(&sn, &cs, (n + 33) * 8192 / 180); + ccburst[x] = sn >> 10; + crt_sincos14(&sn, &cs, n * 8192 / 180); + ccmod[x] = sn >> 10; + } + } else { + memset(ccburst, 0, sizeof(ccburst)); + memset(ccmod, 0, sizeof(ccmod)); + } + xo = AV_BEG + 4 + (AV_LEN - destw) / 2; + yo = CRT_TOP + 2 + (CRT_LINES - desth) / 2; + + s->field &= 1; + s->frame &= 1; + inv_phase = (s->field == s->frame); + ph = CC_PHASE(inv_phase); + + /* align signal */ + xo = (xo & ~3); + + for (n = 0; n < CRT_VRES; n++) { + int t; /* time */ + signed char *line = &v->analog[n * CRT_HRES]; + + t = LINE_BEG; + + if (n <= 3 || (n >= 7 && n <= 9)) { + /* equalizing pulses - small blips of sync, mostly blank */ + while (t < (4 * CRT_HRES / 100)) line[t++] = SYNC_LEVEL; + while (t < (50 * CRT_HRES / 100)) line[t++] = BLANK_LEVEL; + while (t < (54 * CRT_HRES / 100)) line[t++] = SYNC_LEVEL; + while (t < (100 * CRT_HRES / 100)) line[t++] = BLANK_LEVEL; + } else if (n >= 4 && n <= 6) { + int even[4] = { 46, 50, 96, 100 }; + int odd[4] = { 4, 50, 96, 100 }; + int *offs = even; + if (s->field == 1) { + offs = odd; + } + /* vertical sync pulse - small blips of blank, mostly sync */ + while (t < (offs[0] * CRT_HRES / 100)) line[t++] = SYNC_LEVEL; + while (t < (offs[1] * CRT_HRES / 100)) line[t++] = BLANK_LEVEL; + while (t < (offs[2] * CRT_HRES / 100)) line[t++] = SYNC_LEVEL; + while (t < (offs[3] * CRT_HRES / 100)) line[t++] = BLANK_LEVEL; + } else { + int cb; + + /* video line */ + while (t < SYNC_BEG) line[t++] = BLANK_LEVEL; /* FP */ + while (t < BW_BEG) line[t++] = SYNC_LEVEL; /* SYNC */ + while (t < AV_BEG) line[t++] = BLANK_LEVEL; /* BW + CB + BP */ + if (n < CRT_TOP) { + while (t < CRT_HRES) line[t++] = BLANK_LEVEL; + } + + /* CB_CYCLES of color burst at 3.579545 Mhz */ + for (t = CB_BEG; t < CB_BEG + (CB_CYCLES * CRT_CB_FREQ); t++) { +#if (CRT_CHROMA_PATTERN == 1) + cb = ccburst[(t + inv_phase * 2) & 3]; +#else + cb = ccburst[t & 3]; +#endif + line[t] = (BLANK_LEVEL + (cb * BURST_LEVEL)) >> 5; + iccf[t & 3] = line[t]; + } + } + } + + for (y = 0; y < desth; y++) { + int field_offset; + int sy; + + field_offset = (s->field * s->h + desth) / desth / 2; + sy = (y * s->h) / desth; + + sy += field_offset; + + if (sy >= s->h) sy = s->h; + + sy *= s->w; + + reset_iir(&iirY); + reset_iir(&iirI); + reset_iir(&iirQ); + + for (x = 0; x < destw; x++) { + int fy, fi, fq; + int pA, rA, gA, bA; + int ire; /* composite signal */ + + pA = s->rgb[((x * s->w) / destw) + sy]; + rA = (pA >> 16) & 0xff; + gA = (pA >> 8) & 0xff; + bA = (pA >> 0) & 0xff; + + /* RGB to YIQ */ + fy = (19595 * rA + 38470 * gA + 7471 * bA) >> 14; + fi = (39059 * rA - 18022 * gA - 21103 * bA) >> 14; + fq = (13894 * rA - 34275 * gA + 20382 * bA) >> 14; + ire = BLACK_LEVEL + v->black_point; + /* bandlimit Y,I,Q */ + fy = iirf(&iirY, fy); + fi = iirf(&iirI, fi) * ph * ccmod[(x + 0) & 3] >> 4; + fq = iirf(&iirQ, fq) * ph * ccmod[(x + 3) & 3] >> 4; + ire += (fy + fi + fq) * (WHITE_LEVEL * v->white_point / 100) >> 10; + if (ire < 0) ire = 0; + if (ire > 110) ire = 110; + + v->analog[(x + xo) + (y + yo) * CRT_HRES] = ire; + } + } + for (x = 0; x < 4; x++) { + for (n = 0; n < 4; n++) { + v->ccf[n][x] = iccf[x] << 7; + } + } +} +#endif diff --git a/crt_ntsc.h b/crt_ntsc.h new file mode 100644 index 0000000..932df6b --- /dev/null +++ b/crt_ntsc.h @@ -0,0 +1,118 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ +#ifndef _CRT_H_ +#define _CRT_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* crt_ntsc.h + * + * An interface to convert a digital image to an analog NTSC signal. + * + */ +/* 0 = vertical chroma (228 chroma clocks per line) */ +/* 1 = checkered chroma (227.5 chroma clocks per line) */ +#define CRT_CHROMA_PATTERN 1 + +/* chroma clocks (subcarrier cycles) per line */ +#if (CRT_CHROMA_PATTERN == 1) +#define CRT_CC_LINE 2275 +#else +/* this will give the 'rainbow' effect in the famous waterfall scene */ +#define CRT_CC_LINE 2280 +#endif + +/* NOTE, in general, increasing CRT_CB_FREQ reduces blur and bleed */ +#define CRT_CB_FREQ 4 /* carrier frequency relative to sample rate */ +#define CRT_HRES (CRT_CC_LINE * CRT_CB_FREQ / 10) /* horizontal res */ +#define CRT_VRES 262 /* vertical resolution */ +#define CRT_INPUT_SIZE (CRT_HRES * CRT_VRES) + +#define CRT_TOP 21 /* first line with active video */ +#define CRT_BOT 261 /* final line with active video */ +#define CRT_LINES (CRT_BOT - CRT_TOP) /* number of active video lines */ + +/* + * FULL HORIZONTAL LINE SIGNAL (~63500 ns) + * |---------------------------------------------------------------------------| + * HBLANK (~10900 ns) ACTIVE VIDEO (~52600 ns) + * |-------------------||------------------------------------------------------| + * + * + * WITHIN HBLANK PERIOD: + * + * FP (~1500 ns) SYNC (~4700 ns) BW (~600 ns) CB (~2500 ns) BP (~1600 ns) + * |--------------||---------------||------------||-------------||-------------| + * BLANK SYNC BLANK BLANK BLANK + * + */ +#define LINE_BEG 0 +#define FP_ns 1500 /* front porch */ +#define SYNC_ns 4700 /* sync tip */ +#define BW_ns 600 /* breezeway */ +#define CB_ns 2500 /* color burst */ +#define BP_ns 1600 /* back porch */ +#define AV_ns 52600 /* active video */ +#define HB_ns (FP_ns + SYNC_ns + BW_ns + CB_ns + BP_ns) /* h blank */ +/* line duration should be ~63500 ns */ +#define LINE_ns (FP_ns + SYNC_ns + BW_ns + CB_ns + BP_ns + AV_ns) + +/* convert nanosecond offset to its corresponding point on the sampled line */ +#define ns2pos(ns) ((ns) * CRT_HRES / LINE_ns) +/* starting points for all the different pulses */ +#define FP_BEG ns2pos(0) +#define SYNC_BEG ns2pos(FP_ns) +#define BW_BEG ns2pos(FP_ns + SYNC_ns) +#define CB_BEG ns2pos(FP_ns + SYNC_ns + BW_ns) +#define BP_BEG ns2pos(FP_ns + SYNC_ns + BW_ns + CB_ns) +#define AV_BEG ns2pos(HB_ns) +#define AV_LEN ns2pos(AV_ns) + +/* somewhere between 7 and 12 cycles */ +#define CB_CYCLES 10 + +/* frequencies for bandlimiting */ +#define L_FREQ 1431818 /* full line */ +#define Y_FREQ 420000 /* Luma (Y) 4.2 MHz of the 14.31818 MHz */ +#define I_FREQ 150000 /* Chroma (I) 1.5 MHz of the 14.31818 MHz */ +#define Q_FREQ 55000 /* Chroma (Q) 0.55 MHz of the 14.31818 MHz */ + +/* IRE units (100 = 1.0V, -40 = 0.0V) */ +#define WHITE_LEVEL 100 +#define BURST_LEVEL 20 +#define BLACK_LEVEL 7 +#define BLANK_LEVEL 0 +#define SYNC_LEVEL -40 + +#if (CRT_CHROMA_PATTERN == 1) +/* 227.5 subcarrier cycles per line means every other line has reversed phase */ +#define CC_PHASE(ln) (((ln) & 1) ? -1 : 1) +#else +#define CC_PHASE(ln) (1) +#endif + +struct NTSC_SETTINGS { + const int *rgb; /* 32-bit RGB image data (packed as 0xXXRRGGBB) */ + int w, h; /* width and height of image */ + int raw; /* 0 = scale image to fit monitor, 1 = don't scale */ + int as_color; /* 0 = monochrome, 1 = full color */ + int field; /* 0 = even, 1 = odd */ + int frame; /* 0 = even, 1 = odd */ + int hue; /* 0-359 */ +}; + +#ifdef __cplusplus +} +#endif + +#endif