AnalogTerm2: Add recording support
authorMatthew Mondor <mmondor@pulsar-zone.net>
Sun, 2 Jul 2023 15:03:42 +0000 (15:03 +0000)
committerMatthew Mondor <mmondor@pulsar-zone.net>
Sun, 2 Jul 2023 15:03:42 +0000 (15:03 +0000)
- 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.

13 files changed:
mmsoftware/analogterm2/README.txt
mmsoftware/analogterm2/TODO.txt
mmsoftware/analogterm2/src/config.c
mmsoftware/analogterm2/src/config.h
mmsoftware/analogterm2/src/draw.c
mmsoftware/analogterm2/src/draw.h
mmsoftware/analogterm2/src/font.c
mmsoftware/analogterm2/src/main.c
mmsoftware/analogterm2/src/screen.c
mmsoftware/analogterm2/src/state.c
mmsoftware/analogterm2/src/state.h
mmsoftware/analogterm2/tests/at2-aliases.sh
mmsoftware/analogterm2/tests/record.sh [new file with mode: 0755]

index 6d2ac97..20b9268 100644 (file)
@@ -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
+
index 1007233..158c83d 100644 (file)
@@ -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
 - 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
index 6c1883e..58ff1bd 100644 (file)
@@ -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 <var>=<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;<p>;<v>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
index 2b3a5d1..9c7efb1 100644 (file)
@@ -364,5 +364,7 @@ extern bool cfg_uppercaseview;
  */
 #define HAVE_XSHM_EXTENSION    1
 
+extern const char *cfg_recordfifo;
+
 
 #endif
index 9b80271..ce0f676 100644 (file)
@@ -48,6 +48,7 @@
 
 
 #include <err.h>
+#include <fcntl.h>
 #include <math.h>
 #include <signal.h>
 #include <stdbool.h>
@@ -55,7 +56,9 @@
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/stat.h>
 #include <sys/time.h>
+#include <unistd.h>
 
 #include <state.h>
 #include <screen.h>
@@ -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;
+       }
 }
index fcdcef3..5d29b0c 100644 (file)
@@ -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 */
index 97e52a6..8a500d0 100644 (file)
@@ -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 */
index 4ae0237..ce3c54b 100644 (file)
@@ -201,9 +201,9 @@ usage(void)
 {
 
        errno = EINVAL;
-       fprintf(stderr,
+       (void)printf(
            "\nUsage: %s [-1 | -2] [-8|-u] [-w <cols>] [-h <rows>] [-E <n>]\n"
-           "    [-s] [-C <col>] [-c] [-W] [-b] [-r <ms>]\n"
+           "    [-s] [-C <col>] [-c] [-W] [-b] [-r <ms>] [-R <recordfifo>]\n"
            "    [-B <ticks>[,<ticks>]] [-j <n>] [-P] [-m <mode>] [-M] [-d]\n"
            "    [-D] [-S] [-p <parameters>] [-g <w>,<h>] [-l <pixels>]\n"
            "    [-f <delay>] [-t] [-T <ticks>[,<ticks>]] [-U] [-z <ms>]\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);
index c090a22..fb16842 100644 (file)
@@ -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;
index 88e1e38..d65a238 100644 (file)
@@ -25,6 +25,7 @@
 
 #include <ctype.h>
 #include <err.h>
+#include <stdarg.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdint.h>
@@ -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(
index 3adaefd..faf9b3f 100644 (file)
@@ -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 *);
index 6cb88b4..49d91c5 100755 (executable)
@@ -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 (executable)
index 0000000..de1d494
--- /dev/null
@@ -0,0 +1,99 @@
+#!/bin/sh
+#
+# Utility script to record AnalogTerm2 output with ffmpeg.
+# Usage:
+# record.sh <fifo> <WIDTHxHEIGHT>
+
+FIFO="$1"
+SIZE="$2"
+
+if [ -z "$FIFO" -o -z "$SIZE" ]; then
+       echo "Usage: $0 <fifo> <WIDTHxHEIGHT>"
+       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