├── .gitignore ├── Screenshot.chop.png ├── Screenshot.lode.png ├── random.h ├── analogtv-apple2.h ├── Makefile ├── thread.c ├── analogtv-test.c ├── thread.h ├── apple2+.charset.h ├── analogtv.h ├── analogtv-apple2.c ├── README.md └── analogtv.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.d 2 | *.o 3 | out*.png 4 | analogtv-test 5 | *~ 6 | -------------------------------------------------------------------------------- /Screenshot.chop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caldwell/libanalogtv/HEAD/Screenshot.chop.png -------------------------------------------------------------------------------- /Screenshot.lode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caldwell/libanalogtv/HEAD/Screenshot.lode.png -------------------------------------------------------------------------------- /random.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 David Caldwell 2 | // 3 | // Permission to use, copy, modify, distribute, and sell this software and its 4 | // documentation for any purpose is hereby granted without fee, provided that 5 | // the above copyright notice appear in all copies and that both that 6 | // copyright notice and this permission notice appear in supporting 7 | // documentation. No representations are made about the suitability of this 8 | // software for any purpose. It is provided "as is" without express or 9 | // implied warranty. 10 | 11 | #ifndef __RANDOM_H___ 12 | #define __RANDOM_H___ 13 | 14 | #define frand(f) fabs((((double) rand()) * ((double) (f))) / ((double)RAND_MAX)) 15 | #define random() rand() 16 | 17 | #endif /* __RANDOM_H___ */ 18 | -------------------------------------------------------------------------------- /analogtv-apple2.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 David Caldwell 2 | // 3 | // Permission to use, copy, modify, distribute, and sell this software and its 4 | // documentation for any purpose is hereby granted without fee, provided that 5 | // the above copyright notice appear in all copies and that both that 6 | // copyright notice and this permission notice appear in supporting 7 | // documentation. No representations are made about the suitability of this 8 | // software for any purpose. It is provided "as is" without express or 9 | // implied warranty. 10 | 11 | #ifndef __ANALOGTV_APPLE2_H__ 12 | #define __ANALOGTV_APPLE2_H__ 13 | 14 | #include 15 | #include 16 | #include "analogtv.h" 17 | 18 | struct video_mode { 19 | bool graphics; 20 | bool hires; 21 | bool mixed; 22 | unsigned page; 23 | }; 24 | 25 | struct analogtv_apple2; 26 | 27 | struct analogtv_apple2 *analogtv_apple2_setup(unsigned width, unsigned height); 28 | void analogtv_apple2_reconfigure(struct analogtv_apple2 *a2context, unsigned width, unsigned height); 29 | struct framebuffer *analogtv_apple2_render(struct analogtv_apple2 *a2context, 30 | unsigned frame, 31 | unsigned frames__second, 32 | struct video_mode video_mode, 33 | uint8_t *ram); 34 | void analogtv_apple2_cleanup(struct analogtv_apple2 *a2context); 35 | 36 | #endif /* __ANALOGTV_APPLE2_H__ */ 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright © 2015 David Caldwell 2 | # 3 | # Permission to use, copy, modify, distribute, and sell this software and its 4 | # documentation for any purpose is hereby granted without fee, provided that 5 | # the above copyright notice appear in all copies and that both that 6 | # copyright notice and this permission notice appear in supporting 7 | # documentation. No representations are made about the suitability of this 8 | # software for any purpose. It is provided "as is" without express or 9 | # implied warranty. 10 | # 11 | # `make V=1` to enable verbose mode 12 | 13 | Q=$(if $V,,@) 14 | _CC:=$(CC) 15 | %.o: override CC=$(Q)printf "%15s %s\n" "Compiling" $<; $(_CC) 16 | _CXX:=$(CXX) 17 | %.o: override CXX=$(Q)printf "%15s %s\n" "Compiling" $<; $(_CXX) 18 | _FC:=$(FC) 19 | %.o: override FC=$(Q)printf "%15s %s\n" "Compiling" $<; $(_FC) 20 | _AS:=$(AS) 21 | %.o: override AS=$(Q)printf "%15s %s\n" "Assembling" $<; $(_AS) 22 | _AR:=$(AR) 23 | %.a: override AR=$(Q)printf "%15s %s\n" "Archiving" $@; $(_AR) 24 | 25 | all: analogtv-test 26 | 27 | CPPFLAGS += $(if $(PKGCONFIG), $$(pkg-config $(PKGCONFIG) --cflags)) 28 | LDLIBS += $(if $(PKGCONFIG), $$(pkg-config $(PKGCONFIG) --libs-only-l)) 29 | LDFLAGS += $(if $(PKGCONFIG), $$(pkg-config $(PKGCONFIG) --libs-only-L)) 30 | 31 | CFLAGS += -Wno-parentheses 32 | 33 | CFLAGS += -MMD 34 | -include *.d 35 | 36 | ANALOGTV_TEST_OBJS = analogtv-test.o thread.o analogtv.o analogtv-apple2.o 37 | analogtv-test: $(ANALOGTV_TEST_OBJS) 38 | analogtv-test $(ANALOGTV_TEST_OBJS): PKGCONFIG+=gdlib 39 | 40 | out-%.png: analogtv-test 41 | ./analogtv-test $(strip $(if $(findstring graphics,$*),1,0) \ 42 | $(if $(findstring hires,$*),1,0) \ 43 | $(if $(findstring mixed,$*),1,0) \ 44 | $(if $(findstring page,$*),1,0) \ 45 | $@) 46 | 47 | all: out-text.png 48 | all: out-text-page2.png 49 | all: out-graphics-lores.png 50 | all: out-graphics-lores-page2.png 51 | all: out-graphics-lores-mixed.png 52 | all: out-graphics-lores-mixed-page2.png 53 | all: out-graphics-hires.png 54 | all: out-graphics-hires-page2.png 55 | all: out-graphics-hires-mixed.png 56 | all: out-graphics-hires-mixed-page2.png 57 | 58 | clean: 59 | rm -f *.o *.d analog-test 60 | -------------------------------------------------------------------------------- /thread.c: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 David Caldwell 2 | 3 | // Adapted from xscreensaver's thread_util.c: 4 | // thread_util.c, Copyright (c) 2014 Dave Odell 5 | // 6 | // Permission to use, copy, modify, distribute, and sell this software and its 7 | // documentation for any purpose is hereby granted without fee, provided that 8 | // the above copyright notice appear in all copies and that both that 9 | // copyright notice and this permission notice appear in supporting 10 | // documentation. No representations are made about the suitability of this 11 | // software for any purpose. It is provided "as is" without express or 12 | // implied warranty. 13 | 14 | #include 15 | #include 16 | #include "thread.h" 17 | 18 | int threadpool_create(struct threadpool *self, const struct threadpool_class *cls, unsigned count) 19 | { 20 | self->count = count; 21 | 22 | assert(cls); 23 | 24 | self->thread_size = cls->size; 25 | self->thread_destroy = cls->destroy; 26 | 27 | { 28 | void *thread = malloc(cls->size * count); 29 | if(!thread) 30 | return ENOMEM; 31 | 32 | self->serial_threads = thread; 33 | 34 | for (unsigned i = 0; i != count; ++i) { 35 | int error = cls->create(thread, self, i); 36 | if(error) { 37 | self->count = i; 38 | threadpool_destroy(self); 39 | return error; 40 | } 41 | 42 | thread = (char *)thread + self->thread_size; 43 | } 44 | } 45 | return 0; 46 | } 47 | 48 | void threadpool_destroy(struct threadpool *self) 49 | { 50 | void *thread = self->serial_threads; 51 | for(unsigned i = 0; i != self->count; ++i) { 52 | self->thread_destroy(thread); 53 | thread = (char *)thread + self->thread_size; 54 | } 55 | 56 | free(self->serial_threads); 57 | } 58 | 59 | void threadpool_run(struct threadpool *self, void (*func)(void *)) 60 | { 61 | /* It's perfectly valid to move this to the beginning of threadpool_wait(). */ 62 | void *thread = self->serial_threads; 63 | for(unsigned i = 0; i != self->count; ++i) { 64 | func(thread); 65 | thread = (char *)thread + self->thread_size; 66 | } 67 | } 68 | 69 | void threadpool_wait(struct threadpool *self) 70 | { 71 | } 72 | 73 | unsigned thread_memory_alignment() 74 | { 75 | return sizeof(void *); 76 | } 77 | -------------------------------------------------------------------------------- /analogtv-test.c: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 David Caldwell 2 | // 3 | // Permission to use, copy, modify, distribute, and sell this software and its 4 | // documentation for any purpose is hereby granted without fee, provided that 5 | // the above copyright notice appear in all copies and that both that 6 | // copyright notice and this permission notice appear in supporting 7 | // documentation. No representations are made about the suitability of this 8 | // software for any purpose. It is provided "as is" without express or 9 | // implied warranty. 10 | 11 | #include "analogtv.h" 12 | #include "analogtv-apple2.h" 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | gdImagePtr gd_image_from_fb(struct framebuffer *fb) 20 | { 21 | gdImagePtr image = gdImageCreateTrueColor(fb->width, fb->height); 22 | uint8_t (*pix)[fb->height][fb->width*4] = fb->pixels; 23 | for (unsigned y=0; yheight; y++) 24 | for (unsigned x=0; xwidth; x++) 25 | image->tpixels[y][x] = (*pix)[y][x*4+2] << 24 | // gd uses ARGB in native word order 26 | (*pix)[y][x*4+0] << 16 | 27 | (*pix)[y][x*4+1] << 8 | 28 | (*pix)[y][x*4+2] << 0; 29 | return image; 30 | } 31 | 32 | void save_png(char *filename, gdImagePtr image) 33 | { 34 | char *data = NULL; 35 | FILE *out = fopen(filename, "wb"); 36 | if (!out) goto fail; 37 | int size; 38 | data = (char *) gdImagePngPtr(image, &size); 39 | if (!data) goto fail; 40 | if (fwrite(data, 1, size, out) != size) 41 | goto fail; 42 | fail: 43 | if (out) 44 | fclose(out); 45 | gdFree(data); 46 | } 47 | 48 | uint8_t text_screen_ram[] = { 49 | #include "text-screen.h" 50 | }; 51 | 52 | uint8_t hgr_screen_ram[] = { 53 | //#include "hgr-screen.lode.h" 54 | #include "hgr-screen.chop.h" 55 | }; 56 | 57 | int main(int argc, char **argv) 58 | { 59 | struct analogtv_apple2 *a2context = analogtv_apple2_setup(1280, 1024); 60 | 61 | struct video_mode video_mode = { .graphics=true, .hires=true, .mixed=false, .page=0 }; 62 | 63 | char *filename = "out.png"; 64 | if (argc >= 5) { 65 | video_mode.graphics = !!strtoul(argv[1], NULL, 0); 66 | video_mode.hires = !!strtoul(argv[2], NULL, 0); 67 | video_mode.mixed = !!strtoul(argv[3], NULL, 0); 68 | video_mode.page = !!strtoul(argv[4], NULL, 0); 69 | } 70 | if (argc >= 6) 71 | filename = argv[5]; 72 | 73 | printf("graphics=%d, hires=%d, mixed=%d, page=%d\n", video_mode.graphics, video_mode.hires, video_mode.mixed, video_mode.page); 74 | 75 | struct framebuffer *fb = analogtv_apple2_render(a2context, 76 | 10000, 77 | 30, 78 | video_mode, 79 | video_mode.hires ? hgr_screen_ram : text_screen_ram); 80 | 81 | save_png(filename, gd_image_from_fb(fb)); 82 | 83 | analogtv_apple2_cleanup(a2context); 84 | 85 | return 0; 86 | } 87 | 88 | -------------------------------------------------------------------------------- /thread.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 David Caldwell 2 | // 3 | // Adapted from xscreensaver's thread_util.h: 4 | // thread_util.h, Copyright (c) 2014 Dave Odell 5 | // 6 | // Permission to use, copy, modify, distribute, and sell this software and its 7 | // documentation for any purpose is hereby granted without fee, provided that 8 | // the above copyright notice appear in all copies and that both that 9 | // copyright notice and this permission notice appear in supporting 10 | // documentation. No representations are made about the suitability of this 11 | // software for any purpose. It is provided "as is" without express or 12 | // implied warranty. 13 | 14 | #ifndef __THREAD_H__ 15 | #define __THREAD_H__ 16 | 17 | #include 18 | #include // offsetof 19 | 20 | struct threadpool 21 | { 22 | /* This is always the same as the count parameter fed to threadpool_create(). 23 | Here's a neat trick: if the threadpool is zeroed out with a memset, and 24 | threadpool_create() is never called to create 0 threads, then 25 | threadpool::count can be used to determine if the threadpool object was 26 | ever initialized. */ 27 | unsigned count; 28 | 29 | /* Copied from threadpool_class. No need for thread_create here, though. */ 30 | size_t thread_size; 31 | void (*thread_destroy)(void *self); 32 | 33 | void *serial_threads; 34 | }; 35 | 36 | struct threadpool_class 37 | { 38 | /* Size of the thread private object. */ 39 | size_t size; 40 | 41 | /* Create the thread private object. Called in sequence for each thread 42 | (effectively) from threadpool_create. 43 | self: A pointer to size bytes of memory, allocated to hold the thread 44 | object. 45 | pool: The threadpool object that owns all the threads. If the threadpool 46 | is nested in another struct, try GET_PARENT_OBJ. 47 | id: The ID for the thread; numbering starts at zero and goes up by one 48 | for each thread. 49 | Return 0 on success. On failure, return a value from errno.h; this will 50 | be returned from threadpool_create. */ 51 | int (*create)(void *self, struct threadpool *pool, unsigned id); 52 | 53 | /* Destroys the thread private object. Called in sequence (though not always 54 | the same sequence as create). Warning: During shutdown, it is possible 55 | for destroy() to be called while other threads are still in 56 | threadpool_run(). */ 57 | void (*destroy)(void *self); 58 | }; 59 | 60 | int threadpool_create(struct threadpool *self, const struct threadpool_class *cls, unsigned count); 61 | void threadpool_destroy(struct threadpool *self); 62 | void threadpool_run(struct threadpool *self, void (*func)(void *)); 63 | void threadpool_wait(struct threadpool *self); 64 | unsigned thread_memory_alignment(); 65 | 66 | #define hardware_concurrency() 1 67 | 68 | #define thread_malloc(ptr, size) (!((*(ptr)) = malloc(size))) 69 | #define thread_free(ptr) free(ptr) 70 | /* 71 | If a variable 'member' is known to be a member (named 'member_name') of a 72 | struct (named 'struct_name'), then this can find a pointer to the struct 73 | that contains it. 74 | */ 75 | #define GET_PARENT_OBJ(struct_name, member_name, member) (struct_name *)((char *)member - offsetof(struct_name, member_name)); 76 | 77 | #endif /* __THREAD_H__ */ 78 | 79 | -------------------------------------------------------------------------------- /apple2+.charset.h: -------------------------------------------------------------------------------- 1 | // od -A n -t x1 apple2+.charset | perl -ne 'chomp; print join " ", map { "0x$_," } (split(/ +/))[1..16]; print "\n"' > apple2+.charset.js 2 | 3 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x08, 0x00, 4 | 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x14, 0x3e, 0x14, 0x3e, 0x14, 0x14, 0x00, 5 | 0x08, 0x3c, 0x0a, 0x1c, 0x28, 0x1e, 0x08, 0x00, 0x06, 0x26, 0x10, 0x08, 0x04, 0x32, 0x30, 0x00, 6 | 0x04, 0x0a, 0x0a, 0x04, 0x2a, 0x12, 0x2c, 0x00, 0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 7 | 0x10, 0x08, 0x04, 0x04, 0x04, 0x08, 0x10, 0x00, 0x04, 0x08, 0x10, 0x10, 0x10, 0x08, 0x04, 0x00, 8 | 0x08, 0x2a, 0x1c, 0x08, 0x1c, 0x2a, 0x08, 0x00, 0x00, 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00, 0x00, 9 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 10 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x20, 0x10, 0x08, 0x04, 0x02, 0x00, 0x00, 11 | 0x1c, 0x22, 0x32, 0x2a, 0x26, 0x22, 0x1c, 0x00, 0x08, 0x0c, 0x08, 0x08, 0x08, 0x08, 0x1c, 0x00, 12 | 0x1c, 0x22, 0x20, 0x18, 0x04, 0x02, 0x3e, 0x00, 0x3e, 0x20, 0x10, 0x18, 0x20, 0x22, 0x1c, 0x00, 13 | 0x10, 0x18, 0x14, 0x12, 0x3e, 0x10, 0x10, 0x00, 0x3e, 0x02, 0x1e, 0x20, 0x20, 0x22, 0x1c, 0x00, 14 | 0x38, 0x04, 0x02, 0x1e, 0x22, 0x22, 0x1c, 0x00, 0x3e, 0x20, 0x10, 0x08, 0x04, 0x04, 0x04, 0x00, 15 | 0x1c, 0x22, 0x22, 0x1c, 0x22, 0x22, 0x1c, 0x00, 0x1c, 0x22, 0x22, 0x3c, 0x20, 0x10, 0x0e, 0x00, 16 | 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x08, 0x04, 17 | 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x3e, 0x00, 0x00, 0x00, 18 | 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x1c, 0x22, 0x10, 0x08, 0x08, 0x00, 0x08, 0x00, 19 | 0x1c, 0x22, 0x2a, 0x3a, 0x1a, 0x02, 0x3c, 0x00, 0x08, 0x14, 0x22, 0x22, 0x3e, 0x22, 0x22, 0x00, 20 | 0x1e, 0x22, 0x22, 0x1e, 0x22, 0x22, 0x1e, 0x00, 0x1c, 0x22, 0x02, 0x02, 0x02, 0x22, 0x1c, 0x00, 21 | 0x1e, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1e, 0x00, 0x3e, 0x02, 0x02, 0x1e, 0x02, 0x02, 0x3e, 0x00, 22 | 0x3e, 0x02, 0x02, 0x1e, 0x02, 0x02, 0x02, 0x00, 0x3c, 0x02, 0x02, 0x02, 0x32, 0x22, 0x3c, 0x00, 23 | 0x22, 0x22, 0x22, 0x3e, 0x22, 0x22, 0x22, 0x00, 0x1c, 0x08, 0x08, 0x08, 0x08, 0x08, 0x1c, 0x00, 24 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x22, 0x1c, 0x00, 0x22, 0x12, 0x0a, 0x06, 0x0a, 0x12, 0x22, 0x00, 25 | 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x7e, 0x00, 0x22, 0x36, 0x2a, 0x2a, 0x22, 0x22, 0x22, 0x00, 26 | 0x22, 0x22, 0x26, 0x2a, 0x32, 0x22, 0x22, 0x00, 0x1c, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1c, 0x00, 27 | 0x1e, 0x22, 0x22, 0x1e, 0x02, 0x02, 0x02, 0x00, 0x1c, 0x22, 0x22, 0x22, 0x2a, 0x12, 0x2c, 0x00, 28 | 0x1e, 0x22, 0x22, 0x1e, 0x0a, 0x12, 0x22, 0x00, 0x1c, 0x22, 0x02, 0x1c, 0x20, 0x22, 0x1c, 0x00, 29 | 0x3e, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1c, 0x00, 30 | 0x22, 0x22, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x22, 0x22, 0x2a, 0x2a, 0x2a, 0x36, 0x22, 0x00, 31 | 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 32 | 0x3e, 0x20, 0x10, 0x08, 0x04, 0x02, 0x3e, 0x00, 0x3e, 0x06, 0x06, 0x06, 0x06, 0x06, 0x3e, 0x00, 33 | 0x00, 0x02, 0x04, 0x08, 0x10, 0x20, 0x00, 0x00, 0x3e, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3e, 0x00, 34 | 0x08, 0x14, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0xff, 0x00, 35 | 0x04, 0x08, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x20, 0x3c, 0x22, 0x3c, 0x00, 36 | 0x02, 0x02, 0x1a, 0x26, 0x22, 0x22, 0x1e, 0x00, 0x00, 0x00, 0x1c, 0x22, 0x02, 0x22, 0x1c, 0x00, 37 | 0x20, 0x20, 0x2c, 0x32, 0x22, 0x22, 0x3c, 0x00, 0x00, 0x00, 0x1c, 0x22, 0x3e, 0x02, 0x3c, 0x00, 38 | 0x18, 0x24, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x2c, 0x32, 0x22, 0x3c, 0x20, 0x1e, 39 | 0x02, 0x02, 0x1a, 0x26, 0x22, 0x22, 0x22, 0x00, 0x08, 0x00, 0x0c, 0x08, 0x08, 0x08, 0x1c, 0x00, 40 | 0x20, 0x00, 0x20, 0x20, 0x20, 0x20, 0x22, 0x1c, 0x02, 0x02, 0x12, 0x0a, 0x06, 0x0a, 0x12, 0x00, 41 | 0x0c, 0x08, 0x08, 0x08, 0x08, 0x08, 0x1c, 0x00, 0x00, 0x00, 0x16, 0x2a, 0x2a, 0x2a, 0x2a, 0x00, 42 | 0x00, 0x00, 0x1a, 0x26, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, 0x1c, 0x22, 0x22, 0x22, 0x1c, 0x00, 43 | 0x00, 0x00, 0x1e, 0x22, 0x22, 0x1e, 0x02, 0x02, 0x00, 0x00, 0x3c, 0x22, 0x22, 0x3c, 0x20, 0x20, 44 | 0x00, 0x00, 0x1a, 0x26, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x3c, 0x02, 0x1c, 0x20, 0x1e, 0x00, 45 | 0x04, 0x04, 0x1e, 0x04, 0x04, 0x24, 0x18, 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x32, 0x2c, 0x00, 46 | 0x00, 0x00, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00, 0x00, 0x2a, 0x2a, 0x2a, 0x2a, 0x14, 0x00, 47 | 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x3c, 0x20, 0x1e, 48 | 0x00, 0x00, 0x3e, 0x10, 0x08, 0x04, 0x3e, 0x00, 0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08, 0x00, 49 | 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x08, 0x10, 0x10, 0x20, 0x10, 0x10, 0x08, 0x00, 50 | 0x00, 0x26, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x14, 0x2a, 0x14, 0x2a, 0x00, 0x00 51 | -------------------------------------------------------------------------------- /analogtv.h: -------------------------------------------------------------------------------- 1 | /* analogtv, Copyright (c) 2003, 2004 Trevor Blackwell 2 | * 3 | * Permission to use, copy, modify, distribute, and sell this software and its 4 | * documentation for any purpose is hereby granted without fee, provided that 5 | * the above copyright notice appear in all copies and that both that 6 | * copyright notice and this permission notice appear in supporting 7 | * documentation. No representations are made about the suitability of this 8 | * software for any purpose. It is provided "as is" without express or 9 | * implied warranty. 10 | */ 11 | 12 | #ifndef _XSCREENSAVER_ANALOGTV_H 13 | #define _XSCREENSAVER_ANALOGTV_H 14 | 15 | #include 16 | #include "thread.h" 17 | 18 | /* 19 | You'll need these to generate standard NTSC TV signals 20 | */ 21 | enum { 22 | /* We don't handle interlace here */ 23 | ANALOGTV_V=262, 24 | ANALOGTV_TOP=30, 25 | ANALOGTV_VISLINES=200, 26 | ANALOGTV_BOT=ANALOGTV_TOP + ANALOGTV_VISLINES, 27 | 28 | /* This really defines our sampling rate, 4x the colorburst 29 | frequency. Handily equal to the Apple II's dot clock. 30 | You could also make a case for using 3x the colorburst freq, 31 | but 4x isn't hard to deal with. */ 32 | ANALOGTV_H=912, 33 | 34 | /* Each line is 63500 nS long. The sync pulse is 4700 nS long, etc. 35 | Define sync, back porch, colorburst, picture, and front porch 36 | positions */ 37 | ANALOGTV_SYNC_START=0, 38 | ANALOGTV_BP_START=4700*ANALOGTV_H/63500, 39 | ANALOGTV_CB_START=5800*ANALOGTV_H/63500, 40 | /* signal[row][ANALOGTV_PIC_START] is the first displayed pixel */ 41 | ANALOGTV_PIC_START=9400*ANALOGTV_H/63500, 42 | ANALOGTV_PIC_LEN=52600*ANALOGTV_H/63500, 43 | ANALOGTV_FP_START=62000*ANALOGTV_H/63500, 44 | ANALOGTV_PIC_END=ANALOGTV_FP_START, 45 | 46 | /* TVs scan past the edges of the picture tube, so normally you only 47 | want to use about the middle 3/4 of the nominal scan line. 48 | */ 49 | ANALOGTV_VIS_START=ANALOGTV_PIC_START + (ANALOGTV_PIC_LEN*1/8), 50 | ANALOGTV_VIS_END=ANALOGTV_PIC_START + (ANALOGTV_PIC_LEN*7/8), 51 | ANALOGTV_VIS_LEN=ANALOGTV_VIS_END-ANALOGTV_VIS_START, 52 | 53 | ANALOGTV_HASHNOISE_LEN=6, 54 | 55 | ANALOGTV_GHOSTFIR_LEN=4, 56 | 57 | /* analogtv.signal is in IRE units, as defined below: */ 58 | ANALOGTV_WHITE_LEVEL=100, 59 | ANALOGTV_GRAY50_LEVEL=55, 60 | ANALOGTV_GRAY30_LEVEL=35, 61 | ANALOGTV_BLACK_LEVEL=10, 62 | ANALOGTV_BLANK_LEVEL=0, 63 | ANALOGTV_SYNC_LEVEL=-40, 64 | ANALOGTV_CB_LEVEL=20, 65 | 66 | ANALOGTV_SIGNAL_LEN=ANALOGTV_V*ANALOGTV_H, 67 | 68 | /* The number of intensity levels we deal with for gamma correction &c */ 69 | ANALOGTV_CV_MAX=1024, 70 | 71 | /* MAX_LINEHEIGHT corresponds to 2400 vertical pixels, beyond which 72 | it interpolates extra black lines. */ 73 | ANALOGTV_MAX_LINEHEIGHT=12 74 | 75 | }; 76 | 77 | struct framebuffer { // bytewise RGBA 78 | unsigned width; 79 | unsigned bytes_per_line; 80 | unsigned height; 81 | void *pixels; 82 | }; 83 | 84 | struct framebuffer_driver { 85 | struct framebuffer *(*alloc)(unsigned width, unsigned height); 86 | void (*free)(struct framebuffer *framebuffer); 87 | }; 88 | 89 | typedef struct analogtv_input_s { 90 | signed char signal[ANALOGTV_V+1][ANALOGTV_H]; 91 | 92 | int do_teletext; 93 | 94 | /* for client use */ 95 | void (*updater)(struct analogtv_input_s *inp); 96 | void *client_data; 97 | double next_update_time; 98 | 99 | } analogtv_input; 100 | 101 | typedef struct analogtv_reception_s { 102 | 103 | analogtv_input *input; 104 | double ofs; 105 | double level; 106 | double multipath; 107 | double freqerr; 108 | 109 | double ghostfir[ANALOGTV_GHOSTFIR_LEN]; 110 | double ghostfir2[ANALOGTV_GHOSTFIR_LEN]; 111 | 112 | double hfloss; 113 | double hfloss2; 114 | 115 | } analogtv_reception; 116 | 117 | /* 118 | The rest of this should be considered mostly opaque to the analogtv module. 119 | */ 120 | 121 | struct analogtv_yiq_s { 122 | float y,i,q; 123 | } /*yiq[ANALOGTV_PIC_LEN+10] */; 124 | 125 | typedef struct analogtv_s { 126 | 127 | struct threadpool threads; 128 | 129 | float agclevel; 130 | 131 | /* If you change these, call analogtv_set_demod (which was a NOP and 132 | doesn't exist anymore, so you probably don't have to do anything) */ 133 | float color_control,brightness_control,contrast_control; 134 | float height_control, width_control, squish_control; 135 | float horiz_desync; 136 | float squeezebottom; 137 | float powerup; 138 | 139 | int usewidth,useheight,xrepl,subwidth; 140 | struct framebuffer *framebuffer; 141 | struct framebuffer_driver framebuffer_driver; 142 | int screen_xo,screen_yo; /* centers image in window */ 143 | 144 | int flutter_horiz_desync; 145 | 146 | #ifdef DEBUG 147 | struct timeval last_display_time; 148 | #endif 149 | 150 | /* Add hash (in the radio sense, not the programming sense.) These 151 | are the small white streaks that appear in quasi-regular patterns 152 | all over the screen when someone is running the vacuum cleaner or 153 | the blender. We also set shrinkpulse for one period which 154 | squishes the image horizontally to simulate the temporary line 155 | voltate drop when someone turns on a big motor */ 156 | double hashnoise_rpm; 157 | int hashnoise_counter; 158 | int hashnoise_times[ANALOGTV_V]; 159 | int hashnoise_signal[ANALOGTV_V]; 160 | int hashnoise_on; 161 | int hashnoise_enable; 162 | int shrinkpulse; 163 | 164 | float crtload[ANALOGTV_V]; 165 | 166 | unsigned int red_values[ANALOGTV_CV_MAX]; 167 | unsigned int green_values[ANALOGTV_CV_MAX]; 168 | unsigned int blue_values[ANALOGTV_CV_MAX]; 169 | unsigned int alpha_value; 170 | 171 | unsigned long colors[256]; 172 | int cmap_y_levels; 173 | int cmap_i_levels; 174 | int cmap_q_levels; 175 | 176 | int cur_hsync; 177 | int line_hsync[ANALOGTV_V]; 178 | int cur_vsync; 179 | double cb_phase[4]; 180 | double line_cb_phase[ANALOGTV_V][4]; 181 | 182 | int channel_change_cycles; 183 | double rx_signal_level; 184 | float *rx_signal; 185 | 186 | struct { 187 | int index; 188 | double value; 189 | } leveltable[ANALOGTV_MAX_LINEHEIGHT+1][ANALOGTV_MAX_LINEHEIGHT+1]; 190 | 191 | /* Only valid during draw. */ 192 | unsigned random0, random1; 193 | double noiselevel; 194 | const analogtv_reception *const *recs; 195 | unsigned rec_count; 196 | 197 | float *signal_subtotals; 198 | 199 | float puheight; 200 | } analogtv; 201 | 202 | analogtv *analogtv_allocate(int width, int height, struct framebuffer_driver framebuffer_driver); 203 | analogtv_input *analogtv_input_allocate(void); 204 | 205 | /* call if window size changes */ 206 | void analogtv_reconfigure(analogtv *it, int width, int height); 207 | 208 | void analogtv_set_defaults(analogtv *it); 209 | void analogtv_release(analogtv *it); 210 | void analogtv_setup_frame(analogtv *it); 211 | void analogtv_setup_sync(analogtv_input *input, int do_cb, int do_ssavi); 212 | void analogtv_draw(analogtv *it, double noiselevel, 213 | const analogtv_reception *const *recs, unsigned rec_count); 214 | 215 | void analogtv_reception_update(analogtv_reception *inp); 216 | 217 | 218 | #endif /* _XSCREENSAVER_ANALOGTV_H */ 219 | -------------------------------------------------------------------------------- /analogtv-apple2.c: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 David Caldwell 2 | // 3 | // Parts of the render code are: 4 | // Copyright © 2003, 2004 Trevor Blackwell 5 | // Copyright © 1998-2010 Jamie Zawinski 6 | // 7 | // Permission to use, copy, modify, distribute, and sell this software and its 8 | // documentation for any purpose is hereby granted without fee, provided that 9 | // the above copyright notice appear in all copies and that both that 10 | // copyright notice and this permission notice appear in supporting 11 | // documentation. No representations are made about the suitability of this 12 | // software for any purpose. It is provided "as is" without express or 13 | // implied warranty. 14 | 15 | 16 | #include "analogtv-apple2.h" 17 | #include "analogtv.h" 18 | #include 19 | 20 | static struct framebuffer *fb_alloc(unsigned width, unsigned height) 21 | { 22 | struct framebuffer *fb = calloc(sizeof(struct framebuffer), 1); 23 | fb->width = width; 24 | fb->height = height; 25 | fb->bytes_per_line = width * 4; 26 | fb->pixels = malloc(width*height*4); 27 | return fb; 28 | } 29 | 30 | static void fb_free(struct framebuffer *fb) 31 | { 32 | if (fb) { 33 | free(fb->pixels); 34 | free(fb); 35 | } 36 | } 37 | 38 | static unsigned text_line_offset(unsigned y) { 39 | return (y&7)<<7 | ((y&0x18)>>3) * 40; 40 | } 41 | 42 | static unsigned hgr_line_offset(unsigned y) { 43 | return (y&7)<<10 | text_line_offset(y>>3); 44 | } 45 | 46 | static uint8_t apple_2_plus_charset[] = { 47 | #include "apple2+.charset.h" 48 | }; 49 | 50 | struct analogtv_apple2 { 51 | analogtv *atv; 52 | analogtv_input *input; 53 | analogtv_reception reception; 54 | }; 55 | 56 | struct analogtv_apple2 *analogtv_apple2_setup(unsigned width, unsigned height) 57 | { 58 | struct analogtv_apple2 *a2context = calloc(sizeof(struct analogtv_apple2), 1); 59 | assert(a2context); 60 | 61 | a2context->atv = analogtv_allocate(width, height, (struct framebuffer_driver) { .alloc=fb_alloc, .free=fb_free }); 62 | assert(a2context->atv); 63 | 64 | a2context->input = analogtv_input_allocate(); 65 | assert(a2context->input); 66 | 67 | a2context->reception.input = a2context->input; 68 | a2context->reception.level = 1.0; 69 | 70 | analogtv_set_defaults(a2context->atv); 71 | a2context->atv->squish_control=0.05; 72 | 73 | return a2context; 74 | } 75 | 76 | void analogtv_apple2_reconfigure(struct analogtv_apple2 *a2context, unsigned width, unsigned height) 77 | { 78 | analogtv_reconfigure(a2context->atv, width, height); 79 | } 80 | 81 | struct framebuffer *analogtv_apple2_render(struct analogtv_apple2 *a2context, 82 | unsigned frame, 83 | unsigned frames__second, 84 | struct video_mode video_mode, 85 | uint8_t *ram) 86 | { 87 | a2context->atv->powerup = (float)frame / frames__second; 88 | analogtv_setup_sync(a2context->input, video_mode.graphics/*do_cb*/, false/*do_ssavi*/); 89 | 90 | analogtv_setup_frame(a2context->atv); 91 | 92 | float flash_period = 1.44/((12000+2*3300000)*.0000001); // A 555 timer running in astable mode. Formula from National's data sheet. Ra=12K Rb=3.3M C=.1uF 93 | flash_period /= 8; // hack--my calculations are incorrect and it runs way too slow. 94 | bool flash_state = (int)((float)frame / frames__second / flash_period) & 1; 95 | 96 | unsigned text_start = ((unsigned[]){0x0400, 0x0800})[video_mode.page]; // text or lo-res 97 | unsigned hgr_start = ((unsigned[]){0x2000, 0x4000})[video_mode.page]; // hi-res 98 | 99 | // Fill input 100 | for (unsigned textrow=0; textrow<24; textrow++) { 101 | for (unsigned row=textrow*8; rowinput->signal[row+ANALOGTV_TOP+4][ANALOGTV_PIC_START+100]; 110 | if (video_mode.graphics && !video_mode.hires && (row<160 || !video_mode.mixed)) { // lores 111 | for (int x=0; x<40; x++) { 112 | uint8_t c = ram[text_start + text_line_offset(textrow) + x]; 113 | uint8_t nib=c >> (((row/4)&1)*4) & 0xf; 114 | /* The low or high nybble was shifted out one bit at a time. */ 115 | for (unsigned i=0; i<14; i++) { 116 | *pp = (((nib>>((x*14+i)&3))&1) 117 | ?ANALOGTV_WHITE_LEVEL 118 | :ANALOGTV_BLACK_LEVEL); 119 | pp++; 120 | } 121 | } 122 | } else { // hires and text 123 | bool text = !video_mode.graphics || row>=160 && video_mode.mixed; 124 | unsigned row_addr = text ? text_start + text_line_offset(textrow) 125 | : hgr_start + hgr_line_offset(row); 126 | // printf("Row address %3d: %04x\n", row, row_addr); 127 | /* Emulate the mysterious pink line, due to a bit getting 128 | stuck in a shift register between the end of the last 129 | row and the beginning of this one. */ 130 | if ((ram[row_addr + 0] & 0x80) && 131 | (ram[row_addr + 39] & 0x40)) { 132 | pp[-1]=ANALOGTV_WHITE_LEVEL; 133 | } 134 | 135 | for (unsigned xs=0; xs<40; xs++) { 136 | uint8_t seg = ram[row_addr+xs]; 137 | if (text) { 138 | uint8_t ch = seg, y = row&7;; 139 | //seg = charset_scanline(seg, row&7); 140 | bool inverse = (ch & 0xc0) == 0 || // Inverse 141 | (ch & 0xc0) == 0x40 && flash_state; // Flash 142 | seg = apple_2_plus_charset[(ch & 0x3f ^ 0x20)*8 + y] ^ (inverse ? 0x7f : 0); 143 | } 144 | 145 | 146 | int shift=(seg&0x80)?1:0; // apple2.c has ?0:1 but that gives the wrong colors. 147 | 148 | /* Each of the low 7 bits in hires mode corresponded to 2 dot 149 | clocks, shifted by one if the high bit was set. */ 150 | for (unsigned i=0; i<7; i++) { 151 | pp[shift+1] = pp[shift] = (((seg>>i)&1) 152 | ?ANALOGTV_WHITE_LEVEL 153 | :ANALOGTV_BLACK_LEVEL); 154 | pp+=2; 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | const analogtv_reception *recs = &a2context->reception; // fake array of receptions 162 | analogtv_draw(a2context->atv, 0.02, &recs, 1); 163 | 164 | return a2context->atv->framebuffer; 165 | } 166 | 167 | void analogtv_apple2_cleanup(struct analogtv_apple2 *a2context) 168 | { 169 | analogtv_release(a2context->atv); 170 | free(a2context); 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AnalogTV 2 | ======== 3 | 4 | ### A composite CRT simulator library geared towards Apple 2 emulators 5 | 6 | 7 | What is this? 8 | ------------- 9 | 10 | This is Trevor Blackwell's excellent analog TV simulator ripped out of 11 | XScreenSaver, where it's been living for the past 10 years. All the X11 12 | vestiges have been shed and it's presented here as a stand-alone C library 13 | for use with Apple 2 Emulators, or anyone else who wants a fancy way to make 14 | your screen look like an analog monitor. 15 | 16 | What's that all mean, exactly? Here's a couple screenshots: 17 | 18 | ![Screenshot](https://github.com/caldwell/libanalogtv/raw/master/Screenshot.lode.png) 19 | ![Screenshot](https://github.com/caldwell/libanalogtv/raw/master/Screenshot.chop.png) 20 | 21 | 22 | Prerequisites 23 | ------------- 24 | 25 | The main code is self-contained. The `analogtv-test` program uses [libgd][1] 26 | to output .png images. 27 | 28 | [1]: http://libgd.bitbucket.org/ 29 | 30 | Using 31 | ----- 32 | 33 | The easiest way is to use the analogtv-apple2 interface: 34 | 35 | ```C 36 | struct analogtv_apple2 *a2context = analogtv_apple2_setup(1280, 720); 37 | 38 | uint8_t ram[0xC000]; 39 | 40 | struct framebuffer *fb = analogtv_apple2_render(a2context, 41 | 100 /*frame#*/, 42 | 30 /*fps*/, 43 | (struct video_mode video_mode) { 44 | .graphics=true, 45 | .hires=true, 46 | .mixed=false, 47 | .page=0 }, 48 | ram); 49 | analogtv_apple2_cleanup(a2context); 50 | ``` 51 | 52 | ### High level Apple2 emulator API 53 | 54 | #### analogtv_apple2_setup() 55 | 56 | ```C 57 | struct analogtv_apple2 *analogtv_apple2_setup(unsigned width, unsigned height); 58 | ``` 59 | 60 | Call this once to allocate and return an analogtv_apple2 context. `width` 61 | and `height` are the inital dimensions of the framebuffer. Currently it 62 | asserts on any erors. 63 | 64 | #### analogtv_apple2_reconfigure() 65 | 66 | ```C 67 | void analogtv_apple2_reconfigure(struct analogtv_apple2 *a2context, unsigned width, unsigned height); 68 | ``` 69 | 70 | Call this to resize the framebuffer of a given analogtv_apple2 context to 71 | a new `width` and `height`. 72 | 73 | #### analogtv_apple2_render 74 | 75 | ```C 76 | struct framebuffer *analogtv_apple2_render(struct analogtv_apple2 *a2context, 77 | unsigned frame, 78 | unsigned frames__second, 79 | struct video_mode video_mode, 80 | uint8_t *ram); 81 | ``` 82 | 83 | This function renders a frame into the framebuffer and returns a pointer to 84 | it. 85 | 86 | ##### Arguments: 87 | 88 | * `frame` 89 | 90 | The current frame number. See `frames__second` 91 | 92 | * `frame__second` 93 | 94 | Frames per second (read `__` as "over"). This is divided into `frame` 95 | to get the time elapsed which is used by the simulator and to 96 | control the "flash" interval. There's nothing magic about the 97 | relationship between `frames` and `frames__second`: If you don't 98 | have a set frames per second timer then just pass milliseconds into 99 | `frames` and `1000` into `frames__second`. 100 | 101 | * `video_mode` 102 | 103 | This structure has several fields that control the rendering process: 104 | 105 | * `.graphics` 106 | 107 | This `bool` controls whether the screen should be rendered in 108 | Text or Graphics mode. See Apple device address 0xc050 and 109 | 0xc051. 110 | 111 | * `.hires` 112 | 113 | This bool controls whether the screen should render Hires or Lores 114 | graphics. It has no meaning in Text mode. See Apple device 115 | address 0xc056 and 0xc057. 116 | 117 | * `.mixed` 118 | 119 | This bool controls whether the bottom 4 text lines of the screen 120 | should be displayed when in Graphics mode. It has no meaning in 121 | Text mode. See Apple device address 0xc052 and 0xc053. 122 | 123 | 124 | * `.page` 125 | 126 | This should be `0` to render page 1 Text or Graphics `1` to 127 | render page 2. See Apple device address 0xc054 and 0xc055. 128 | 129 | * `ram` 130 | 131 | This should be a pointer to the RAM image of the Apple 2 being 132 | emulated. The render function will only look at the appropriate 133 | place for the data to render (based on the settings in 134 | `video_mode`): 135 | 136 | * 0x0400-0x07FF for Page 1 of Text or Lores Graphics 137 | * 0x0800-0x0bFF for Page 2 of Text or Lores Graphics 138 | * 0x4000-0X5FFF for Page 2 of Hires Graphics 139 | * 0x2000-0x3FFF for Page 1 of Hires Graphics 140 | 141 | ##### Returns 142 | 143 | `analogtv_apple2_render` returns a `struct framebuffer *`: 144 | 145 | ```C 146 | struct framebuffer { 147 | unsigned width; 148 | unsigned bytes_per_line; 149 | unsigned height; 150 | void *pixels; // bytewise RGBA 151 | }; 152 | ``` 153 | 154 | * `width` and `height` 155 | 156 | The width and height of the framebuffer in pixels. This *may* be 157 | different from the width and height requested by `analogtv_apple2_setup` 158 | or `analogtv_apple2_reconfigure`—The analogtv code will choose new 159 | dimensions if the requested dimensions are too far outside the optimal 160 | aspect ratio (4:3 <= ratio <= 16:9), or if the dimensions are too small 161 | (minimum 266x200 pixels). 162 | 163 | * `bytes_per_line` 164 | 165 | The number of bytes to get from one row to the next. Currently this is 166 | always `width` * 4. 167 | 168 | * `pixels` 169 | 170 | A pointer to the pixel data. The data will be bytewise RGBA format. Ie: 171 | 172 | ((uint8_t*)pixels)[0] // red 173 | ((uint8_t*)pixels)[1] // green 174 | ((uint8_t*)pixels)[2] // blue 175 | ((uint8_t*)pixels)[3] // alpha 176 | 177 | #### analogtv_apple2_cleanup 178 | 179 | ```C 180 | void analogtv_apple2_cleanup(struct analogtv_apple2 *a2context); 181 | ``` 182 | 183 | Call this to deallocate an analogtv_apple2 context. 184 | 185 | ### Low level AnalogTV simulator API 186 | 187 | Undocumented for now. I don't actually understand a lot of it! Documentation 188 | patches welcome. :-) 189 | 190 | Credits 191 | ------- 192 | 193 | This code was taken from Jamie Zawinski's excellent [XScreenSaver][2], 194 | written originally by Trevor Blackwell in 2003. 195 | 196 | David Caldwell ripped all the X11 out of it and most of 197 | the XScreenSaver (couldn't bear to completely remove the threading module) 198 | and made it a standalone library in 2015. 199 | 200 | [2]: http://www.jwz.org/xscreensaver/ 201 | 202 | * Copyright © 2003, 2004 Trevor Blackwell 203 | * Copyright © 1998-2010 Jamie Zawinski 204 | * Copyright © 2003, 2004 Trevor Blackwell 205 | * Copyright © 2014 Dave Odell (thread code) 206 | * Copyright © 2015 David Caldwell 207 | 208 | License 209 | ------- 210 | 211 | Permission to use, copy, modify, distribute, and sell this software and its 212 | documentation for any purpose is hereby granted without fee, provided that 213 | the above copyright notice appear in all copies and that both that 214 | copyright notice and this permission notice appear in supporting 215 | documentation. No representations are made about the suitability of this 216 | software for any purpose. It is provided "as is" without express or 217 | implied warranty. 218 | -------------------------------------------------------------------------------- /analogtv.c: -------------------------------------------------------------------------------- 1 | /* analogtv, Copyright (c) 2003, 2004 Trevor Blackwell -*- c-basic-offset: 2; -*- 2 | * 3 | * Permission to use, copy, modify, distribute, and sell this software and its 4 | * documentation for any purpose is hereby granted without fee, provided that 5 | * the above copyright notice appear in all copies and that both that 6 | * copyright notice and this permission notice appear in supporting 7 | * documentation. No representations are made about the suitability of this 8 | * software for any purpose. It is provided "as is" without express or 9 | * implied warranty. 10 | */ 11 | 12 | /* 13 | 14 | This is the code for implementing something that looks like a conventional 15 | analog TV set. It simulates the following characteristics of standard 16 | televisions: 17 | 18 | - Realistic rendering of a composite video signal 19 | - Compression & brightening on the right, as the scan gets truncated 20 | because of saturation in the flyback transformer 21 | - Blooming of the picture dependent on brightness 22 | - Overscan, cutting off a few pixels on the left side. 23 | - Colored text in mixed graphics/text modes 24 | 25 | It's amazing how much it makes your high-end monitor look like at large 26 | late-70s TV. All you need is to put a big "Solid State" logo in curly script 27 | on it and you'd be set. 28 | 29 | In DirectColor or TrueColor modes, it generates pixel values 30 | directly from RGB values it calculates across each scan line. In 31 | PseudoColor mode, it consider each possible pattern of 5 preceding 32 | bit values in each possible position modulo 4 and allocates a color 33 | for each. A few things, like the brightening on the right side as 34 | the horizontal trace slows down, aren't done in PseudoColor. 35 | 36 | I originally wrote it for the Apple ][ emulator, and generalized it 37 | here for use with a rewrite of xteevee and possibly others. 38 | 39 | A maxim of technology is that failures reveal underlying mechanism. 40 | A good way to learn how something works is to push it to failure. 41 | The way it fails will usually tell you a lot about how it works. The 42 | corollary for this piece of software is that in order to emulate 43 | realistic failures of a TV set, it has to work just like a TV set. 44 | So there is lots of DSP-style emulation of analog circuitry for 45 | things like color decoding, H and V sync following, and more. In 46 | 2003, computers are just fast enough to do this at television signal 47 | rates. We use a 14 MHz sample rate here, so we can do on the order 48 | of a couple hundred instructions per sample and keep a good frame 49 | rate. 50 | 51 | Trevor Blackwell 52 | */ 53 | 54 | /* 55 | 2014-04-20, Dave Odell : 56 | API change: Folded analogtv_init_signal and *_add_signal into *_draw(). 57 | Added SMP support. 58 | Replaced doubles with floats, including constants and transcendental functions. 59 | Fixed a bug or two. 60 | */ 61 | 62 | #include 63 | 64 | #include 65 | #include 66 | #include 67 | #include 68 | #include 69 | #include "analogtv.h" 70 | #include "random.h" 71 | 72 | /* #define DEBUG 1 */ 73 | 74 | #ifdef DEBUG 75 | #include 76 | #endif 77 | 78 | #if defined(DEBUG) && (defined(__linux) || defined(__FreeBSD__)) 79 | /* only works on linux + freebsd */ 80 | #include 81 | 82 | #define DTIME_DECL u_int64_t dtimes[100]; int n_dtimes 83 | #define DTIME_START do {n_dtimes=0; dtimes[n_dtimes++]=rdtsc(); } while (0) 84 | #define DTIME dtimes[n_dtimes++]=rdtsc() 85 | #define DTIME_SHOW(DIV) \ 86 | do { \ 87 | double _dtime_div=(DIV); \ 88 | printf("time/%.1f: ",_dtime_div); \ 89 | for (i=1; ipowerup-start; 114 | float ret; 115 | if (pt<0.0f) return 0.0f; 116 | if (pt>900.0f || pt/tc>8.0f) return 1.0f; 117 | 118 | ret=(1.0f-expf(-pt/tc))*over; 119 | if (ret>1.0f) return 1.0f; 120 | return ret*ret; 121 | } 122 | 123 | /* 124 | There are actual standards for TV signals: NTSC and RS-170A describe the 125 | system used in the US and Japan. Europe has slightly different systems, but 126 | not different enough to make substantially different screensaver displays. 127 | Sadly, the standards bodies don't do anything so useful as publish the spec on 128 | the web. Best bets are: 129 | 130 | http://www.ee.washington.edu/conselec/CE/kuhn/ntsc/95x4.htm 131 | http://www.ntsc-tv.com/ntsc-index-02.htm 132 | 133 | In DirectColor or TrueColor modes, it generates pixel values directly from RGB 134 | values it calculates across each scan line. In PseudoColor mode, it consider 135 | each possible pattern of 5 preceding bit values in each possible position 136 | modulo 4 and allocates a color for each. A few things, like the brightening on 137 | the right side as the horizontal trace slows down, aren't done in PseudoColor. 138 | 139 | I'd like to add a bit of visible retrace, but it conflicts with being able to 140 | bitcopy the image when fast scrolling. After another couple of CPU 141 | generations, we could probably regenerate the whole image from scratch every 142 | time. On a P4 2 GHz it can manage this fine for blinking text, but scrolling 143 | looks too slow. 144 | */ 145 | 146 | /* localbyteorder is MSBFirst or LSBFirst */ 147 | #define LSBFirst 0 148 | #define MSBFirst 1 149 | static int localbyteorder; 150 | static const double float_low8_ofs=8388608.0; 151 | static int float_extraction_works; 152 | 153 | typedef union { 154 | float f; 155 | int i; 156 | } float_extract_t; 157 | 158 | static void 159 | analogtv_init(void) 160 | { 161 | int i; 162 | { 163 | unsigned int localbyteorder_loc = (MSBFirst<<24) | (LSBFirst<<0); 164 | localbyteorder=*(char *)&localbyteorder_loc; 165 | } 166 | 167 | if (1) { 168 | float_extract_t fe; 169 | int ans; 170 | 171 | float_extraction_works=1; 172 | for (i=0; i<256*4; i++) { 173 | fe.f=float_low8_ofs+(double)i; 174 | ans=fe.i&0x3ff; 175 | if (ans != i) { 176 | #ifdef DEBUG 177 | printf("Float extraction failed for %d => %d\n",i,ans); 178 | #endif 179 | float_extraction_works=0; 180 | break; 181 | } 182 | } 183 | } 184 | 185 | } 186 | 187 | void 188 | analogtv_set_defaults(analogtv *it) 189 | { 190 | it->color_control = 70/100.0; 191 | it->brightness_control = 2/100.0; 192 | it->contrast_control = 150/100.0; 193 | it->height_control = 1.0; 194 | it->width_control = 1.0; 195 | it->squish_control = 0.0; 196 | it->powerup=1000.0; 197 | 198 | it->hashnoise_rpm=0; 199 | it->hashnoise_on=0; 200 | it->hashnoise_enable=1; 201 | 202 | it->horiz_desync=frand(10.0)-5.0; 203 | it->squeezebottom=frand(5.0)-1.0; 204 | 205 | #ifdef DEBUG 206 | printf(" use: shm=%d cmap=%d color=%d\n", 207 | it->use_shm,it->use_cmap,it->use_color); 208 | printf(" controls: tint=%g color=%g brightness=%g contrast=%g\n", 209 | it->tint_control, it->color_control, it->brightness_control, 210 | it->contrast_control); 211 | printf(" desync: %g %d\n", 212 | it->horiz_desync, it->flutter_horiz_desync); 213 | printf(" hashnoise rpm: %g\n", 214 | it->hashnoise_rpm); 215 | printf(" size: %d %d %d %d xrepl=%d\n", 216 | it->usewidth, it->useheight, 217 | it->screen_xo, it->screen_yo, it->xrepl); 218 | 219 | printf(" ANALOGTV_V=%d\n",ANALOGTV_V); 220 | printf(" ANALOGTV_TOP=%d\n",ANALOGTV_TOP); 221 | printf(" ANALOGTV_VISLINES=%d\n",ANALOGTV_VISLINES); 222 | printf(" ANALOGTV_BOT=%d\n",ANALOGTV_BOT); 223 | printf(" ANALOGTV_H=%d\n",ANALOGTV_H); 224 | printf(" ANALOGTV_SYNC_START=%d\n",ANALOGTV_SYNC_START); 225 | printf(" ANALOGTV_BP_START=%d\n",ANALOGTV_BP_START); 226 | printf(" ANALOGTV_CB_START=%d\n",ANALOGTV_CB_START); 227 | printf(" ANALOGTV_PIC_START=%d\n",ANALOGTV_PIC_START); 228 | printf(" ANALOGTV_PIC_LEN=%d\n",ANALOGTV_PIC_LEN); 229 | printf(" ANALOGTV_FP_START=%d\n",ANALOGTV_FP_START); 230 | printf(" ANALOGTV_PIC_END=%d\n",ANALOGTV_PIC_END); 231 | printf(" ANALOGTV_HASHNOISE_LEN=%d\n",ANALOGTV_HASHNOISE_LEN); 232 | 233 | #endif 234 | 235 | } 236 | 237 | void 238 | analogtv_reconfigure(analogtv *it, int width, int height) 239 | { 240 | int oldwidth=it->usewidth; 241 | int oldheight=it->useheight; 242 | int wlim,hlim,height_diff; 243 | 244 | /* If the window is very small, don't let the image we draw get lower 245 | than the actual TV resolution (266x200.) 246 | 247 | If the aspect ratio of the window is close to a 4:3 or 16:9 ratio, 248 | then scale the image to exactly fill the window. 249 | 250 | Otherwise, center the image either horizontally or vertically, 251 | letterboxing or pillarboxing (but not both). 252 | 253 | If it's very close (2.5%) to a multiple of VISLINES, make it exact 254 | For example, it maps 1024 => 1000. 255 | */ 256 | float percent = 0.15; 257 | float min_ratio = 4.0 / 3.0 * (1 - percent); 258 | float max_ratio = 16.0 / 9.0 * (1 + percent); 259 | float ratio; 260 | float height_snap=0.025; 261 | 262 | hlim = height; 263 | wlim = width; 264 | ratio = wlim / (float) hlim; 265 | 266 | #ifdef USE_IPHONE 267 | /* Fill the whole iPhone screen, even though that distorts the image. */ 268 | min_ratio = 0; 269 | max_ratio = 10; 270 | #endif 271 | 272 | if (wlim < 266 || hlim < 200) 273 | { 274 | wlim = 266; 275 | hlim = 200; 276 | # ifdef DEBUG 277 | fprintf (stderr, 278 | "size: minimal: %dx%d in %dx%d (%.3f < %.3f < %.3f)\n", 279 | wlim, hlim, width, height, 280 | min_ratio, ratio, max_ratio); 281 | # endif 282 | } 283 | else if (ratio > min_ratio && ratio < max_ratio) 284 | { 285 | # ifdef DEBUG 286 | fprintf (stderr, 287 | "size: close enough: %dx%d (%.3f < %.3f < %.3f)\n", 288 | wlim, hlim, min_ratio, ratio, max_ratio); 289 | # endif 290 | } 291 | else if (ratio >= max_ratio) 292 | { 293 | wlim = hlim*max_ratio; 294 | # ifdef DEBUG 295 | fprintf (stderr, 296 | "size: center H: %dx%d in %dx%d (%.3f < %.3f < %.3f)\n", 297 | wlim, hlim, width, height, 298 | min_ratio, ratio, max_ratio); 299 | # endif 300 | } 301 | else /* ratio <= min_ratio */ 302 | { 303 | hlim = wlim/min_ratio; 304 | # ifdef DEBUG 305 | fprintf (stderr, 306 | "size: center V: %dx%d in %dx%d (%.3f < %.3f < %.3f)\n", 307 | wlim, hlim, width, height, 308 | min_ratio, ratio, max_ratio); 309 | # endif 310 | } 311 | 312 | 313 | height_diff = ((hlim + ANALOGTV_VISLINES/2) % ANALOGTV_VISLINES) - ANALOGTV_VISLINES/2; 314 | if (height_diff != 0 && abs(height_diff) < hlim * height_snap) 315 | { 316 | hlim -= height_diff; 317 | } 318 | 319 | 320 | /* Most times this doesn't change */ 321 | if (wlim != oldwidth || hlim != oldheight) { 322 | 323 | it->usewidth=wlim; 324 | it->useheight=hlim; 325 | 326 | it->xrepl=1+it->usewidth/640; 327 | if (it->xrepl>2) it->xrepl=2; 328 | it->subwidth=it->usewidth/it->xrepl; 329 | 330 | it->framebuffer_driver.free(it->framebuffer); 331 | it->framebuffer = it->framebuffer_driver.alloc(it->usewidth, it->useheight); 332 | } 333 | 334 | it->screen_xo = (width-it->usewidth)/2; 335 | it->screen_yo = (height-it->useheight)/2; 336 | } 337 | 338 | /* Can be any power-of-two <= 32. 16 a slightly better choice for 2-3 threads. */ 339 | #define ANALOGTV_SUBTOTAL_LEN 32 340 | 341 | typedef struct analogtv_thread_s 342 | { 343 | analogtv *it; 344 | unsigned thread_id; 345 | size_t signal_start, signal_end; 346 | } analogtv_thread; 347 | 348 | #define SIGNAL_OFFSET(thread_id) \ 349 | ((ANALOGTV_SIGNAL_LEN * (thread_id) / threads->count) & align) 350 | 351 | static int analogtv_thread_create(void *thread_raw, struct threadpool *threads, 352 | unsigned thread_id) 353 | { 354 | analogtv_thread *thread = (analogtv_thread *)thread_raw; 355 | unsigned align; 356 | 357 | thread->it = GET_PARENT_OBJ(analogtv, threads, threads); 358 | thread->thread_id = thread_id; 359 | 360 | align = thread_memory_alignment() / 361 | sizeof(thread->it->signal_subtotals[0]); 362 | if (!align) 363 | align = 1; 364 | align = ~(align * ANALOGTV_SUBTOTAL_LEN - 1); 365 | 366 | thread->signal_start = SIGNAL_OFFSET(thread_id); 367 | thread->signal_end = thread_id + 1 == threads->count ? 368 | ANALOGTV_SIGNAL_LEN : 369 | SIGNAL_OFFSET(thread_id + 1); 370 | 371 | return 0; 372 | } 373 | 374 | static void analogtv_thread_destroy(void *thread_raw) 375 | { 376 | } 377 | 378 | analogtv * 379 | analogtv_allocate(int width, int height, struct framebuffer_driver framebuffer_driver) 380 | { 381 | static const struct threadpool_class cls = { 382 | sizeof(analogtv_thread), 383 | analogtv_thread_create, 384 | analogtv_thread_destroy 385 | }; 386 | 387 | analogtv *it=NULL; 388 | int i; 389 | const size_t rx_signal_len = ANALOGTV_SIGNAL_LEN + 2*ANALOGTV_H; 390 | 391 | analogtv_init(); 392 | 393 | it=(analogtv *)calloc(1,sizeof(analogtv)); 394 | if (!it) return 0; 395 | it->threads.count=0; 396 | it->rx_signal=NULL; 397 | it->signal_subtotals=NULL; 398 | 399 | if (thread_malloc((void **)&it->rx_signal, 400 | sizeof(it->rx_signal[0]) * rx_signal_len)) 401 | goto fail; 402 | 403 | assert(!(ANALOGTV_SIGNAL_LEN % ANALOGTV_SUBTOTAL_LEN)); 404 | if (thread_malloc((void **)&it->signal_subtotals, 405 | sizeof(it->signal_subtotals[0]) * 406 | (rx_signal_len / ANALOGTV_SUBTOTAL_LEN))) 407 | goto fail; 408 | 409 | if (threadpool_create(&it->threads, &cls, hardware_concurrency())) 410 | goto fail; 411 | 412 | assert(it->threads.count); 413 | 414 | it->shrinkpulse=-1; 415 | 416 | { 417 | int red_invprec, red_shift; 418 | int green_invprec, green_shift; 419 | int blue_invprec, blue_shift; 420 | red_invprec = green_invprec = blue_invprec = 16 - 8; 421 | if (localbyteorder == MSBFirst) { 422 | red_shift = 24; 423 | green_shift = 16; 424 | blue_shift = 8; 425 | it->alpha_value = 0x000000ff; 426 | } else { 427 | red_shift = 0; 428 | green_shift = 8; 429 | blue_shift = 16; 430 | it->alpha_value = 0xff000000; 431 | } 432 | 433 | for (i=0; i65535) intensity=65535; 436 | it->red_values[i]=((intensity>>red_invprec)<green_values[i]=((intensity>>green_invprec)<blue_values[i]=((intensity>>blue_invprec)< %08x\n", it->red_values[0], it->red_values[ANALOGTV_CV_MAX-1]); 441 | // printf("green %08x -> %08x\n", it->green_values[0], it->green_values[ANALOGTV_CV_MAX-1]); 442 | // printf("blue %08x -> %08x\n", it->blue_values[0], it->blue_values[ANALOGTV_CV_MAX-1]); 443 | // printf("alpha %8s %08x\n", "", it->alpha_value); 444 | } 445 | 446 | it->framebuffer_driver = framebuffer_driver; 447 | analogtv_reconfigure(it, width, height); 448 | 449 | return it; 450 | 451 | fail: 452 | if (it) { 453 | if(it->threads.count) 454 | threadpool_destroy(&it->threads); 455 | thread_free(it->signal_subtotals); 456 | thread_free(it->rx_signal); 457 | free(it); 458 | } 459 | return NULL; 460 | } 461 | 462 | void 463 | analogtv_release(analogtv *it) 464 | { 465 | if (it->framebuffer) 466 | it->framebuffer_driver.free(it->framebuffer); 467 | threadpool_destroy(&it->threads); 468 | thread_free(it->rx_signal); 469 | thread_free(it->signal_subtotals); 470 | free(it); 471 | } 472 | 473 | 474 | /* 475 | First generate the I and Q reference signals, which we'll multiply 476 | the input signal by to accomplish the demodulation. Normally they 477 | are shifted 33 degrees from the colorburst. I think this was convenient 478 | for inductor-capacitor-vacuum tube implementation. 479 | 480 | The tint control, FWIW, just adds a phase shift to the chroma signal, 481 | and the color control controls the amplitude. 482 | 483 | In text modes (colormode==0) the system disabled the color burst, and no 484 | color was detected by the monitor. 485 | 486 | freq_error gives a mismatch between the built-in oscillator and the 487 | TV's colorbust. Some II Plus machines seemed to occasionally get 488 | instability problems -- the crystal oscillator was a single 489 | transistor if I remember correctly -- and the frequency would vary 490 | enough that the tint would change across the width of the screen. 491 | The left side would be in correct tint because it had just gotten 492 | resynchronized with the color burst. 493 | 494 | */ 495 | 496 | 497 | /* Here we model the analog circuitry of an NTSC television. 498 | Basically, it splits the signal into 3 signals: Y, I and Q. Y 499 | corresponds to luminance, and you get it by low-pass filtering the 500 | input signal to below 3.57 MHz. 501 | 502 | I and Q are the in-phase and quadrature components of the 3.57 MHz 503 | subcarrier. We get them by multiplying by cos(3.57 MHz*t) and 504 | sin(3.57 MHz*t), and low-pass filtering. Because the eye has less 505 | resolution in some colors than others, the I component gets 506 | low-pass filtered at 1.5 MHz and the Q at 0.5 MHz. The I component 507 | is approximately orange-blue, and Q is roughly purple-green. See 508 | http://www.ntsc-tv.com for details. 509 | 510 | We actually do an awful lot to the signal here. I suspect it would 511 | make sense to wrap them all up together by calculating impulse 512 | response and doing FFT convolutions. 513 | 514 | */ 515 | 516 | static void 517 | analogtv_ntsc_to_yiq(const analogtv *it, int lineno, const float *signal, 518 | int start, int end, struct analogtv_yiq_s *it_yiq) 519 | { 520 | enum {MAXDELAY=32}; 521 | int i; 522 | const float *sp; 523 | int phasecorr=(signal-it->rx_signal)&3; 524 | struct analogtv_yiq_s *yiq; 525 | int colormode; 526 | float agclevel=it->agclevel; 527 | float brightadd=it->brightness_control*100.0 - ANALOGTV_BLACK_LEVEL; 528 | float delay[MAXDELAY+ANALOGTV_PIC_LEN], *dp; 529 | float multiq2[4]; 530 | 531 | { 532 | 533 | double cb_i=(it->line_cb_phase[lineno][(2+phasecorr)&3]- 534 | it->line_cb_phase[lineno][(0+phasecorr)&3])/16.0; 535 | double cb_q=(it->line_cb_phase[lineno][(3+phasecorr)&3]- 536 | it->line_cb_phase[lineno][(1+phasecorr)&3])/16.0; 537 | 538 | colormode = (cb_i * cb_i + cb_q * cb_q) > 2.8; 539 | 540 | if (colormode) { 541 | double tint_i = -cos((103 + it->color_control)*3.1415926/180); 542 | double tint_q = sin((103 + it->color_control)*3.1415926/180); 543 | 544 | multiq2[0] = (cb_i*tint_i - cb_q*tint_q) * it->color_control; 545 | multiq2[1] = (cb_q*tint_i + cb_i*tint_q) * it->color_control; 546 | multiq2[2]=-multiq2[0]; 547 | multiq2[3]=-multiq2[1]; 548 | } 549 | } 550 | 551 | #if 0 552 | if (lineno==100) { 553 | printf("it->line_cb_phase = [%0.3f %0.3f %0.3f %0.3f]\n", 554 | it->line_cb_phase[lineno][0],it->line_cb_phase[lineno][1], 555 | it->line_cb_phase[lineno][2],it->line_cb_phase[lineno][3]); 556 | printf("multiq2 = [%0.3f %0.3f %0.3f %0.3f]\n", 557 | multiq2[0],multiq2[1],multiq2[2],multiq2[3]); 558 | } 559 | #endif 560 | 561 | dp=delay+ANALOGTV_PIC_LEN-MAXDELAY; 562 | for (i=0; i<5; i++) dp[i]=0.0f; 563 | 564 | assert(start>=0); 565 | assert(end < ANALOGTV_PIC_LEN+10); 566 | 567 | dp=delay+ANALOGTV_PIC_LEN-MAXDELAY; 568 | for (i=0; i<24; i++) dp[i]=0.0; 569 | for (i=start, yiq=it_yiq+start, sp=signal+start; 570 | iy = dp[8] + brightadd; 597 | } 598 | 599 | if (colormode) { 600 | dp=delay+ANALOGTV_PIC_LEN-MAXDELAY; 601 | for (i=0; i<27; i++) dp[i]=0.0; 602 | 603 | for (i=start, yiq=it_yiq+start, sp=signal+start; 604 | ii=dp[8] = (dp[5] + dp[0] 616 | +3.0f*(dp[4] + dp[1]) 617 | +4.0f*(dp[3] + dp[2]) 618 | -0.3333333333f * dp[10]); 619 | 620 | dp[16] = sig*multiq2[(i+3)&3] * 0.0833333333333f; 621 | yiq->q=dp[24] = (dp[16+5] + dp[16+0] 622 | +3.0f*(dp[16+4] + dp[16+1]) 623 | +4.0f*(dp[16+3] + dp[16+2]) 624 | -0.3333333333f * dp[24+2]); 625 | } 626 | } else { 627 | for (i=start, yiq=it_yiq+start; ii = yiq->q = 0.0f; 629 | } 630 | } 631 | } 632 | 633 | void 634 | analogtv_setup_teletext(analogtv_input *input) 635 | { 636 | int x,y; 637 | int teletext=ANALOGTV_BLACK_LEVEL; 638 | 639 | /* Teletext goes in line 21. But I suspect there are other things 640 | in the vertical retrace interval */ 641 | 642 | for (y=19; y<22; y++) { 643 | for (x=ANALOGTV_PIC_START; xsignal[y][x]=teletext; 648 | } 649 | } 650 | } 651 | 652 | void 653 | analogtv_setup_frame(analogtv *it) 654 | { 655 | int i,x,y; 656 | 657 | if (it->flutter_horiz_desync) { 658 | /* Horizontal sync during vertical sync instability. */ 659 | it->horiz_desync += -0.10*(it->horiz_desync-3.0) + 660 | ((int)(random()&0xff)-0x80) * 661 | ((int)(random()&0xff)-0x80) * 662 | ((int)(random()&0xff)-0x80) * 0.000001; 663 | } 664 | 665 | for (i=0; ihashnoise_times[i]=0; 667 | } 668 | 669 | if (it->hashnoise_enable && !it->hashnoise_on) { 670 | if (random()%10000==0) { 671 | it->hashnoise_on=1; 672 | it->shrinkpulse=random()%ANALOGTV_V; 673 | } 674 | } 675 | if (random()%1000==0) { 676 | it->hashnoise_on=0; 677 | } 678 | if (it->hashnoise_on) { 679 | it->hashnoise_rpm += (15000.0 - it->hashnoise_rpm)*0.05 + 680 | ((int)(random()%2000)-1000)*0.1; 681 | } else { 682 | it->hashnoise_rpm -= 100 + 0.01*it->hashnoise_rpm; 683 | if (it->hashnoise_rpm<0.0) it->hashnoise_rpm=0.0; 684 | } 685 | if (it->hashnoise_rpm > 0.0) { 686 | int hni; 687 | double hni_double; 688 | int hnc=it->hashnoise_counter; /* in 24.8 format */ 689 | 690 | /* Convert rpm of a 16-pole motor into dots in 24.8 format */ 691 | hni_double = ANALOGTV_V * ANALOGTV_H * 256.0 / 692 | (it->hashnoise_rpm * 16.0 / 60.0 / 60.0); 693 | hni = (hni_double <= INT_MAX) ? (int)hni_double : INT_MAX; 694 | 695 | while (hnc < (ANALOGTV_V * ANALOGTV_H)<<8) { 696 | y=(hnc>>8)/ANALOGTV_H; 697 | x=(hnc>>8)%ANALOGTV_H; 698 | 699 | if (x>0 && xhashnoise_times[y]=x; 701 | } 702 | /* hnc += hni + (int)(random()%65536)-32768; */ 703 | { 704 | hnc += (int)(random()%65536)-32768; 705 | if ((hnc >= 0) && (INT_MAX - hnc < hni)) break; 706 | hnc += hni; 707 | } 708 | } 709 | /* hnc -= (ANALOGTV_V * ANALOGTV_H)<<8;*/ 710 | } 711 | 712 | if (it->rx_signal_level != 0.0) 713 | it->agclevel = 1.0/it->rx_signal_level; 714 | 715 | 716 | #ifdef DEBUG2 717 | printf("filter: "); 718 | for (i=0; ighostfir[i]); 720 | } 721 | printf(" siglevel=%g agc=%g\n", siglevel, it->agclevel); 722 | #endif 723 | } 724 | 725 | void 726 | analogtv_setup_sync(analogtv_input *input, int do_cb, int do_ssavi) 727 | { 728 | int i,lineno,vsync; 729 | signed char *sig; 730 | 731 | int synclevel = do_ssavi ? ANALOGTV_WHITE_LEVEL : ANALOGTV_SYNC_LEVEL; 732 | 733 | for (lineno=0; lineno=3 && lineno<7; 735 | 736 | sig=input->signal[lineno]; 737 | 738 | i=ANALOGTV_SYNC_START; 739 | if (vsync) { 740 | while (icur_hsync; 763 | int cur_vsync=it->cur_vsync; 764 | int lineno = 0; 765 | int i,j; 766 | float osc,filt; 767 | float *sp; 768 | float cbfc=1.0f/128.0f; 769 | 770 | /* sp = it->rx_signal + lineno*ANALOGTV_H + cur_hsync;*/ 771 | for (i=-32; i<32; i++) { 772 | lineno = (cur_vsync + i + ANALOGTV_V) % ANALOGTV_V; 773 | sp = it->rx_signal + lineno*ANALOGTV_H; 774 | filt=0.0f; 775 | for (j=0; jagclevel; 779 | 780 | osc = (float)(ANALOGTV_V+i)/(float)ANALOGTV_V; 781 | 782 | if (osc >= 1.05f+0.0002f * filt) break; 783 | } 784 | cur_vsync = (cur_vsync + i + ANALOGTV_V) % ANALOGTV_V; 785 | 786 | for (lineno=0; lineno5 && linenorx_signal + lineno2*ANALOGTV_H + cur_hsync; 792 | for (i=-8; i<8; i++) { 793 | osc = (float)(ANALOGTV_H+i)/(float)ANALOGTV_H; 794 | filt=(sp[i-3]+sp[i-2]+sp[i-1]+sp[i]) * it->agclevel; 795 | 796 | if (osc >= 1.005f + 0.0001f*filt) break; 797 | } 798 | cur_hsync = (cur_hsync + i + ANALOGTV_H) % ANALOGTV_H; 799 | } 800 | 801 | it->line_hsync[lineno]=(cur_hsync + ANALOGTV_PIC_START + 802 | ANALOGTV_H) % ANALOGTV_H; 803 | 804 | /* Now look for the colorburst, which is a few cycles after the H 805 | sync pulse, and store its phase. 806 | The colorburst is 9 cycles long, and we look at the middle 5 807 | cycles. 808 | */ 809 | 810 | if (lineno>15) { 811 | sp = it->rx_signal + lineno*ANALOGTV_H + (cur_hsync&~3); 812 | for (i=ANALOGTV_CB_START+8; icb_phase[i&3] = it->cb_phase[i&3]*(1.0f-cbfc) + 814 | sp[i]*it->agclevel*cbfc; 815 | } 816 | } 817 | 818 | { 819 | float tot=0.1f; 820 | float cbgain; 821 | 822 | for (i=0; i<4; i++) { 823 | tot += it->cb_phase[i]*it->cb_phase[i]; 824 | } 825 | cbgain = 32.0f/sqrtf(tot); 826 | 827 | for (i=0; i<4; i++) { 828 | it->line_cb_phase[lineno][i]=it->cb_phase[i]*cbgain; 829 | } 830 | } 831 | 832 | #ifdef DEBUG 833 | if (0) printf("hs=%d cb=[%0.3f %0.3f %0.3f %0.3f]\n", 834 | cur_hsync, 835 | it->cb_phase[0], it->cb_phase[1], 836 | it->cb_phase[2], it->cb_phase[3]); 837 | #endif 838 | 839 | /* if (random()%2000==0) cur_hsync=random()%ANALOGTV_H; */ 840 | } 841 | 842 | it->cur_hsync = cur_hsync; 843 | it->cur_vsync = cur_vsync; 844 | } 845 | 846 | /* 847 | The point of this stuff is to ensure that when useheight is not a 848 | multiple of VISLINES so that TV scan lines map to different numbers 849 | of vertical screen pixels, the total brightness of each scan line 850 | remains the same. 851 | ANALOGTV_MAX_LINEHEIGHT corresponds to 2400 vertical pixels, beyond which 852 | it interpolates extra black lines. 853 | */ 854 | 855 | static void 856 | analogtv_setup_levels(analogtv *it, double avgheight) 857 | { 858 | int i,height; 859 | static const double levelfac[3]={-7.5, 5.5, 24.5}; 860 | 861 | for (height=0; heightleveltable[height][i].index = 2; 865 | } 866 | 867 | if (avgheight>=3) { 868 | it->leveltable[height][0].index=0; 869 | } 870 | if (avgheight>=5) { 871 | if (height >= 1) it->leveltable[height][height-1].index=0; 872 | } 873 | if (avgheight>=7) { 874 | it->leveltable[height][1].index=1; 875 | if (height >= 2) it->leveltable[height][height-2].index=1; 876 | } 877 | 878 | for (i=0; ileveltable[height][i].value = 880 | (40.0 + levelfac[it->leveltable[height][i].index]*puramp(it, 3.0, 6.0, 1.0)) / 256.0; 881 | } 882 | 883 | } 884 | } 885 | 886 | static void rnd_combine(unsigned *a0, unsigned *c0, unsigned a1, unsigned c1) 887 | { 888 | *a0 = (*a0 * a1) & 0xffffffffu; 889 | *c0 = (c1 + a1 * *c0) & 0xffffffffu; 890 | } 891 | 892 | static void rnd_seek_ac(unsigned *a, unsigned *c, unsigned dist) 893 | { 894 | unsigned int a1 = *a, c1 = *c; 895 | *a = 1, *c = 0; 896 | 897 | while(dist) 898 | { 899 | if(dist & 1) 900 | rnd_combine(a, c, a1, c1); 901 | dist >>= 1; 902 | rnd_combine(&a1, &c1, a1, c1); 903 | } 904 | } 905 | 906 | static unsigned int rnd_seek(unsigned a, unsigned c, unsigned rnd, unsigned dist) 907 | { 908 | rnd_seek_ac(&a, &c, dist); 909 | return a * rnd + c; 910 | } 911 | 912 | static void analogtv_init_signal(const analogtv *it, double noiselevel, unsigned start, unsigned end) 913 | { 914 | float *ps=it->rx_signal + start; 915 | float *pe=it->rx_signal + end; 916 | float *p=ps; 917 | unsigned int fastrnd=rnd_seek(FASTRND_A, FASTRND_C, it->random0, start); 918 | unsigned int fastrnd_offset; 919 | float nm1,nm2; 920 | float noisemul = sqrt(noiselevel*150)/(float)0x7fffffff; 921 | 922 | fastrnd_offset = fastrnd - 0x7fffffff; 923 | nm1 = (fastrnd_offset <= INT_MAX ? (int)fastrnd_offset : -1 - (int)(UINT_MAX - fastrnd_offset)) * noisemul; 924 | while (p != pe) { 925 | nm2=nm1; 926 | fastrnd = (fastrnd*FASTRND_A+FASTRND_C) & 0xffffffffu; 927 | fastrnd_offset = fastrnd - 0x7fffffff; 928 | nm1 = (fastrnd_offset <= INT_MAX ? (int)fastrnd_offset : -1 - (int)(UINT_MAX - fastrnd_offset)) * noisemul; 929 | *p++ = nm1*nm2; 930 | } 931 | } 932 | 933 | static void analogtv_add_signal(const analogtv *it, const analogtv_reception *rec, unsigned start, unsigned end, int ec) 934 | { 935 | analogtv_input *inp=rec->input; 936 | float *ps=it->rx_signal + start; 937 | float *pe=it->rx_signal + end; 938 | float *p=ps; 939 | signed char *ss=&inp->signal[0][0]; 940 | signed char *se=&inp->signal[0][0] + ANALOGTV_SIGNAL_LEN; 941 | signed char *s=ss + ((start + (unsigned)rec->ofs) % ANALOGTV_SIGNAL_LEN); 942 | signed char *s2; 943 | int i; 944 | float level=rec->level; 945 | float hfloss=rec->hfloss; 946 | unsigned int fastrnd=rnd_seek(FASTRND_A, FASTRND_C, it->random1, start); 947 | float dp[5]; 948 | 949 | const float noise_decay = 0.99995f; 950 | float noise_ampl = 1.3f * powf(noise_decay, start); 951 | 952 | if (ec > end) 953 | ec = end; 954 | 955 | /* assert((se-ss)%4==0 && (se-s)%4==0); */ 956 | 957 | for (i = start; i < ec; i++) { /* Sometimes start > ec. */ 958 | 959 | /* Do a big noisy transition. We can make the transition noise of 960 | high constant strength regardless of signal strength. 961 | 962 | There are two separate state machines. here, One is the noise 963 | process and the other is the 964 | 965 | We don't bother with the FIR filter here 966 | */ 967 | 968 | float sig0=(float)s[0]; 969 | unsigned int fastrnd_offset = fastrnd - 0x7fffffff; 970 | float noise = (fastrnd_offset <= INT_MAX ? (int)fastrnd_offset : -1 - (int)(UINT_MAX - fastrnd_offset)) * (50.0f/(float)0x7fffffff); 971 | fastrnd = (fastrnd*FASTRND_A+FASTRND_C) & 0xffffffffu; 972 | 973 | p[0] += sig0 * level * (1.0f - noise_ampl) + noise * noise_ampl; 974 | 975 | noise_ampl *= noise_decay; 976 | 977 | p++; 978 | s++; 979 | if (s>=se) s=ss; 980 | } 981 | 982 | dp[0]=0.0; 983 | s2 = s; 984 | for (i=1; i<5; i++) { 985 | s2 -= 4; 986 | if (s2 < ss) 987 | s2 += ANALOGTV_SIGNAL_LEN; 988 | dp[i] = (float)((int)s2[0]+(int)s2[1]+(int)s2[2]+(int)s2[3]); 989 | } 990 | 991 | assert(p <= pe); 992 | assert(!((pe - p) % 4)); 993 | while (p != pe) { 994 | float sig0,sig1,sig2,sig3,sigr; 995 | 996 | sig0=(float)s[0]; 997 | sig1=(float)s[1]; 998 | sig2=(float)s[2]; 999 | sig3=(float)s[3]; 1000 | 1001 | dp[0]=sig0+sig1+sig2+sig3; 1002 | 1003 | /* Get the video out signal, and add some ghosting, typical of RF 1004 | monitor cables. This corresponds to a pretty long cable, but 1005 | looks right to me. 1006 | */ 1007 | 1008 | sigr=(dp[1]*rec->ghostfir[0] + dp[2]*rec->ghostfir[1] + 1009 | dp[3]*rec->ghostfir[2] + dp[4]*rec->ghostfir[3]); 1010 | dp[4]=dp[3]; dp[3]=dp[2]; dp[2]=dp[1]; dp[1]=dp[0]; 1011 | 1012 | p[0] += (sig0+sigr + sig2*hfloss) * level; 1013 | p[1] += (sig1+sigr + sig3*hfloss) * level; 1014 | p[2] += (sig2+sigr + sig0*hfloss) * level; 1015 | p[3] += (sig3+sigr + sig1*hfloss) * level; 1016 | 1017 | p += 4; 1018 | s += 4; 1019 | if (s>=se) s = ss + (s-se); 1020 | } 1021 | 1022 | assert(p == pe); 1023 | } 1024 | 1025 | static void analogtv_thread_add_signals(void *thread_raw) 1026 | { 1027 | const analogtv_thread *thread = (analogtv_thread *)thread_raw; 1028 | const analogtv *it = thread->it; 1029 | unsigned i, j; 1030 | unsigned subtotal_end; 1031 | 1032 | unsigned start = thread->signal_start; 1033 | while(start != thread->signal_end) 1034 | { 1035 | float *p; 1036 | 1037 | /* Work on 8 KB blocks; these should fit in L1. */ 1038 | /* (Though it doesn't seem to help much on my system.) */ 1039 | unsigned end = start + 2048; 1040 | if(end > thread->signal_end) 1041 | end = thread->signal_end; 1042 | 1043 | analogtv_init_signal (it, it->noiselevel, start, end); 1044 | 1045 | for (i = 0; i != it->rec_count; ++i) { 1046 | analogtv_add_signal (it, it->recs[i], start, end, 1047 | !i ? it->channel_change_cycles : 0); 1048 | } 1049 | 1050 | assert (!(start % ANALOGTV_SUBTOTAL_LEN)); 1051 | assert (!(end % ANALOGTV_SUBTOTAL_LEN)); 1052 | 1053 | p = it->rx_signal + start; 1054 | subtotal_end = end / ANALOGTV_SUBTOTAL_LEN; 1055 | for (i = start / ANALOGTV_SUBTOTAL_LEN; i != subtotal_end; ++i) { 1056 | float sum = p[0]; 1057 | for (j = 1; j != ANALOGTV_SUBTOTAL_LEN; ++j) 1058 | sum += p[j]; 1059 | it->signal_subtotals[i] = sum; 1060 | p += ANALOGTV_SUBTOTAL_LEN; 1061 | } 1062 | 1063 | start = end; 1064 | } 1065 | } 1066 | 1067 | static int analogtv_get_line(const analogtv *it, int lineno, int *slineno, 1068 | int *ytop, int *ybot, unsigned *signal_offset) 1069 | { 1070 | *slineno=lineno-ANALOGTV_TOP; 1071 | *ytop=(int)((*slineno*it->useheight/ANALOGTV_VISLINES - 1072 | it->useheight/2)*it->puheight) + it->useheight/2; 1073 | *ybot=(int)(((*slineno+1)*it->useheight/ANALOGTV_VISLINES - 1074 | it->useheight/2)*it->puheight) + it->useheight/2; 1075 | #if 0 1076 | int linesig=analogtv_line_signature(input,lineno) 1077 | + it->hashnoise_times[lineno]; 1078 | #endif 1079 | *signal_offset = ((lineno+it->cur_vsync+ANALOGTV_V) % ANALOGTV_V) * ANALOGTV_H + 1080 | it->line_hsync[lineno]; 1081 | 1082 | if (*ytop==*ybot) return 0; 1083 | if (*ybot<0 || *ytop>it->useheight) return 0; 1084 | if (*ytop<0) *ytop=0; 1085 | if (*ybot>it->useheight) *ybot=it->useheight; 1086 | 1087 | if (*ybot > *ytop+ANALOGTV_MAX_LINEHEIGHT) *ybot=*ytop+ANALOGTV_MAX_LINEHEIGHT; 1088 | return 1; 1089 | } 1090 | 1091 | static void 1092 | analogtv_blast_imagerow(const analogtv *it, 1093 | float *rgbf, float *rgbf_end, 1094 | int ytop, int ybot) 1095 | { 1096 | int i,y; 1097 | float *rpf; 1098 | char *level_copyfrom[3]; 1099 | int xrepl=it->xrepl; 1100 | unsigned lineheight = ybot - ytop; 1101 | if (lineheight > ANALOGTV_MAX_LINEHEIGHT) lineheight = ANALOGTV_MAX_LINEHEIGHT; 1102 | for (i=0; i<3; i++) level_copyfrom[i]=NULL; 1103 | 1104 | for (y=ytop; yframebuffer->pixels + y*it->framebuffer->bytes_per_line; 1106 | unsigned line = y-ytop; 1107 | 1108 | int level=it->leveltable[lineheight][line].index; 1109 | float levelmult=it->leveltable[lineheight][line].value; 1110 | 1111 | /* Fast special cases to avoid the slow XPutPixel. Ugh. It goes to show 1112 | why standard graphics sw has to be fast, or else people will have to 1113 | work around it and risk incompatibility. The quickdraw folks 1114 | understood this. The other answer would be for X11 to have fewer 1115 | formats for bitm.. oh, never mind. If neither of these cases work 1116 | (they probably cover 99% of setups) it falls back on the Xlib 1117 | routines. */ 1118 | 1119 | if (level_copyfrom[level]) { 1120 | memcpy(rowdata, level_copyfrom[level], it->framebuffer->bytes_per_line); 1121 | } 1122 | else { 1123 | level_copyfrom[level] = rowdata; 1124 | 1125 | if (1/* all we support is 32 bit RGBA format */) { 1126 | /* int is more likely to be 32 bits than long */ 1127 | unsigned int *pixelptr=(unsigned int *)rowdata; 1128 | unsigned int pix; 1129 | 1130 | for (rpf=rgbf; rpf!=rgbf_end; rpf+=3) { 1131 | int ntscri=rpf[0]*levelmult; 1132 | int ntscgi=rpf[1]*levelmult; 1133 | int ntscbi=rpf[2]*levelmult; 1134 | if (ntscri>=ANALOGTV_CV_MAX) ntscri=ANALOGTV_CV_MAX-1; 1135 | if (ntscgi>=ANALOGTV_CV_MAX) ntscgi=ANALOGTV_CV_MAX-1; 1136 | if (ntscbi>=ANALOGTV_CV_MAX) ntscbi=ANALOGTV_CV_MAX-1; 1137 | pix = (it->red_values[ntscri] | 1138 | it->green_values[ntscgi] | 1139 | it->blue_values[ntscbi] | 1140 | it->alpha_value); 1141 | pixelptr[0] = pix; 1142 | if (xrepl>=2) { 1143 | pixelptr[1] = pix; 1144 | if (xrepl>=3) pixelptr[2] = pix; 1145 | } 1146 | pixelptr+=xrepl; 1147 | } 1148 | } 1149 | } 1150 | } 1151 | } 1152 | 1153 | static void analogtv_thread_draw_lines(void *thread_raw) 1154 | { 1155 | const analogtv_thread *thread = (analogtv_thread *)thread_raw; 1156 | const analogtv *it = thread->it; 1157 | 1158 | int lineno; 1159 | 1160 | float *raw_rgb_start; 1161 | float *raw_rgb_end; 1162 | raw_rgb_start=(float *)calloc(it->subwidth*3, sizeof(float)); 1163 | 1164 | if (! raw_rgb_start) return; 1165 | 1166 | raw_rgb_end=raw_rgb_start+3*it->subwidth; 1167 | 1168 | for (lineno=ANALOGTV_TOP + thread->thread_id; 1169 | linenothreads.count) { 1171 | int i; 1172 | 1173 | int slineno, ytop, ybot; 1174 | unsigned signal_offset; 1175 | 1176 | const float *signal; 1177 | 1178 | int scanstart_i,scanend_i,squishright_i,squishdiv,pixrate; 1179 | float *rgb_start, *rgb_end; 1180 | float pixbright; 1181 | int pixmultinc; 1182 | 1183 | float *rrp; 1184 | 1185 | struct analogtv_yiq_s yiq[ANALOGTV_PIC_LEN+10]; 1186 | 1187 | if (! analogtv_get_line(it, lineno, &slineno, &ytop, &ybot, 1188 | &signal_offset)) 1189 | continue; 1190 | 1191 | signal = it->rx_signal + signal_offset; 1192 | 1193 | { 1194 | 1195 | float bloomthisrow,shiftthisrow; 1196 | float viswidth,middle; 1197 | float scanwidth; 1198 | int scw,scl,scr; 1199 | 1200 | bloomthisrow = -10.0f * it->crtload[lineno]; 1201 | if (bloomthisrow<-10.0f) bloomthisrow=-10.0f; 1202 | if (bloomthisrow>2.0f) bloomthisrow=2.0f; 1203 | if (slineno<16) { 1204 | shiftthisrow=it->horiz_desync * (expf(-0.17f*slineno) * 1205 | (0.7f+cosf(slineno*0.6f))); 1206 | } else { 1207 | shiftthisrow=0.0f; 1208 | } 1209 | 1210 | viswidth=ANALOGTV_PIC_LEN * 0.79f - 5.0f*bloomthisrow; 1211 | middle=ANALOGTV_PIC_LEN/2 - shiftthisrow; 1212 | 1213 | scanwidth=it->width_control * puramp(it, 0.5f, 0.3f, 1.0f); 1214 | 1215 | scw=it->subwidth*scanwidth; 1216 | if (scw>it->subwidth) scw=it->usewidth; 1217 | scl=it->subwidth/2 - scw/2; 1218 | scr=it->subwidth/2 + scw/2; 1219 | 1220 | pixrate=(int)((viswidth*65536.0f*1.0f)/it->subwidth)/scanwidth; 1221 | scanstart_i=(int)((middle-viswidth*0.5f)*65536.0f); 1222 | scanend_i=(ANALOGTV_PIC_LEN-1)*65536; 1223 | squishright_i=(int)((middle+viswidth*(0.25f + 0.25f*puramp(it, 2.0f, 0.0f, 1.1f) 1224 | - it->squish_control)) *65536.0f); 1225 | squishdiv=it->subwidth/15; 1226 | 1227 | rgb_start=raw_rgb_start+scl*3; 1228 | rgb_end=raw_rgb_start+scr*3; 1229 | 1230 | assert(scanstart_i>=0); 1231 | 1232 | #ifdef DEBUG 1233 | if (0) printf("scan %d: %0.3f %0.3f %0.3f scl=%d scr=%d scw=%d\n", 1234 | lineno, 1235 | scanstart_i/65536.0f, 1236 | squishright_i/65536.0f, 1237 | scanend_i/65536.0f, 1238 | scl,scr,scw); 1239 | #endif 1240 | } 1241 | 1242 | { 1243 | analogtv_ntsc_to_yiq(it, lineno, signal, 1244 | (scanstart_i>>16)-10, (scanend_i>>16)+10, yiq); 1245 | 1246 | pixbright=it->contrast_control * puramp(it, 1.0f, 0.0f, 1.0f) 1247 | / (0.5f+0.5f*it->puheight) * 1024.0f/100.0f; 1248 | pixmultinc=pixrate; 1249 | i=scanstart_i; rrp=rgb_start; 1250 | while (i<0 && rrp!=rgb_end) { 1251 | rrp[0]=rrp[1]=rrp[2]=0; 1252 | i+=pixmultinc; 1253 | rrp+=3; 1254 | } 1255 | while (i>16; 1259 | float r,g,b; 1260 | 1261 | float interpy=(yiq[pati].y*invpixfrac + yiq[pati+1].y*pixfrac); 1262 | float interpi=(yiq[pati].i*invpixfrac + yiq[pati+1].i*pixfrac); 1263 | float interpq=(yiq[pati].q*invpixfrac + yiq[pati+1].q*pixfrac); 1264 | 1265 | /* 1266 | According to the NTSC spec, Y,I,Q are generated as: 1267 | 1268 | y=0.30 r + 0.59 g + 0.11 b 1269 | i=0.60 r - 0.28 g - 0.32 b 1270 | q=0.21 r - 0.52 g + 0.31 b 1271 | 1272 | So if you invert the implied 3x3 matrix you get what standard 1273 | televisions implement with a bunch of resistors (or directly in the 1274 | CRT -- don't ask): 1275 | 1276 | r = y + 0.948 i + 0.624 q 1277 | g = y - 0.276 i - 0.639 q 1278 | b = y - 1.105 i + 1.729 q 1279 | */ 1280 | 1281 | r=(interpy + 0.948f*interpi + 0.624f*interpq) * pixbright; 1282 | g=(interpy - 0.276f*interpi - 0.639f*interpq) * pixbright; 1283 | b=(interpy - 1.105f*interpi + 1.729f*interpq) * pixbright; 1284 | if (r<0.0f) r=0.0f; 1285 | if (g<0.0f) g=0.0f; 1286 | if (b<0.0f) b=0.0f; 1287 | rrp[0]=r; 1288 | rrp[1]=g; 1289 | rrp[2]=b; 1290 | 1291 | if (i>=squishright_i) { 1292 | pixmultinc += pixmultinc/squishdiv; 1293 | pixbright += pixbright/squishdiv/2; 1294 | } 1295 | i+=pixmultinc; 1296 | rrp+=3; 1297 | } 1298 | while (rrp != rgb_end) { 1299 | rrp[0]=rrp[1]=rrp[2]=0.0f; 1300 | rrp+=3; 1301 | } 1302 | 1303 | analogtv_blast_imagerow(it, raw_rgb_start, raw_rgb_end, 1304 | ytop,ybot); 1305 | } 1306 | } 1307 | 1308 | free(raw_rgb_start); 1309 | } 1310 | 1311 | void 1312 | analogtv_draw(analogtv *it, double noiselevel, 1313 | const analogtv_reception *const *recs, unsigned rec_count) 1314 | { 1315 | int i,lineno; 1316 | int /*bigloadchange,*/drawcount; 1317 | double baseload; 1318 | 1319 | it->rx_signal_level = noiselevel; 1320 | for (i = 0; i != rec_count; ++i) { 1321 | const analogtv_reception *rec = recs[i]; 1322 | double level = rec->level; 1323 | analogtv_input *inp=rec->input; 1324 | 1325 | it->rx_signal_level = 1326 | sqrt(it->rx_signal_level * it->rx_signal_level + 1327 | (level * level * (1.0 + 4.0*(rec->ghostfir[0] + rec->ghostfir[1] + 1328 | rec->ghostfir[2] + rec->ghostfir[3])))); 1329 | 1330 | /* duplicate the first line into the Nth line to ease wraparound computation */ 1331 | memcpy(inp->signal[ANALOGTV_V], inp->signal[0], 1332 | ANALOGTV_H * sizeof(inp->signal[0][0])); 1333 | } 1334 | 1335 | analogtv_setup_frame(it); 1336 | 1337 | it->random0 = random(); 1338 | it->random1 = random(); 1339 | it->noiselevel = noiselevel; 1340 | it->recs = recs; 1341 | it->rec_count = rec_count; 1342 | threadpool_run(&it->threads, analogtv_thread_add_signals); 1343 | threadpool_wait(&it->threads); 1344 | 1345 | it->channel_change_cycles=0; 1346 | 1347 | /* rx_signal has an extra 2 lines at the end, where we copy the 1348 | first 2 lines so we can index into it while only worrying about 1349 | wraparound on a per-line level */ 1350 | memcpy(&it->rx_signal[ANALOGTV_SIGNAL_LEN], 1351 | &it->rx_signal[0], 1352 | 2*ANALOGTV_H*sizeof(it->rx_signal[0])); 1353 | 1354 | /* Repeat for signal_subtotals. */ 1355 | 1356 | memcpy(&it->signal_subtotals[ANALOGTV_SIGNAL_LEN / ANALOGTV_SUBTOTAL_LEN], 1357 | &it->signal_subtotals[0], 1358 | (2*ANALOGTV_H/ANALOGTV_SUBTOTAL_LEN)*sizeof(it->signal_subtotals[0])); 1359 | 1360 | analogtv_sync(it); /* Requires the add_signals be complete. */ 1361 | 1362 | baseload=0.5; 1363 | /* if (it->hashnoise_on) baseload=0.5; */ 1364 | 1365 | /*bigloadchange=1;*/ 1366 | drawcount=0; 1367 | it->crtload[ANALOGTV_TOP-1]=baseload; 1368 | it->puheight = puramp(it, 2.0, 1.0, 1.3) * it->height_control * 1369 | (1.125 - 0.125*puramp(it, 2.0, 2.0, 1.1)); 1370 | 1371 | analogtv_setup_levels(it, it->puheight * (double)it->useheight/(double)ANALOGTV_VISLINES); 1372 | 1373 | for (lineno=ANALOGTV_TOP; linenoshrinkpulse) { 1380 | baseload += 0.4; 1381 | /*bigloadchange=1;*/ 1382 | it->shrinkpulse=-1; 1383 | } 1384 | 1385 | #if 0 1386 | if (it->hashnoise_rpm>0.0 && 1387 | !(bigloadchange || 1388 | (slineno<20 && it->flutter_horiz_desync) || 1389 | it->gaussiannoise_level>30 || 1390 | ((it->gaussiannoise_level>2.0 || 1391 | it->multipath) && random()%4) || 1392 | linesig != it->onscreen_signature[lineno])) { 1393 | continue; 1394 | } 1395 | it->onscreen_signature[lineno] = linesig; 1396 | #endif 1397 | drawcount++; 1398 | 1399 | /* 1400 | Interpolate the 600-dotclock line into however many horizontal 1401 | screen pixels we're using, and convert to RGB. 1402 | 1403 | We add some 'bloom', variations in the horizontal scan width with 1404 | the amount of brightness, extremely common on period TV sets. They 1405 | had a single oscillator which generated both the horizontal scan and 1406 | (during the horizontal retrace interval) the high voltage for the 1407 | electron beam. More brightness meant more load on the oscillator, 1408 | which caused an decrease in horizontal deflection. Look for 1409 | (bloomthisrow). 1410 | 1411 | Also, the A2 did a bad job of generating horizontal sync pulses 1412 | during the vertical blanking interval. This, and the fact that the 1413 | horizontal frequency was a bit off meant that TVs usually went a bit 1414 | out of sync during the vertical retrace, and the top of the screen 1415 | would be bent a bit to the left or right. Look for (shiftthisrow). 1416 | 1417 | We also add a teeny bit of left overscan, just enough to be 1418 | annoying, but you can still read the left column of text. 1419 | 1420 | We also simulate compression & brightening on the right side of the 1421 | screen. Most TVs do this, but you don't notice because they overscan 1422 | so it's off the right edge of the CRT. But the A2 video system used 1423 | so much of the horizontal scan line that you had to crank the 1424 | horizontal width down in order to not lose the right few characters, 1425 | and you'd see the compression on the right edge. Associated with 1426 | compression is brightening; since the electron beam was scanning 1427 | slower, the same drive signal hit the phosphor harder. Look for 1428 | (squishright_i) and (squishdiv). 1429 | */ 1430 | 1431 | { 1432 | /* This used to be an int, I suspect by mistake. - Dave */ 1433 | float totsignal=0; 1434 | float ncl/*,diff*/; 1435 | unsigned frac; 1436 | size_t end0, end1; 1437 | const float *p; 1438 | 1439 | frac = signal_offset & (ANALOGTV_SUBTOTAL_LEN - 1); 1440 | p = it->rx_signal + (signal_offset & ~(ANALOGTV_SUBTOTAL_LEN - 1)); 1441 | for (i=0; i != frac; i++) { 1442 | totsignal -= p[i]; 1443 | } 1444 | 1445 | end0 = (signal_offset + ANALOGTV_PIC_LEN); 1446 | 1447 | end1 = end0 / ANALOGTV_SUBTOTAL_LEN; 1448 | for (i=signal_offset / ANALOGTV_SUBTOTAL_LEN; isignal_subtotals[i]; 1450 | } 1451 | 1452 | frac = end0 & (ANALOGTV_SUBTOTAL_LEN - 1); 1453 | p = it->rx_signal + (end0 & ~(ANALOGTV_SUBTOTAL_LEN - 1)); 1454 | for (i=0; i != frac; i++) { 1455 | totsignal += p[i]; 1456 | } 1457 | 1458 | totsignal *= it->agclevel; 1459 | ncl = 0.95f * it->crtload[lineno-1] + 1460 | 0.05f*(baseload + 1461 | (totsignal-30000)/100000.0f + 1462 | (slineno>184 ? (slineno-184)*(lineno-184)*0.001f * it->squeezebottom 1463 | : 0.0f)); 1464 | /*diff=ncl - it->crtload[lineno];*/ 1465 | /*bigloadchange = (diff>0.01 || diff<-0.01);*/ 1466 | it->crtload[lineno]=ncl; 1467 | } 1468 | } 1469 | 1470 | threadpool_run(&it->threads, analogtv_thread_draw_lines); 1471 | threadpool_wait(&it->threads); 1472 | 1473 | #if 0 1474 | /* poor attempt at visible retrace */ 1475 | for (i=0; i<15; i++) { 1476 | int ytop=(int)((i*it->useheight/15 - 1477 | it->useheight/2)*puheight) + it->useheight/2; 1478 | int ybot=(int)(((i+1)*it->useheight/15 - 1479 | it->useheight/2)*puheight) + it->useheight/2; 1480 | int div=it->usewidth*3/2; 1481 | 1482 | for (x=0; xusewidth; x++) { 1483 | y = ytop + (ybot-ytop)*x / div; 1484 | if (y<0 || y>=it->useheight) continue; 1485 | XPutPixel(it->image, x, y, 0xffffff); 1486 | } 1487 | } 1488 | #endif 1489 | 1490 | #ifdef DEBUG 1491 | if (0) { 1492 | struct timeval tv; 1493 | double fps; 1494 | char buf[256]; 1495 | gettimeofday(&tv,NULL); 1496 | 1497 | fps=1.0/((tv.tv_sec - it->last_display_time.tv_sec) 1498 | + 0.000001*(tv.tv_usec - it->last_display_time.tv_usec)); 1499 | sprintf(buf, "FPS=%0.1f",fps); 1500 | XDrawString(it->dpy, it->window, it->gc, 50, it->useheight*2/3, 1501 | buf, strlen(buf)); 1502 | 1503 | it->last_display_time=tv; 1504 | } 1505 | #endif 1506 | } 1507 | 1508 | analogtv_input * 1509 | analogtv_input_allocate() 1510 | { 1511 | analogtv_input *ret=(analogtv_input *)calloc(1,sizeof(analogtv_input)); 1512 | 1513 | return ret; 1514 | } 1515 | 1516 | 1517 | #ifdef FIXME 1518 | /* add hash */ 1519 | if (it->hashnoise_times[lineno]) { 1520 | int hnt=it->hashnoise_times[lineno] - input->line_hsync[lineno]; 1521 | 1522 | if (hnt>=0 && hnt ANALOGTV_PIC_LEN-hnt) len=ANALOGTV_PIC_LEN-hnt; 1527 | for (i=0; imultipath > 0.0) { 1548 | for (i=0; ighostfir2[i] += 1550 | -(rec->ghostfir2[i]/16.0) + rec->multipath * (frand(0.02)-0.01); 1551 | } 1552 | if (random()%20==0) { 1553 | rec->ghostfir2[random()%(ANALOGTV_GHOSTFIR_LEN)] 1554 | = rec->multipath * (frand(0.08)-0.04); 1555 | } 1556 | for (i=0; ighostfir[i] = 0.8*rec->ghostfir[i] + 0.2*rec->ghostfir2[i]; 1558 | } 1559 | 1560 | if (0) { 1561 | rec->hfloss2 += -(rec->hfloss2/16.0) + rec->multipath * (frand(0.08)-0.04); 1562 | rec->hfloss = 0.5*rec->hfloss + 0.5*rec->hfloss2; 1563 | } 1564 | 1565 | } else { 1566 | for (i=0; ighostfir[i] = (i>=ANALOGTV_GHOSTFIR_LEN/2) ? ((i&1) ? +0.04 : -0.08) /ANALOGTV_GHOSTFIR_LEN 1568 | : 0.0; 1569 | } 1570 | } 1571 | } 1572 | 1573 | --------------------------------------------------------------------------------