From: Matthew Mondor Date: Sun, 2 Jul 2023 15:03:42 +0000 (+0000) Subject: AnalogTerm2: Add recording support X-Git-Url: http://git.pulsar-zone.net/?a=commitdiff_plain;h=0b985cb262580036e88eac92d78d2c312f4d6e8d;p=mmondor.git AnalogTerm2: Add recording support - Window size not padded at a multiple of 16 to be compatible with efficient frame processing as expected by ffmpeg(1). - The dimensions in characters and pixels are now shown at the top left of the terminal when started. - The -R option was added to enable the recording capability. The fullpath to a FIFO file (see mkfifo(1)) is expected. AT2 writes to that FIFO when recording is enabled. A recording application is expected to already be listening on the other side. - A record.sh example recording script, using ffmpeg(1) was added. - README and TODO were updated. - A bug was fixed where attempting to blank character 0 crashed attempting to dereference a NULL pointer. - More aliases were added to at-aliases.sh, including to enable and disable recording, atrecord/atnorecord. --- diff --git a/mmsoftware/analogterm2/README.txt b/mmsoftware/analogterm2/README.txt index 6d2ac97..20b9268 100644 --- a/mmsoftware/analogterm2/README.txt +++ b/mmsoftware/analogterm2/README.txt @@ -247,3 +247,28 @@ commands: /usr/local/bin/analogterm2 -WS -w80 -h25 -e '/bin/cat /usr/local/share/analogterm2/ds-stat-setup.txt; clear; fortune' Exercise left to the reader for a window to launch wargames(6). :) + + +Recording +========= + +If AnalogTerm ][ is started with the -R option to specify a FIFO +file to issue video frames to, it is possible for a recording +application to read them there. It is important for both to use +the same FIFO and the same resolution. When started, AT2 reports +its resolution at the upper left corner of the terminal. The +xwininfo(1) command can also be used to obtain the dimensions of +the window. The recording application should already be listening +on the FIFO before recording begins. AnalogTerm2 can then be told +when to begin and stop recording, using the following special ATC +sequences, respectively: [?658467;65552;1h and [?658467;65552;0h. +Utility aliases for these exist in at2-aliases.sh, atrecord and +atnorecord. You may want to adapt record.sh for your needs. In +the future, AT2 may manage its own ffmpeg(1) process or directly +use the library. + +Example with the provided script, depending on ffmpeg(1): + +$ analogterm2 -W -w80 -h50 -R /tmp/at2recfifo1 & +$ record.sh /tmp/at2recfifo1 1122x904 + diff --git a/mmsoftware/analogterm2/TODO.txt b/mmsoftware/analogterm2/TODO.txt index 1007233..158c83d 100644 --- a/mmsoftware/analogterm2/TODO.txt +++ b/mmsoftware/analogterm2/TODO.txt @@ -55,7 +55,7 @@ https://en.wikipedia.org/wiki/Box-drawing_character - Overwrite/clear selections before freeing them - ≣ † ☆ ツ ⌘›🍺∴ ( ͡° ͜ʖ ͡°) ƒ ︵ ₂ 😈 θ ƒ › ʼ ƒ ∂ ʻ μ › ∫ ◇ ♪ - ► ə β ə ſ ρ ə ∴ ♪ 😱 † 😳 › ▛ ᵗ * ‽ ℣ Ω ⌘   ❇ + ► ə β ə ſ ρ ə ∴ ♪ 😱 † 😳 › ▛ ᵗ * ‽ ℣ Ω ⌘   ❇ ⸮ - Verify if dead key support is incomplete for ISO-8859-4 and ISO-8859-10. There were special characters that were unicode since the start but also could have punctuation. And others that used two at a time... It might @@ -237,7 +237,9 @@ - Tack seems to be a decent program for terminal testing, other than vttest. - Maybe include ffmpeg recording as part of analogterm. This would be easier - of already supporting YUV internally for xvideo(4). + if already supporting YUV internally for xvideo(4). + Currently AT2 can send frames via a FIFO for an external process to record. + It would also be possible to manage our own ffmpeg process. - Maybe consider xvideo(4) zooming. If YUV was native, it'd be useful for xvideo and ffmpeg and custom ratios could be applied. - Maybe an alternative VT52 or ADM-3A emulation mode for use with old CP/M diff --git a/mmsoftware/analogterm2/src/config.c b/mmsoftware/analogterm2/src/config.c index 6c1883e..58ff1bd 100644 --- a/mmsoftware/analogterm2/src/config.c +++ b/mmsoftware/analogterm2/src/config.c @@ -74,6 +74,7 @@ int cfg_font_width, cfg_font_height; int cfg_leading; int cfg_inputsleep, cfg_inputsleepskip; bool cfg_uppercaseview; +const char *cfg_recordfifo; void @@ -127,6 +128,8 @@ cfg_setdefaults(void) cfg_inputsleepskip = INPUT_SLEEP_SKIP; cfg_uppercaseview = UPPERCASE_VIEW; + + cfg_recordfifo = NULL; } int @@ -150,14 +153,14 @@ cfg_showparams(void) { const struct eparam_s *ep; - fprintf(stderr, "\nScanline emulation parameters (-p):\n" + (void)printf("\nScanline emulation parameters (-p):\n" "These should be comma-separated and in the form =<0-255>.\n"); - fprintf(stderr, "The following equivalent sequence may be used:\n" + (void)printf("The following equivalent sequence may be used:\n" " [?658467;65539;

