]> git.cworth.org Git - mnemon/blobdiff - mnemon.c
Add a simple text-to-speech challenge type.
[mnemon] / mnemon.c
index db8f62fe87c917de251bcb4537c65eed7f65c15c..3dee0347e153c5e2a5c00f93d717f57605fd787d 100644 (file)
--- a/mnemon.c
+++ b/mnemon.c
@@ -26,6 +26,7 @@
 
 #include <sys/types.h>
 #include <sys/time.h>
+#include <unistd.h>
 #include <dirent.h>
 #include <errno.h>
 #include <string.h>
 #include <readline/readline.h>
 #include <readline/history.h>
 
+#define ASSERT_NOT_REACHED             \
+do {                                   \
+    static const int NOT_REACHED = 0;  \
+    assert (NOT_REACHED);              \
+} while (0)
+
 typedef int bool_t;
 
 typedef struct _item {
@@ -54,6 +61,14 @@ typedef enum {
     CATEGORY_ORDER_SEQUENTIAL
 } category_order_t;
 
+typedef enum {
+    CHALLENGE_TYPE_TEXT,
+    CHALLENGE_TYPE_IMAGE,
+    CHALLENGE_TYPE_AUDIO,
+    CHALLENGE_TYPE_MIDI,
+    CHALLENGE_TYPE_TEXT_TO_SPEECH
+} challenge_type_t;
+
 typedef struct _category {
     char *name;
     int items_size;
@@ -65,6 +80,10 @@ typedef struct _category {
     /* Support categories where responses are timed (0.0 == disable). */
     double time_limit;
     int bin_zero_head;
+    /* Support challenges of non-text types (image, audio, etc.) */
+    challenge_type_t challenge_type;
+    /* Whether to repeat afterwards (for a little extra reinforcement) */
+    bool_t repeat;
 } category_t;
 
 typedef struct _mnemon {
@@ -192,6 +211,8 @@ category_init (category_t *category,
     category->order = CATEGORY_ORDER_RANDOM;
     category->time_limit = 0.0;
     category->bin_zero_head = 0;
+    category->challenge_type = CHALLENGE_TYPE_TEXT;
+    category->repeat = 0;
 }
 
 static void
@@ -261,6 +282,28 @@ category_print (category_t *category,
     fprintf (file, "time = %f\n\n",
             category->time_limit);
 
+    fprintf (file, "challenge = ");
+    switch (category->challenge_type) {
+    case CHALLENGE_TYPE_TEXT:
+       fprintf (file, "text");
+       break;
+    case CHALLENGE_TYPE_IMAGE:
+       fprintf (file, "image");
+       break;
+    case CHALLENGE_TYPE_AUDIO:
+       fprintf (file, "audio");
+       break;
+    case CHALLENGE_TYPE_MIDI:
+       fprintf (file, "midi");
+       break;
+    case CHALLENGE_TYPE_TEXT_TO_SPEECH:
+       fprintf (file, "text-to-speech");
+       break;
+    }
+    fprintf (file, "\n\n");
+
+    fprintf (file, "repeat = %d\n\n", category->repeat);
+
     for (i = 0; i < category->num_items; i++) {
        item = &category->items[i];
        if (i != 0)
@@ -577,7 +620,7 @@ mnemon_load_category (mnemon_t              *mnemon,
 
        /* An initial digit means we hit an item. Trigger the
         * spaghetti machine. */
-       if (*line >= '0' && *line <= '9')
+       if ((*line >= '0' && *line <= '9') || *line == '-')
            goto PARSE_BIN;
 
        equal = strchr (line, '=');
@@ -617,6 +660,27 @@ mnemon_load_category (mnemon_t             *mnemon,
                         value, path, line_count);
                exit (1);
            }
+       } else if (strcmp (name, "challenge") == 0) {
+           if (strcmp (value, "text") == 0) {
+               category->challenge_type = CHALLENGE_TYPE_TEXT;
+           } else if (strcmp (value, "image") == 0) {
+               category->challenge_type = CHALLENGE_TYPE_IMAGE;
+           } else if (strcmp (value, "audio") == 0) {
+               category->challenge_type = CHALLENGE_TYPE_AUDIO;
+           } else if (strcmp (value, "midi") == 0) {
+               category->challenge_type = CHALLENGE_TYPE_MIDI;
+           } else if (strcmp (value, "text-to-speech") == 0) {
+               category->challenge_type = CHALLENGE_TYPE_TEXT_TO_SPEECH;
+           } else {
+               fprintf (stderr, "Unknown value for \"challenge\" option \"%s\" at %s:%d\n",
+                        value, path, line_count);
+               exit (1);
+           }
+       } else if (strcmp (name, "repeat") == 0) {
+           if (strcmp (value, "0") == 0) 
+               category->repeat = 0;
+           else
+               category->repeat = 1;
        } else {
            fprintf (stderr, "Unknown option %s at %s:%d\n",
                     name, path, line_count);
@@ -730,6 +794,7 @@ mnemon_save (mnemon_t *mnemon)
 
        category_print (category, file);
 
+       fsync (fileno (file));
        fclose (file);
 
        err = rename (lock_filename, filename);
@@ -863,9 +928,20 @@ mnemon_select_item (mnemon_t        *mnemon,
     category_t *category;
 
     bin_index = rand_within_exponential (mnemon->num_bins);
-
     bin = &mnemon->bins[bin_index];
 
+    /* The most intuitive understanding of the to_introduce counter is
+     * that it's tracking never-before-learned items as they are
+     * pulled from the bin with score 0. But that bin can become
+     * empty. So the refined rule is that we decrement to_introduce
+     * whenever we pull from the lowest-indexed bin with a
+     * non-negative score. */
+    if (mnemon->to_introduce && bin->score >=0 &&
+       (bin_index == 0 || mnemon->bins[bin_index-1].score < 0))
+    {
+       mnemon->to_introduce--;
+    }
+
     item_index = rand_within (bin->num_items);
 
     item = bin->items[item_index];
@@ -987,6 +1063,7 @@ mnemon_handle_command (mnemon_t            *mnemon,
     const char *arg;
     int len;
     switch (command[0]) {
+       /* 'h' for histogram */
        case 'h':
        {
            char *category = NULL;
@@ -1005,6 +1082,12 @@ mnemon_handle_command (mnemon_t          *mnemon,
            mnemon_print_histogram (mnemon, category, length);
        }
        break;
+       /* 'r' for repeat */
+        case 'r':
+       {
+           /* Nothing necessary for repeating. */
+       }
+       break;
         default:
            printf ("Unknown command: %s\n", command);
            break;
@@ -1039,13 +1122,11 @@ mnemon_handle_response (mnemon_t        *mnemon,
        (time_limit == 0.0 || response_time < time_limit))
     {
        item->score++;
-       mnemon->to_master--;
        /* We reserve an item score of 0 for an item that has
         * never been asked. */
        if (item->score == 0) {
            item->score = 1;
            mnemon->unlearned--;
-           mnemon->to_master--;
            printf ("You got it!");
        } else if (item->score < 0) {
            printf ("Yes---just give me %d more.",
@@ -1054,6 +1135,8 @@ mnemon_handle_response (mnemon_t  *mnemon,
            printf ("On your first try, no less!");
        } else {
            printf ("Masterful (%dx).", item->score);
+           if (mnemon->to_master)
+               mnemon->to_master--;
        }
     } else {
        if (! correct)
@@ -1069,12 +1152,13 @@ mnemon_handle_response (mnemon_t        *mnemon,
                printf (" Oops, you knew that, right? (%dx)\n ",
                        item->score);
            mnemon->unlearned++;
-           /* We add three here, (rather than just 2 to track the
-            * change in the item's score below), as an extra
-            * penalty. If the user is forgetting stuff learned
-            * previously, then more time should be spent on mastering
-            * than learning new items. */
-           mnemon->to_master += item->score + 3;
+           /* We increase to_master here as an extra penalty. If the
+            * user is forgetting stuff learned previously, then more
+            * time should be spent on mastering than learning new
+            * items. Note that we only do this during the initial
+            * phase while new items are still being introduced. */
+           if (mnemon->to_introduce)
+               mnemon->to_master++;
            /* We go to -2 to force a little extra reinforcement
             * when re-learning an item, (otherwise, it will often
             * get asked again immediately where it is easy to get
@@ -1082,7 +1166,6 @@ mnemon_handle_response (mnemon_t  *mnemon,
            item->score = -2;
        } else {
            item->score--;
-           mnemon->to_master++;
        }
     }
 
@@ -1100,6 +1183,66 @@ mnemon_handle_response (mnemon_t *mnemon,
     bin_add_item (bin, item);
 }
 
+static void
+mnemon_show_challenge (mnemon_t *mnemon,
+                      challenge_type_t challenge_type,
+                      const char *challenge)
+{
+    const char *program;
+    char *command;
+
+    if (challenge_type == CHALLENGE_TYPE_TEXT) {
+       printf ("%s\n", challenge);
+       return;
+    }
+
+    /* XXX: Yes, shelling out to system is total cheese. The planned
+     * fix here is to bring graphical display in process, (or at least
+     * have a custom external program that accepts image filenames on
+     * stdin.
+     */
+    switch (challenge_type) {
+    case CHALLENGE_TYPE_TEXT:
+       ASSERT_NOT_REACHED;
+       break;
+    case CHALLENGE_TYPE_IMAGE:
+       program = "xli -gamma 2.2";
+       break;
+    case CHALLENGE_TYPE_AUDIO:
+       program = "play";
+       break;
+    case CHALLENGE_TYPE_MIDI:
+       program = "timidity -Os";
+       break;
+    case CHALLENGE_TYPE_TEXT_TO_SPEECH:
+       program = "mnemon-tts";
+       break;
+    }
+
+    xasprintf (&command, "%s %s/%s >/dev/null 2>&1 &",
+              program,
+              mnemon->dir_name,
+              challenge);
+    system (command);
+    free (command);
+}
+
+static void
+mnemon_hide_challenge (mnemon_t *mnemon, challenge_type_t challenge_type)
+{
+    char * command;
+
+    if (challenge_type != CHALLENGE_TYPE_IMAGE)
+       return;
+
+    /* XXX: And this is just embarrassing (obviously wrong in several
+     * ways). Hopefully I'll amend away any commit that includes this.
+     */
+    xasprintf (&command, "killall xli");
+    system (command);
+    free (command);
+}
+
 static void
 mnemon_do_challenges (mnemon_t *mnemon)
 {
@@ -1146,21 +1289,21 @@ mnemon_do_challenges (mnemon_t *mnemon)
        mnemon_select_item (mnemon, &bin, &item_index, &category);
        item = bin->items[item_index];
 
-       if (bin->score == 0)
-           mnemon->to_introduce--;
-
        while (1) {
            if (category->time_limit > 0.0) {
                response = readline ("The next one is timed. Press enter when ready:");
                free (response);
            }
-               
-           printf ("%s\n", item->challenge);
+
+           mnemon_show_challenge (mnemon, category->challenge_type,
+                                  item->challenge);
 
            gettimeofday (&start, NULL);
            response = readline ("> ");
            gettimeofday (&end, NULL);
 
+           mnemon_hide_challenge (mnemon, category->challenge_type);
+
            /* Terminate on EOF */
            if (response == NULL) {
                printf ("\n");
@@ -1181,6 +1324,15 @@ mnemon_do_challenges (mnemon_t *mnemon)
                                (start.tv_sec + start.tv_usec / 1e6),
                                category->time_limit);
        free (response);
+
+       /* Replay audio challenges for reinforcement. */
+       if (category->repeat)
+       {
+           mnemon_show_challenge (mnemon, category->challenge_type,
+                                  item->challenge);
+           printf ("%s\n", item->challenge);
+           sleep (2);
+       }
     } while (mnemon->to_introduce ||
             mnemon->unlearned ||
             mnemon->to_master > 0);