diff --git a/crt_snes.c b/crt_snes.c new file mode 100644 index 0000000..2912134 --- /dev/null +++ b/crt_snes.c @@ -0,0 +1,327 @@ +/*****************************************************************************/ +/* + * 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_SNES) +#include +#include + +#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) +{ +#if CRT_DO_BANDLIMITING + f->h += EXP_MUL(s - f->h, f->c); +#if HIPASS + return s - f->h; +#else + return f->h; +#endif +#else + return s; +#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[CRT_CC_VPER][CRT_CC_SAMPLES]; + int ccmodI[CRT_CC_VPER][CRT_CC_SAMPLES]; /* color phase for mod */ + int ccmodQ[CRT_CC_VPER][CRT_CC_SAMPLES]; /* color phase for mod */ + int ccburst[CRT_CC_VPER][CRT_CC_SAMPLES]; /* color phase for burst */ + int sn, cs, n, ph; + int bpp; + + if (!s->iirs_initialized) { + init_iir(&iirY, L_FREQ, Y_FREQ); + init_iir(&iirI, L_FREQ, I_FREQ); + init_iir(&iirQ, L_FREQ, Q_FREQ); + s->iirs_initialized = 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 (y = 0; y < CRT_CC_VPER; y++) { + int vert = (y + s->dot_crawl_offset) * (360 / CRT_CC_VPER); + for (x = 0; x < CRT_CC_SAMPLES; x++) { + int step = (360 / CRT_CC_SAMPLES); + n = vert + s->hue + x * step; + crt_sincos14(&sn, &cs, (n - step + HUE_OFFSET) * 8192 / 180); + ccburst[y][x] = sn >> 10; + crt_sincos14(&sn, &cs, n * 8192 / 180); + ccmodI[y][x] = sn >> 10; + crt_sincos14(&sn, &cs, (n + Q_OFFSET) * 8192 / 180); + ccmodQ[y][x] = sn >> 10; + } + } + } else { + memset(ccburst, 0, sizeof(ccburst)); + memset(ccmodI, 0, sizeof(ccmodI)); + memset(ccmodQ, 0, sizeof(ccmodQ)); + } + + bpp = crt_bpp4fmt(s->format); + if (bpp == 0) { + return; /* just to be safe */ + } + xo = AV_BEG + s->xoffset + (AV_LEN - destw) / 2; + yo = CRT_TOP + s->yoffset + (CRT_LINES - desth) / 2; + + s->field &= 1; + s->frame &= 1; + + /* align signal */ + xo = xo - (xo % CRT_CC_SAMPLES); + + for (n = 0; n < CRT_VRES; n++) { + int t; /* time */ + signed char *line = &v->analog[n * CRT_HRES]; + + t = LINE_BEG; + + if ((n >= EQU_REGION_A_LO && n <= EQU_REGION_A_HI) || + (n >= EQU_REGION_B_LO && n <= EQU_REGION_B_HI)) { + /* 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 >= SYNC_REGION_LO && n <= SYNC_REGION_HI) { + int even[4] = { 46, 50, 96, 100 }; + int *offs = even; + /* 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 */ + for (t = CB_BEG; t < CB_BEG + (CB_CYCLES * CRT_CB_FREQ); t++) { + cb = ccburst[n % CRT_CC_VPER][t % CRT_CC_SAMPLES]; + line[t] = (BLANK_LEVEL + (cb * BURST_LEVEL)) >> 5; + iccf[(n + 3) % CRT_CC_VPER][t % CRT_CC_SAMPLES] = line[t]; + } + } + } + + for (y = 0; y < desth; y++) { + int sy; + + sy = (y * s->h) / desth; + + + if (sy >= s->h) sy = s->h; + + sy *= s->w; + + reset_iir(&iirY); + reset_iir(&iirI); + reset_iir(&iirQ); + + ph = (y + yo) % CRT_CC_VPER; + + for (x = 0; x < destw; x++) { + int fy, fi, fq; + int rA, gA, bA; + const unsigned char *pix; + int ire; /* composite signal */ + int xoff; + /* RGB to YIQ matrix in 16.16 fixed point format */ + static int yiqmat[9] = { + 19595, 38470, 7471, /* Y */ + 39059, -18022, -21103, /* I */ + 13894, -34275, 20382, /* Q */ + }; + pix = s->data + ((((x * s->w) / destw) + sy) * bpp); + switch (s->format) { + case CRT_PIX_FORMAT_RGB: + case CRT_PIX_FORMAT_RGBA: + rA = pix[0]; + gA = pix[1]; + bA = pix[2]; + break; + case CRT_PIX_FORMAT_BGR: + case CRT_PIX_FORMAT_BGRA: + rA = pix[2]; + gA = pix[1]; + bA = pix[0]; + break; + case CRT_PIX_FORMAT_ARGB: + rA = pix[1]; + gA = pix[2]; + bA = pix[3]; + break; + case CRT_PIX_FORMAT_ABGR: + rA = pix[3]; + gA = pix[2]; + bA = pix[1]; + break; + default: + rA = gA = bA = 0; + break; + } + + /* RGB to YIQ */ + fy = (yiqmat[0] * rA + yiqmat[1] * gA + yiqmat[2] * bA) >> 14; + fi = (yiqmat[3] * rA + yiqmat[4] * gA + yiqmat[5] * bA) >> 14; + fq = (yiqmat[6] * rA + yiqmat[7] * gA + yiqmat[8] * bA) >> 14; + ire = BLACK_LEVEL + v->black_point; + + xoff = (x + xo) % CRT_CC_SAMPLES; + /* bandlimit Y,I,Q */ + fy = iirf(&iirY, fy); + fi = iirf(&iirI, fi) * ccmodI[ph][xoff] >> 4; + fq = iirf(&iirQ, fq) * ccmodQ[ph][xoff] >> 4; + /* modulate as (Y + sin(x) * I + cos(x) * Q) */ + ire += (fy + fi + fq) * (WHITE_LEVEL * v->white_point / 100) >> 10; + if (ire < IRE_MIN) ire = IRE_MIN; + if (ire > IRE_MAX) ire = IRE_MAX; + + v->analog[(x + xo) + (y + yo) * CRT_HRES] = ire; + } + } + + for (n = 0; n < CRT_CC_VPER; n++) { + for (x = 0; x < CRT_CC_SAMPLES; x++) { + v->ccf[n][x] = iccf[n][x] << 7; + } + } +} +#endif diff --git a/crt_snes.h b/crt_snes.h new file mode 100644 index 0000000..9aaa2b8 --- /dev/null +++ b/crt_snes.h @@ -0,0 +1,164 @@ +/*****************************************************************************/ +/* + * 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_SNES_H_ +#define _CRT_SNES_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* crt_snes.h + * + * An interface to convert a digital image to an analog NTSC signal in a + * fashion similar to an SNES. + * + */ +#define CRT_CC_LINE 2273 + +/* 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 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 */ + +#define CRT_CC_SAMPLES 4 /* samples per chroma period (samples per 360 deg) */ +#define CRT_CC_VPER 3 /* vertical period in which the artifacts repeat */ + +/* search windows, hsync is in terms of samples, vsync is lines */ +#define CRT_HSYNC_WINDOW 6 +#define CRT_VSYNC_WINDOW 6 + +/* accumulated signal threshold required for sync detection. + * Larger = more stable, until it's so large that it is never reached in which + * case the CRT won't be able to sync + */ +#define CRT_HSYNC_THRESH 4 +#define CRT_VSYNC_THRESH 94 + +/* + * 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 + +#define CRT_DO_BANDLIMITING 0 /* enable/disable bandlimiting when encoding */ +/* frequencies for bandlimiting */ +#define L_FREQ 1431818 /* full line */ +#define Y_FREQ 420000 /* Luma (Y) 4.2 MHz */ +#define I_FREQ 150000 /* Chroma (I) 1.5 MHz */ +#define Q_FREQ 55000 /* Chroma (Q) 0.55 MHz */ + +/* IRE units (100 = 1.0V, -40 = 0.0V) + * IRE is used because it fits nicely in an 8-bit signed char + */ +#define WHITE_LEVEL 100 +#define BURST_LEVEL 20 +#define BLACK_LEVEL 7 +#define BLANK_LEVEL 0 +#define SYNC_LEVEL -40 +#define IRE_MAX 110 /* max value is max value of signed char */ +#define IRE_MIN 0 /* min value is min value of signed char */ + +/* how much Q's phase is offset relative to I. + * Should generally be 90 degrees, however, changing + * variables like CRT_CB_FREQ and CRT_CC_SAMPLES can + * cause its sign to change from + to - or vice versa. + * If you notice the SMPTE bars having colors in the wrong order even + * when the right hue is set, then this might be what you need to change. + */ +#define Q_OFFSET (-90) /* in degrees */ + +/* burst hue offset */ +#define HUE_OFFSET (210) /* in degrees */ + +/* define line ranges in which sync is generated + * the numbers are inclusive + * Make sure these numbers fit in (0, CRT_VRES) + */ +#define SYNC_REGION_LO 3 +#define SYNC_REGION_HI 6 +/* same as above but for equalizing pulses */ +#define EQU_REGION_A_LO 0 +#define EQU_REGION_A_HI 2 + +#define EQU_REGION_B_LO 7 +#define EQU_REGION_B_HI 9 + +struct NTSC_SETTINGS { + const unsigned char *data; /* image data */ + int format; /* pix format (one of the CRT_PIX_FORMATs in crt_core.h) */ + 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; /* unused */ + int frame; /* unused */ + int hue; /* 0-359 */ + int xoffset; /* x offset in sample space. 0 is minimum value */ + int yoffset; /* y offset in # of lines. 0 is minimum value */ + int dot_crawl_offset; /* 0-3 */ + /* make sure your NTSC_SETTINGS struct is zeroed out before you do anything */ + int iirs_initialized; /* internal state */ +}; + +#ifdef __cplusplus +} +#endif + +#endif