;h where p = 0-4 and v = 0-255.\n"); for (ep = eparams; ep->param != NULL; ep++) - fprintf(stderr, " %s (%d) - %s\n", + (void)printf(" %s (%d) - %s\n", ep->param, *ep->var, ep->descr); - fprintf(stderr, "\n"); + (void)printf("\n"); } void diff --git a/mmsoftware/analogterm2/src/config.h b/mmsoftware/analogterm2/src/config.h index 2b3a5d1..9c7efb1 100644 --- a/mmsoftware/analogterm2/src/config.h +++ b/mmsoftware/analogterm2/src/config.h @@ -364,5 +364,7 @@ extern bool cfg_uppercaseview; */ #define HAVE_XSHM_EXTENSION 1 +extern const char *cfg_recordfifo; + #endif diff --git a/mmsoftware/analogterm2/src/draw.c b/mmsoftware/analogterm2/src/draw.c index 9b80271..ce0f676 100644 --- a/mmsoftware/analogterm2/src/draw.c +++ b/mmsoftware/analogterm2/src/draw.c @@ -48,6 +48,7 @@ #include +#include #include #include #include @@ -55,7 +56,9 @@ #include #include #include +#include #include +#include #include #include @@ -106,6 +109,10 @@ static int scanjmp = 0; static int cursor_blink_ticks = 0, text_blink_ticks = 0; static uint8_t *empty = NULL; +/* Recording */ +static int recfd = -1; +static size_t recsize = 0; + /* Exported as a general utility */ bool draw_cursor_blink_state = true, draw_text_blink_state = true; @@ -146,11 +153,8 @@ draw_init(state_t *st, screen_t *sc) if ((row_selected = malloc(cfg_text_width * sizeof(bool))) == NULL) goto err; - /* Compute scanjmp */ - if (cfg_condensed) - scanjmp = (int)((cfg_text_width * cfg_font_width) * 1.5) + 1; - else - scanjmp = ((cfg_text_width * cfg_font_width) * 2) + 1; + /* Scanline length, scanjmp */ + scanjmp = screen->pixels_width; /* Setup signal handler */ act.sa_handler = alarm_sighandler; @@ -168,6 +172,12 @@ draw_init(state_t *st, screen_t *sc) if (setitimer(ITIMER_REAL, &itv, NULL) == -1) goto err; + /* + * Ignore SIGPIPE to not be killed when trying to write for recording + */ + act.sa_handler = SIG_IGN; + (void)sigaction(SIGPIPE, &act, NULL); + (void)atexit(draw_cleanup); return; @@ -791,4 +801,52 @@ draw_update_screen(screen_t *sc, state_t *st) draw_lines(st, sc, low, high); draw_update(sc); } + + /* + * If in record mode, write video memory to FIFO. + */ + if (recfd != -1) { + uint8_t *ptr; + ssize_t tsize, rsize = 0; + for (ptr = (uint8_t *)sc->pixels, tsize = recsize; tsize > 0; + tsize -= rsize, ptr += rsize) { + if ((rsize = write(recfd, ptr, tsize)) == -1) { + warn("draw_update_screen() - write(recfifo)"); + record_stop(); + break; + } + } + } +} + +void +record_start(void) +{ + struct stat st; + + if (cfg_recordfifo == NULL) + return; + + /* Ensure FIFO exists */ + if (stat(cfg_recordfifo, &st) != 0 || !(S_ISFIFO(st.st_mode))) + return; + + /* If not already open, attempt to. */ + if (recfd == -1) { + recfd = open(cfg_recordfifo, O_WRONLY); + if (recfd != -1) + (void)fprintf(stderr, "Recording: START\n"); + recsize = screen->pixels_width * screen->pixels_height * 4; + } +} + +void +record_stop(void) +{ + + if (recfd != -1) { + (void)fprintf(stderr, "Recording: STOP\n"); + (void)close(recfd); + recfd = -1; + } } diff --git a/mmsoftware/analogterm2/src/draw.h b/mmsoftware/analogterm2/src/draw.h index fcdcef3..5d29b0c 100644 --- a/mmsoftware/analogterm2/src/draw.h +++ b/mmsoftware/analogterm2/src/draw.h @@ -63,6 +63,8 @@ void draw_reset_cursor(void); void draw_custom_rgb(int, int, int); void draw_update(screen_t *); void draw_update_screen(screen_t *, state_t *); +void record_start(void); +void record_stop(void); /* Regularly toggled by a timer */ diff --git a/mmsoftware/analogterm2/src/font.c b/mmsoftware/analogterm2/src/font.c index 97e52a6..8a500d0 100644 --- a/mmsoftware/analogterm2/src/font.c +++ b/mmsoftware/analogterm2/src/font.c @@ -116,7 +116,7 @@ static const uint32_t latin4_characters[59] = { }; /* Map of supported Latin-8 characters in glyph order. */ -static uint32_t iso8859_14_characters[27] = { +static uint32_t iso8859_14_characters[26] = { 0x0174, 0x0175, 0x0176, 0x0177, 0x1E02, 0x1E03, 0x1E0A, 0x1E0B, 0x1E1E, 0x1E1F, 0x1E40, 0x1E41, 0x1E56, 0x1E57, 0x1E60, 0x1E61, 0x1E6A, 0x1E6B, 0x1E80, 0x1E81, 0x1E82, 0x1E83, 0x1E84, 0x1E85, @@ -711,6 +711,10 @@ font_glyph(state_t *state, uint32_t c, bool decgfx, bool raw) raw: + /* Special, used for delays on some terminals */ + if (c == 0) + goto done; + /* Contiguous character blocks */ for (font = state->font; font != NULL; font = font->next) { if (c < font->end && c >= font->start) { @@ -721,7 +725,7 @@ raw: /* * Glyph order character tables. - * XXX In these grow significantly a hash table or tree may be useful. + * XXX If these grow significantly a hash table or tree may be useful. */ for (table = state->ftable; table != NULL; table = table->next) { /* Check for possible empty user table */ diff --git a/mmsoftware/analogterm2/src/main.c b/mmsoftware/analogterm2/src/main.c index 4ae0237..ce3c54b 100644 --- a/mmsoftware/analogterm2/src/main.c +++ b/mmsoftware/analogterm2/src/main.c @@ -201,9 +201,9 @@ usage(void) { errno = EINVAL; - fprintf(stderr, + (void)printf( "\nUsage: %s [-1 | -2] [-8|-u] [-w ] [-h ] [-E ]\n" - " [-s] [-C ] [-c] [-W] [-b] [-r ]\n" + " [-s] [-C ] [-c] [-W] [-b] [-r ] [-R ]\n" " [-B [,]] [-j ] [-P] [-m ] [-M] [-d]\n" " [-D] [-S] [-p ] [-g ,] [-l ]\n" " [-f ] [-t] [-T [,]] [-U] [-z ]\n" @@ -232,6 +232,13 @@ usage(void) " blinking rate settings -B/-T and the speed of smooth\n" " scrolling when enabled. Low settings save CPU but will\n" " affect interactive performance.\n" + " -R - Specify the fullpath to the FIFO file AT2 should write\n" + " video frames to when recording (not allowed by default).\n" + " To begin and stop recording, the following ATC sequences\n" + " should be issued, respectively: [?658467;65552;1h and\n" + " [?658467;65552;0h. A recording program is expected to be\n" + " reading from that file. See mkfifo(1) and the example\n" + " script record.sh.\n" " -b - Toggle blinking cursor.\n" " -B - Set the cursor blinking speed, in refresh ticks.\n" " Two comma-separated values may optionally be provided for\n" @@ -341,7 +348,7 @@ main(int argc, char **argv, char **envp) progname = strdup(argv[0]); while ((ch = getopt(argc, argv, - "?128uw:h:E:scC:Wr:bB:j:Pm:MdDStT:p:g:l:f:Uz:Z:e:")) != -1) { + "?128uw:h:E:scC:Wr:R:bB:j:Pm:MdDStT:p:g:l:f:Uz:Z:e:")) != -1) { switch (ch) { case '1': cfg_slowscroll = cfg_smoothscroll = true; @@ -361,7 +368,7 @@ main(int argc, char **argv, char **envp) cfg_text_width = i; break; case 'h': - if ((i = atoi(optarg)) >= 24 && i < 10000) + if ((i = atoi(optarg)) >= 4 && i < 10000) cfg_text_height = i; break; case 'E': @@ -400,6 +407,10 @@ main(int argc, char **argv, char **envp) if ((i = atoi(optarg)) >= 11111 && i <= 100000) cfg_refreshspeed = i; break; + case 'R': + if ((cfg_recordfifo = strdup(optarg)) == NULL) + err(EXIT_FAILURE, "strdup()"); + break; case 'b': cfg_cursorblink = !cfg_cursorblink; break; @@ -539,9 +550,10 @@ main(int argc, char **argv, char **envp) goto endopt; } break; - case '?': /* FALLTHROUGH */ - default: + case '?': + default: /* FALLTHROUGH */ usage(); + break; } } argc -= optind; @@ -597,6 +609,10 @@ endopt: /* * Fancy retro welcome message. */ + state_goto(state, 0, 1); + state_printf(state, 0, "(%dx%d, %dx%d)", + screen->pixels_width, screen->pixels_height, + cfg_text_width, cfg_text_height); state_goto(state, 1, 1); state_prints(state, "READY", 0); state_goto(state, 1, (cfg_text_width / 2) - 17); diff --git a/mmsoftware/analogterm2/src/screen.c b/mmsoftware/analogterm2/src/screen.c index c090a22..fb16842 100644 --- a/mmsoftware/analogterm2/src/screen.c +++ b/mmsoftware/analogterm2/src/screen.c @@ -195,6 +195,8 @@ screen_init(void) #else height = (cfg_text_height * (cfg_font_height + cfg_leading)) * 2; #endif + width += width % 16; + height += height % 16; s->pixels_width = width; s->pixels_height = height; diff --git a/mmsoftware/analogterm2/src/state.c b/mmsoftware/analogterm2/src/state.c index 88e1e38..d65a238 100644 --- a/mmsoftware/analogterm2/src/state.c +++ b/mmsoftware/analogterm2/src/state.c @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -554,6 +555,20 @@ state_prints(state_t *st, const char *s, uint32_t m) } void +state_printf(state_t *st, uint32_t m, const char *fmt, ...) +{ + va_list lst; + char buf[1024]; + + va_start(lst, fmt); + (void)vsnprintf(buf, 1023, fmt, lst); + va_end(lst); + buf[1023] = '\0'; + + state_prints(st, buf, m); +} + +void state_goto(state_t *st, int y, int x) { @@ -1358,6 +1373,16 @@ state_emul_printc(state_t *st, uint8_t c) break; } st->text_updateall = true; + } else if (state->curparam == 2 && + state->csiparam[1] == 65552) { + switch (state->csiparam[2]) { + case 0: + record_stop(); + break; + case 1: + record_start(); + break; + } } else if (state->curparam == 3) { /* Custom foreground color */ draw_custom_rgb( diff --git a/mmsoftware/analogterm2/src/state.h b/mmsoftware/analogterm2/src/state.h index 3adaefd..faf9b3f 100644 --- a/mmsoftware/analogterm2/src/state.h +++ b/mmsoftware/analogterm2/src/state.h @@ -131,6 +131,7 @@ void state_printc_1(state_t *, uint32_t, uint32_t, bool); void state_printc(state_t *, uint32_t, uint32_t); void state_printc_noscroll(state_t *, uint32_t, uint32_t); void state_prints(state_t *, const char *, uint32_t); +void state_printf(state_t *, uint32_t, const char *, ...); void state_goto(state_t *, int, int); void state_emul_printc(state_t *, uint8_t); void state_emul_prints(state_t *, const char *); diff --git a/mmsoftware/analogterm2/tests/at2-aliases.sh b/mmsoftware/analogterm2/tests/at2-aliases.sh index 6cb88b4..49d91c5 100755 --- a/mmsoftware/analogterm2/tests/at2-aliases.sh +++ b/mmsoftware/analogterm2/tests/at2-aliases.sh @@ -110,13 +110,24 @@ alias atbar="printf '\033[5 q'" alias atintreset="printf '\033[?658467;65539;0;115h\033[?658467;65539;1;26h\033[?658467;65539;2;230h\033[?658467;65539;3;13h\033[?658467;65539;4;0h'" alias atintbright1="printf '\033[?658467;65539;0;150h\033[?658467;65539;1;26h\033[?658467;65539;2;255h\033[?658467;65539;3;16h\033[?658467;65539;4;16h'" alias atintbright2="printf '\033[?658467;65539;0;140h\033[?658467;65539;1;26h\033[?658467;65539;2;255h\033[?658467;65539;3;26h\033[?658467;65539;4;26h'" +alias atintbright3="printf '\033[?658467;65539;0;255h\033[?658467;65539;1;255h\033[?658467;65539;2;255h\033[?658467;65539;3;0h\033[?658467;65539;4;0h'" alias atintscan1="printf '\033[?658467;65539;0;160h\033[?658467;65539;1;26h\033[?658467;65539;2;32h\033[?658467;65539;3;32h\033[?658467;65539;4;0h'" alias atintscan2="printf '\033[?658467;65539;0;160h\033[?658467;65539;1;26h\033[?658467;65539;2;32h\033[?658467;65539;3;16h\033[?658467;65539;4;0h'" alias atintscan3="printf '\033[?658467;65539;0;100h\033[?658467;65539;1;35h\033[?658467;65539;2;32h\033[?658467;65539;3;16h\033[?658467;65539;4;0h'" alias atintscan4="printf '\033[?658467;65539;0;160h\033[?658467;65539;1;20h\033[?658467;65539;2;8h\033[?658467;65539;3;8h\033[?658467;65539;4;8h'" +alias atintscan5="printf '\033[?658467;65539;0;100h\033[?658467;65539;1;50h\033[?658467;65539;2;80h\033[?658467;65539;3;0h\033[?658467;65539;4;0h'" alias atintgrad="printf '\033[?658467;65539;0;100h\033[?658467;65539;1;18h\033[?658467;65539;2;230h\033[?658467;65539;3;13h\033[?658467;65539;4;0h'" +alias atintreverse="printf '\033[?658467;65539;0;85h\033[?658467;65539;1;5h\033[?658467;65539;2;255h\033[?658467;65539;3;0h\033[?658467;65539;4;0h'" # Font alias atdashedzero="printf '\033[?658467;65540;48;0;0h\033[?658467;65540;48;1;28h\033[?658467;65540;48;2;34h\033[?658467;65540;48;3;38h\033[?658467;65540;48;4;42h\033[?658467;65540;48;5;50h\033[?658467;65540;48;6;34h\033[?658467;65540;48;7;28h\033[?658467;65540;48;8;0h'" alias atfontreset="printf '\033[?658467;65540h'" alias atfontblank="printf '\033[?658467;65540;0h'" + +# Reverse video +alias atreverse="printf '\033[?5h'" +alias atnoreverse="printf '\033[?5l'" + +# Recording +alias atrecord="printf '\033[?658467;65552;1h'" +alias atnorecord="printf '\033[?658467;65552;0h'" diff --git a/mmsoftware/analogterm2/tests/record.sh b/mmsoftware/analogterm2/tests/record.sh new file mode 100755 index 0000000..de1d494 --- /dev/null +++ b/mmsoftware/analogterm2/tests/record.sh @@ -0,0 +1,99 @@ +#!/bin/sh +# +# Utility script to record AnalogTerm2 output with ffmpeg. +# Usage: +# record.sh + +FIFO="$1" +SIZE="$2" + +if [ -z "$FIFO" -o -z "$SIZE" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Adapt these to your needs +FFMPEG=$(find 2>/dev/null $(echo $PATH | sed s/:/\ /g) -iname 'ffmpeg*' | sort -n | head -n1) +VBITRATE=512k +# Also acceptable but lower quality +#VBITRATE=384k +THREADS=4 +FPS=30 + +# Echo parameters +echo +echo "AnalogTerm ][ recording script" +echo +echo "Using:" +echo " ffmpeg command: $FFMPEG" +echo " video bitrate: $VBITRATE" +echo "encoding threads: $THREADS" +echo " resolution: $SIZE" +echo " rate (FPS): $FPS" +echo " input FIFO file: $FIFO" +echo +echo "Copy and customize this script as necessary." +echo "Should be running before starting AnalogTerm2 recording." +echo + +# Ensure that FIFO exists +if [ ! -p "$FIFO" ]; then + echo "Creating FIFO $FIFO" + mkfifo -m 600 "$FIFO" +fi + +echo "$0: started" +echo + +VPID='' + +cleanup() +{ + + # Kill accumulated subprocesses + pkill -fx "cat $FIFO" + + # ffmpeg expects 3 interrupts to quit. + # In some cases, it just hangs and needs SIGKILL. + if [ -n VPID ]; then + kill $VPID + kill $VPID + kill $VPID + sleep 1 + kill -9 $VPID + fi + + exit 0 +} +trap cleanup INT TERM + +while true; do + + # Launch our video and audio encoding processes. + # Cat is useful here since ffmpeg does not gracefully handle + # FIFO file input with its pipe mode designed for piping. + # Cat does this conversion and gracefully exits when the + # writer closes its end; ffmpeg then gracefully exits doing + # its final envelope adjustments like if it had been told + # to exit using q at a terminal. + # Using cat(1) appears redundant but it helps ffmpeg to + # cleanly save its buffers and exit when the pipe chain breaks. + # If it doesn't work, use 2> to a temporary file to obtain the + # error messages issued by ffmpeg. + + TIMESTAMP=$(date -u +"%Y-%m-%d_%H:%M:%S") + + { cat "$FIFO" | \ + $FFMPEG >/dev/null 2>&1 -analyzeduration 100000 -f rawvideo \ + -vcodec rawvideo -s $SIZE -pix_fmt bgra -r $FPS \ + -fflags nobuffer -i pipe:0 -an -b:v $VBITRATE \ + -pix_fmt yuv420p -vf realtime -threads $THREADS -y \ + video-${TIMESTAMP}.mp4; } & + VPID=$! + + # Wait for processes to exit + wait $VPID + + VPID='' + +done