Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- |||||||||| || || || || || || ||
- || || || |||||| || || |||||||| ||||||||
- || || || || || || || || ||
- mkvhs by G. Moran
- version 1.0.0 [28/02/2023]
- This program is in the public domain, licensed under the Unlicense:
- https://unlicense.org/
- */
- /* MANUAL AND FAQ
- ______________________________________________________________________________*/
- /*
- * How do I use this?
- ********************
- To use mkvhs you need to...
- (1) Compile this file.
- (2) Download the SoX program.
- (3) Know how to use the terminal or command prompt.
- Save this file as `mkvhs.c` then compile it using any C compiler.
- For example if using GCC simply run:
- gcc mkvhs.c -o mkvhs
- and you'll get an executable named `mkvhs` or `mkvhs.exe` .
- Then download SoX from here: https://sox.sourceforge.net/
- and try running:
- sox --version
- If a version number is displayed then you're good to go.
- You can now use mkvhs by running:
- mkvhs file.tga
- where `file.tga` is a 24-bit Targa image with dimensions 640 x 480.
- The output will be named `file.tga.vhs.tga` .
- You can customize the file naming in the OPTIONS section below.
- * Can I use mkvhs with video?
- *****************************
- Yes, by extracting the frames of the video as TGA images then
- feeding each image into mkvhs like this:
- mkvhs frame0001.tga
- mkvhs frame0002.tga
- mkvhs frame0003.tga
- ...
- Then combining the new frames back into a video file.
- WARNING: Be careful with long videos!
- A single 640 x 480 TGA image is almost 1 MB in file size,
- so a video that's 1 minute long running at 30 frames per second
- will produce more than 1.5 GB worth of TGA files. You have been warned.
- * How do I extract and recombine video frames?
- **********************************************
- You can use a program called FFMPEG to manipulate video frames.
- Download FFMPEG from here: https://ffmpeg.org/download.html
- Copy your video to an empty directory, then extract each video frame
- into a separate TGA file by running:
- ffmpeg -i yourvideo.mp4 -rle 0 frame%04d.tga
- Then loop through the frame files and feed each one into mkvhs.
- On linux for example you can achieve this by running:
- for i in frame*; do mkvhs `basename "$i"`; done
- Finally recombine the new frames into a video by running:
- ffmpeg -framerate 30 -pattern_type glob -i '*.vhs.tga' output.mp4
- while using the framerate value from the original video.
- * How do I tweak the way the output looks?
- ******************************************
- Scroll down to the OPTIONS section and change the values to your liking.
- You can fine-tune noise, blur, colors...etc. Feel free to experiment.
- Make sure to always read the WARNING messages.
- When you're done making changes, recompile this file.
- * What are the limitations of mkvhs?
- ************************************
- (1) The input image is resampled row-by-row, so any sharp vertical
- transitions in color are preserved. This is currently mitigated by
- slightly blurring the resampled image in the vertical direction.
- (2) Since the Luma (Y) and Chroma (Cb/Cr) channels are resampled
- separately, there's no interference between them. This causes
- the output to lack color bleed and "rainbow artifacts".
- * How do I force mkvhs to overwrite the input file?
- ***************************************************
- Scroll down to the OPTIONS section and clear the OUTSUFFIX string:
- #define OUTSUFFIX ""
- then recompile the program.
- */
- #include <stdio.h>
- #include <stdlib.h>
- /* OPTIONS
- ______________________________________________________________________________*/
- /* output file names
- WARNING: add a prefix only when passing base names!
- i.e. `file.tga` not `path/to/file.tga`
- */
- #define OUTPREFIX ""
- #define OUTSUFFIX ".vhs.tga"
- /* create file with raw Y/Cb/Cr triplets (default=no) */
- #define MAKEYUV NO
- #define YUVPREFIX ""
- #define YUVSUFFIX ".vhs.yuv"
- /* temporary files to be created in the current directory */
- /* WARNING: if these files already exist they will be overwritten! */
- #define TEMPY1 "y1.raw"
- #define TEMPU1 "u1.raw"
- #define TEMPV1 "v1.raw"
- #define TEMPY2 "y2.raw"
- #define TEMPU2 "u2.raw"
- #define TEMPV2 "v2.raw"
- /* keep temporary files (default=no) */
- #define KEEPTEMP NO
- /* image dimensions (do not change) */
- #define WIDTH 640
- #define HEIGHT 480
- /* account for overscan during processing (default=yes) */
- #define OVERSCAN YES
- /* image width incl. overscan, image offset (do not change) */
- #define OVERWIDTH (WIDTH*5/4)
- #define OVEROFFSET ((OVERWIDTH-WIDTH)/2)
- #define FINALWIDTH (OVERSCAN ? OVERWIDTH : WIDTH)
- /* "wash out" colors; ignore headroom and footroom (default=yes) */
- #define WASHOUT YES
- /* make image black and white (default=no) */
- #define GRAYSCALE NO
- /* add noise (default=yes) */
- #define NOISE YES
- /* noise levels (0=none, 8=maximum) */
- #define NOISEY 3
- #define NOISEU 4
- #define NOISEV 4
- /* noise scales w.r.t. image resolution */
- #define NSCALEY 1/2
- #define NSCALEU 1/4
- #define NSCALEV 1/4
- /* frequency ratios for resampling
- brand new mint tape --> 4:2:1:1
- 2nd generation tape --> 6:2:1:1
- 3rd generation tape --> 8:2:1:1
- ...
- */
- #define FREQO "8k"
- #define FREQY "2k"
- #define FREQU "1k"
- #define FREQV "1k"
- /* vertically blur image after resampling (default=yes) */
- #define VBLUR YES
- /* vertical blur strength (0=none, 8=maximum) */
- #define VBLURY 1
- #define VBLURU 2
- #define VBLURV 2
- /* USEFUL MACROS
- ______________________________________________________________________________*/
- /* booleans */
- #define YES 1
- #define NO 0
- /* make a string literal out of a macro */
- #define STR(A) STR_(A)
- #define STR_(A) #A
- /* surround a string with escaped quotes and spaces */
- #define ESCQUO(SS) ESCQUO_(SS)
- #define ESCQUO_(S) " \"" S "\" "
- /* bound a value between a lower and upper limit */
- #define BOUND(X,A,B) ( BOUND_((X),(A),(B)) )
- #define BOUND_(X,A,B) X<A? A : X>B? B : X
- /* print stuff */
- #define DISPERROR(S) !!printf("ERROR: %s\n", S)
- #define SHOWUSAGE() !!printf("%s\n", "USAGE: mkvhs file.tga")
- /* NOISE GENERATOR
- ______________________________________________________________________________*/
- /*
- this RNG was chosen specifically because the lower bytes create a pattern
- similar to TV static
- https://en.wikipedia.org/wiki/Pseudorandom_binary_sequence
- https://archive.today/DHyJL
- */
- long
- randnum(long seed)
- {
- long nbit = ~((seed >> 30) ^ (seed >> 27)) & 1;
- seed = (seed << 1) | nbit;
- return seed &= 0xFFFFFFFF;
- }
- /* write [length] bytes of noise into an array */
- int
- makenoise(unsigned char *out, long length, long seed)
- {
- long i;
- if (!out) return 1;
- for (i=0; i<length; i++) {
- seed = randnum(seed);
- out[i] = (unsigned char) (seed&0xFF);
- }
- return 0;
- }
- /* BILINEAR FILTER
- ______________________________________________________________________________*/
- /* scale an image using bilinear interpolation */
- int
- scale(
- unsigned char *in, /* input array (flattened 2D grid) */
- FILE *out, /* output file */
- long w1, long h1, /* input dimensions */
- long w2, long h2, /* output dimensions */
- char ch, /* number of channels (bytes) per pixel */
- long off /* input offset to image data */
- )
- {
- long i, j;
- char k;
- if (!in | !out) return 1;
- for (j=0; j<h2; j++)
- for (i=0; i<w2; i++)
- for (k=0; k<ch; k++) {
- long px, py, i0, i1, j0, j1;
- short v00, v01, v10, v11,
- v0, v1, v,
- dx, dy;
- /* ratios (%) */
- px = (i*100)*(w1-1)/(w2-1);
- py = (j*100)*(h1-1)/(h2-1);
- px = BOUND(px,0,(w1-1)*100);
- py = BOUND(py,0,(h1-1)*100);
- /* round down */
- i0 = px / 100;
- j0 = py / 100;
- /* round up */
- i1=i0+1;
- j1=j0+1;
- /* bound */
- i0 = BOUND(i0,0,w1-1);
- i1 = BOUND(i1,0,w1-1);
- j0 = BOUND(j0,0,h1-1);
- j1 = BOUND(j1,0,h1-1);
- /* read 4 pixels surrounding point */
- v00 = in[off+j0*w1*ch+i0*ch+k];
- v10 = in[off+j0*w1*ch+i1*ch+k];
- v01 = in[off+j1*w1*ch+i0*ch+k];
- v11 = in[off+j1*w1*ch+i1*ch+k];
- /* horizontal interpolation */
- dx = i1*100-px;
- v0 = (v00*dx + v10*(100-dx)) / 100;
- v1 = (v01*dx + v11*(100-dx)) / 100;
- /* vertical interpolation */
- dy = j1*100-py;
- v = ( v0*dy + v1*(100-dy)) / 100;
- /* write interpolated byte */
- fputc(v, out);
- }
- return 0;
- }
- /* BLURRING FILTER
- ______________________________________________________________________________*/
- /* vertically blur an image */
- int
- vblur(
- unsigned char *in, /* input array (flattened 2D grid) */
- FILE *out, /* output file */
- long w, long h, /* input dimensions */
- char ch, /* number of channels (bytes) per pixel */
- long off, /* input offset to image data */
- char strength /* blur strength */
- )
- {
- long i, j;
- char k;
- short size;
- if (!in | !out) return 1;
- size = 1 << strength;
- for (j=0; j<h; j++)
- for (i=0; i<w; i++)
- for (k=0; k<ch; k++) {
- long jj, start, end, idx;
- unsigned total;
- start = j - (size/2-1);
- end = j + (size/2);
- total = 0;
- for (jj=start; jj<=end; jj++) {
- idx = BOUND(jj,0,h-1);
- total += (long)in[off + idx*w*ch + i*ch + k];
- }
- total >>= strength;
- fputc((int)total, out);
- }
- return 0;
- }
- /* MAIN
- ______________________________________________________________________________*/
- /* start here */
- int main(int argc, char* argv[]) {
- FILE *in, *out,
- *fy1, *fu1, *fv1,
- *fy2, *fu2, *fv2,
- *yuv;
- long nsy, nsu, nsv;
- long i, j, o;
- unsigned char buffer[FINALWIDTH * HEIGHT * 3];
- char outname[FILENAME_MAX];
- int temp, idfield, tgaorigin;
- /*** VERIFY INPUT ***/
- if (argc != 2)
- return (--argc && DISPERROR("Only one input file allowed")) &
- SHOWUSAGE();
- in = fopen(argv[1], "rb");
- if (!in)
- return DISPERROR("Could not open input file");
- /* start reading TGA image header */
- idfield = fgetc(in);
- temp = fgetc(in);
- if (temp || fgetc(in) != 2)
- return DISPERROR("Only unmapped 24-bit TGA files are allowed");
- fgetc(in); fgetc(in); fgetc(in); fgetc(in); fgetc(in);
- fgetc(in); fgetc(in); fgetc(in); fgetc(in);
- temp = fgetc(in);
- temp |= fgetc(in)<<8;
- if (temp != WIDTH)
- return DISPERROR("Image width is not " STR(WIDTH));
- temp = fgetc(in);
- temp |= fgetc(in)<<8;
- if (temp != HEIGHT)
- return DISPERROR("Image height is not " STR(HEIGHT));
- if (fgetc(in) != 24)
- return DISPERROR("Only unmapped 24-bit TGA files are allowed");
- tgaorigin = fgetc(in);
- if (tgaorigin != 32 && tgaorigin)
- return DISPERROR("Unknown TGA descriptor byte");
- while (idfield-- > 0)
- fgetc(in);
- /*** GENERATE NOISE ***/
- if (NOISE) {
- /* calculate seeds from filename */
- unsigned char *name = (unsigned char*) argv[1];
- unsigned seed, n;
- long seedy, seedu, seedv;
- for (seed=n=0; *name; name++, n++)
- seed = (seed + (*name << (n&7))) & 0x7FFFFFFF;
- seedy = (long) seed + 'Y';
- seedu = (long) seed + 'C' + 'b';
- seedv = (long) seed + 'C' + 'r';
- /* create noise files */
- fy2 = fopen(TEMPY2, "wb");
- fu2 = fopen(TEMPU2, "wb");
- fv2 = fopen(TEMPV2, "wb");
- if (!fy2 | !fu2 | !fv2)
- return DISPERROR("Could not create noise files");
- /* generate noise in memory, then write it scaled up to file */
- makenoise(buffer, WIDTH*NSCALEY * HEIGHT*NSCALEY, seedy);
- scale(buffer,fy2, WIDTH*NSCALEY , HEIGHT*NSCALEY,WIDTH,HEIGHT,1,0);
- makenoise(buffer, WIDTH*NSCALEU * HEIGHT*NSCALEU, seedu);
- scale(buffer,fu2, WIDTH*NSCALEU , HEIGHT*NSCALEU,WIDTH,HEIGHT,1,0);
- makenoise(buffer, WIDTH*NSCALEV * HEIGHT*NSCALEV, seedv);
- scale(buffer,fv2, WIDTH*NSCALEV , HEIGHT*NSCALEV,WIDTH,HEIGHT,1,0);
- fclose(fy2);
- fclose(fu2);
- fclose(fv2);
- }
- /*** CONVERT IMAGE TO YCBCR ***/
- /* create separate Y/Cb/Cr files */
- fy1 = fopen(TEMPY1, "wb");
- fu1 = fopen(TEMPU1, "wb");
- fv1 = fopen(TEMPV1, "wb");
- if (!fy1 | !fu1 | !fv1)
- return DISPERROR("Could not create temporary files");
- /* open noise files */
- if (NOISE) {
- fy2 = fopen(TEMPY2, "rb");
- fu2 = fopen(TEMPU2, "rb");
- fv2 = fopen(TEMPV2, "rb");
- if (!fy2 | !fu2 | !fv2)
- return DISPERROR("Could not open noise files");
- /* set noise levels (shifts) */
- nsy = 8-NOISEY;
- nsu = 8-NOISEU;
- nsv = 8-NOISEV;
- }
- /*
- convert RGB to YCbCr BT.601
- http://www.equasys.de/colorconversion.html
- https://archive.today/8yPOE
- */
- for (j=0; j<HEIGHT; j++) {
- for (o=OVERSCAN*OVEROFFSET; o; o--) {
- fputc( 16, fy1);
- fputc(128, fu1);
- fputc(128, fv1);
- }
- for (i=0; i<WIDTH; i++) {
- long r, g, b, y, u, v;
- short sy, su, sv;
- b = fgetc(in);
- g = fgetc(in);
- r = fgetc(in);
- /* half range: Y [16-235], Cb/Cr [16-240]
- Y = 16 + 0.257 R + 0.504 G + 0.098 B
- U = 128 - 0.148 R - 0.291 G + 0.439 B
- V = 128 + 0.439 R - 0.368 G - 0.071 B
- */
- y = 16 + ( 66*r + 129*g + 25*b) / 256;
- u = 128 + (- 38*r - 74*g + 112*b) / 256;
- v = 128 + ( 112*r - 94*g - 18*b) / 256;
- if (GRAYSCALE)
- u = v = 128;
- if (NOISE) {
- sy = (fgetc(fy2)>>nsy) - (128>>nsy);
- su = (fgetc(fu2)>>nsu) - (128>>nsu);
- sv = (fgetc(fv2)>>nsv) - (128>>nsv);
- y += sy; u += su; v += sv;
- y = BOUND(y,16,235);
- u = BOUND(u,16,240);
- v = BOUND(v,16,240);
- }
- fputc(y, fy1);
- fputc(u, fu1);
- fputc(v, fv1);
- }
- for (o=OVERSCAN*OVEROFFSET; o; o--) {
- fputc( 16, fy1);
- fputc(128, fu1);
- fputc(128, fv1);
- }
- }
- fclose(in);
- fclose(fy1);
- fclose(fu1);
- fclose(fv1);
- if (NOISE) {
- fclose(fy2);
- fclose(fu2);
- fclose(fv2);
- }
- /*** RESAMPLE IMAGE ***/
- /* Y */
- system( "sox -r " FREQO " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPY1)
- " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPY2)
- "rate -m -b 85 -p 50 -a " FREQY );
- system( "sox -r " FREQY " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPY2)
- " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPY1)
- "rate -m -b 85 -p 50 -a " FREQO );
- /* Cb */
- system( "sox -r " FREQO " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPU1)
- " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPU2)
- "rate -m -b 85 -p 75 -a " FREQU );
- system( "sox -r " FREQU " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPU2)
- " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPU1)
- "rate -m -b 85 -p 75 -a " FREQO );
- /* Cr */
- system( "sox -r " FREQO " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPV1)
- " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPV2)
- "rate -m -b 85 -p 75 -a " FREQV );
- system( "sox -r " FREQV " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPV2)
- " -e unsigned -b 8 -c 1 -D" ESCQUO(TEMPV1)
- "rate -m -b 85 -p 75 -a " FREQO );
- /*** VERTICALLY BLUR RESAMPLED IMAGE ***/
- if (VBLUR) {
- int f;
- char *filestrs[3] = {TEMPY1, TEMPU1, TEMPV1};
- char vbstrens[3] = {VBLURY, VBLURU, VBLURV};
- FILE *fptr;
- for (f=0; f<3; f++) {
- /* read resampled Y/Cb/Cr file into memory */
- fptr = fopen(filestrs[f], "rb");
- if (!fptr)
- return DISPERROR("Could not open temporary files");
- for (j=0; j<HEIGHT; j++)
- for (i=0; i<FINALWIDTH; i++)
- buffer[j*FINALWIDTH + i] = fgetc(fptr);
- fclose(fptr);
- /* write blurred image into same file */
- fptr = fopen(filestrs[f], "wb");
- if (!fptr)
- return DISPERROR("Could not create temporary files");
- vblur(buffer,fptr,FINALWIDTH,HEIGHT,1,0,vbstrens[f]);
- fclose(fptr);
- }
- }
- /*** CONVERT BACK TO RGB AND WRITE FINAL IMAGE ***/
- fy1 = fopen(TEMPY1, "rb");
- fu1 = fopen(TEMPU1, "rb");
- fv1 = fopen(TEMPV1, "rb");
- if (!fy1 | !fu1 | !fv1)
- return DISPERROR("Could not open temporary files");
- sprintf(outname, "%s%s%s", OUTPREFIX, argv[1], OUTSUFFIX);
- out = fopen(outname, "wb");
- if (!out)
- return DISPERROR("Could not create output RGB file");
- if (MAKEYUV) {
- sprintf(outname, "%s%s%s", YUVPREFIX, argv[1], YUVSUFFIX);
- yuv = fopen(outname, "wb");
- if (!yuv)
- return DISPERROR("Could not create output YUV file");
- }
- /* write TGA image header */
- fputc(0,out); fputc(0,out);
- fputc(2,out);
- fputc(0,out); fputc(0,out); fputc(0,out); fputc(0,out); fputc(0,out);
- fputc(0,out); fputc(0,out); fputc(0,out); fputc(0,out);
- fputc(WIDTH & 0xFF,out); fputc(WIDTH >>8 & 0xFF,out);
- fputc(HEIGHT & 0xFF,out); fputc(HEIGHT>>8 & 0xFF,out);
- fputc(24,out);
- fputc(tgaorigin,out);
- /*
- convert YCbCr BT.601 back to RGB
- http://www.equasys.de/colorconversion.html
- https://archive.today/8yPOE
- */
- for (j=0; j<HEIGHT; j++) {
- for (o=OVERSCAN*OVEROFFSET; o; o--) {
- fgetc(fy1);
- fgetc(fu1);
- fgetc(fv1);
- }
- for (i=0; i<WIDTH; i++) {
- long r, g, b, y, u, v;
- y = fgetc(fy1);
- u = fgetc(fu1);
- v = fgetc(fv1);
- if (MAKEYUV) {
- fputc(y, yuv);
- fputc(u, yuv);
- fputc(v, yuv);
- }
- /* full range: Y/Cb/Cr [0-255]
- R = Y + + 1.400 (V-128)
- G = Y - 0.343 (U-128) - 0.711 (V-128)
- B = Y + 1.765 (U-128)
- */
- if (WASHOUT) {
- u-=128, v-=128;
- r = y + ( 358*v) / 256;
- g = y + (- 88*u - 182*v) / 256;
- b = y + ( 452*u ) / 256;
- }
- /* half range: Y [16-235], Cb/Cr [16-240]
- R = 1.164 (Y-16) + 1.596 (Cr-128)
- G = 1.164 (Y-16) - 0.392 (Cb-128) - 0.813 (Cr-128)
- B = 1.164 (Y-16) + 2.017 (Cb-128)
- */
- else {
- y -= 16, u -= 128, v -= 128;
- r = (298*y + 409*v) / 256;
- g = (298*y - 100*u - 208*v) / 256;
- b = (298*y + 516*u ) / 256;
- }
- /* necessary; values can be over 255 or negative */
- r = BOUND(r,0,255);
- g = BOUND(g,0,255);
- b = BOUND(b,0,255);
- fputc(b,out);
- fputc(g,out);
- fputc(r,out);
- }
- for (o=OVERSCAN*OVEROFFSET; o; o--) {
- fgetc(fy1);
- fgetc(fu1);
- fgetc(fv1);
- }
- }
- fclose(out);
- fclose(fy1);
- fclose(fu1);
- fclose(fv1);
- if (MAKEYUV) fclose(yuv);
- /* clean up */
- if (!KEEPTEMP) {
- remove(TEMPY1);
- remove(TEMPU1);
- remove(TEMPV1);
- remove(TEMPY2);
- remove(TEMPU2);
- remove(TEMPV2);
- }
- return 0;
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement