diff --git a/crt_ntscvhs.c b/crt_ntscvhs.c new file mode 100644 index 0000000..261afed --- /dev/null +++ b/crt_ntscvhs.c @@ -0,0 +1,338 @@ +/*****************************************************************************/ +/* + * 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_NTSCVHS) +#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[CRT_CC_SAMPLES]; + int ccmodI[CRT_CC_SAMPLES]; /* color phase for mod */ + int ccmodQ[CRT_CC_SAMPLES]; /* color phase for mod */ + int ccburst[CRT_CC_SAMPLES]; /* color phase for burst */ + int sn, cs, n, ph; + int inv_phase = 0; + int bpp; + int aberration = 0; + + 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 (x = 0; x < CRT_CC_SAMPLES; x++) { + n = s->hue + x * (360 / CRT_CC_SAMPLES); + crt_sincos14(&sn, &cs, (n + 33) * 8192 / 180); + ccburst[x] = sn >> 10; + crt_sincos14(&sn, &cs, n * 8192 / 180); + ccmodI[x] = sn >> 10; + crt_sincos14(&sn, &cs, (n - 90) * 8192 / 180); + ccmodQ[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; + inv_phase = (s->field == s->frame); + ph = CC_PHASE(inv_phase); + + /* align signal */ + xo = (xo & ~3); + if (s->do_aberration) { + aberration = ((rand() % 12) - 8) + 14; + } + 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; + if (n < (CRT_VRES - aberration)) { + /* 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) + int off180 = CRT_CC_SAMPLES / 2; + cb = ccburst[(t + inv_phase * off180) % CRT_CC_SAMPLES]; +#else + cb = ccburst[t % CRT_CC_SAMPLES]; +#endif + line[t] = (BLANK_LEVEL + (cb * BURST_LEVEL)) >> 5; + iccf[t % CRT_CC_SAMPLES] = line[t]; + } + } + } + /* reset hsync every frame so only the bottom part is warped */ + v->hsync = 0; + + 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 rA, gA, bA; + const unsigned char *pix; + int ire; /* composite signal */ + int xoff; + + 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 = (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; + + xoff = (x + xo) % CRT_CC_SAMPLES; + /* bandlimit Y,I,Q */ + fy = iirf(&iirY, fy); + fi = iirf(&iirI, fi) * ph * ccmodI[xoff] >> 4; + fq = iirf(&iirQ, fq) * ph * ccmodQ[xoff] >> 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 (n = 0; n < CRT_CC_VPER; n++) { + for (x = 0; x < CRT_CC_SAMPLES; x++) { + v->ccf[n][x] = 0; + } + } +} +#endif diff --git a/crt_ntscvhs.h b/crt_ntscvhs.h new file mode 100644 index 0000000..4e2a80e --- /dev/null +++ b/crt_ntscvhs.h @@ -0,0 +1,150 @@ +/*****************************************************************************/ +/* + * 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_NTSC_VHS_H_ +#define _CRT_NTSC_VHS_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* crt_ntscvhs.h + * + * An interface to convert a digital image to an analog NTSC signal with VHS + * quality and some optional signal aberration at the bottom. + * + */ +/* 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 */ + +#define CRT_CC_SAMPLES 4 /* samples per chroma period (samples per 360 deg) */ +#define CRT_CC_VPER 1 /* vertical period in which the artifacts repeat */ + +/* search windows, in samples */ +#define CRT_HSYNC_WINDOW 8 +#define CRT_VSYNC_WINDOW 8 + +/* 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 (~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 + +#define VHS_SP 0 +#define VHS_LP 1 +#define VHS_EP 2 + +#define VHS_MODE VHS_SP + +/* frequencies for bandlimiting */ +#if (VHS_MODE == VHS_SP) +#define L_FREQ 1431818 /* full line */ +#define Y_FREQ 300000 /* Luma (Y) 3.0 MHz of the 14.31818 MHz */ +#define I_FREQ 62700 /* Chroma (I) 627 kHz of the 14.31818 MHz */ +#define Q_FREQ 62700 /* Chroma (Q) 627 kHz of the 14.31818 MHz */ +#elif (VHS_MODE == VHS_LP) +#define L_FREQ 1431818 /* full line */ +#define Y_FREQ 240000 /* Luma (Y) 2.4 MHz of the 14.31818 MHz */ +#define I_FREQ 40000 /* Chroma (I) 400 kHz of the 14.31818 MHz */ +#define Q_FREQ 40000 /* Chroma (Q) 400 kHz of the 14.31818 MHz */ +#elif (VHS_MODE == VHS_EP) +#define L_FREQ 1431818 /* full line */ +#define Y_FREQ 200000 /* Luma (Y) 2.0 MHz of the 14.31818 MHz */ +#define I_FREQ 37000 /* Chroma (I) 370 kHz of the 14.31818 MHz */ +#define Q_FREQ 37000 /* Chroma (Q) 370 kHz of the 14.31818 MHz */ +#endif + +/* 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 + +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; /* 0 = even, 1 = odd */ + int frame; /* 0 = even, 1 = odd */ + 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 do_aberration; /* 0 = no aberration, 1 = with aberration */ + /* make sure your NTSC_SETTINGS struct is zeroed out before you do anything */ + int iirs_initialized; /* internal state */ +}; + +#ifdef __cplusplus +} +#endif + +#endif