mixer: Use lsu_merge_config_file_options().
[paraslash.git] / mood.c
diff --git a/mood.c b/mood.c
index 897c22b9997d21d25f3644cd866706a7152182d9..a63d4d2af5d10d7b64c319d915e00b9b7ea62e89 100644 (file)
--- a/mood.c
+++ b/mood.c
@@ -1,13 +1,10 @@
-/*
- * Copyright (C) 2007-2014 Andre Noll <maan@systemlinux.org>
- *
- * Licensed under the GPL v2. For licencing details see COPYING.
- */
+/* Copyright (C) 2007 Andre Noll <maan@tuebingen.mpg.de>, see file COPYING. */
 
 /** \file mood.c Paraslash's mood handling functions. */
 
 #include <regex.h>
 #include <osl.h>
+#include <lopsub.h>
 
 #include "para.h"
 #include "error.h"
 #include "afh.h"
 #include "afs.h"
 #include "list.h"
-#include "ipc.h"
 #include "mm.h"
-#include "sideband.h"
 #include "mood.h"
 
+/*
+ * Mood parser API. It's overkill to have an own header file for
+ * these declarations as they are only needed in this .c file.
+ */
+struct mp_context;
+int mp_init(const char *definition, int nbytes, struct mp_context **result,
+                char **errmsg);
+bool mp_eval_row(const struct osl_row *aft_row, struct mp_context *ctx);
+void mp_shutdown(struct mp_context *ctx);
+
 /**
  * Contains statistical data of the currently admissible audio files.
  *
@@ -30,7 +35,7 @@ struct afs_statistics {
        int64_t num_played_sum;
        /** Sum of last played times over all admissible files. */
        int64_t last_played_sum;
-       /** Quadratic deviation of num played time. */
+       /** Quadratic deviation of num played count. */
        int64_t num_played_qd;
        /** Quadratic deviation of last played time. */
        int64_t last_played_qd;
@@ -57,15 +62,13 @@ struct mood_item {
        struct list_head mood_item_node;
 };
 
-/**
- * Created from the mood definition by mood_open().
+/*
+ * Created from the mood definition by \ref change_current_mood().
  *
  * When a mood is opened, each line of its definition is investigated, and a
- * corresponding mood item is produced. Each mood line starts with \p accept,
- * \p deny, or \p score which determines the type of the mood line.  For each
- * such type a linked list is maintained whose entries are the mood items.
- *
- * \sa mood_item, mood_open().
+ * corresponding mood item is produced. Each mood line starts with accept,
+ * deny, or score which determines the type of the mood line. For each such
+ * type a linked list is maintained whose entries are the mood items.
  */
 struct mood {
        /** The name of this mood. */
@@ -76,40 +79,70 @@ struct mood {
        struct list_head deny_list;
        /** The list of mood items of type \p score. */
        struct list_head score_list;
+       /* Only used for version 2 moods. */
+       struct mp_context *parser_context;
 };
 
 /*
  * If current_mood is NULL then no mood is currently open. If
- * current_mood->name is NULL, the dummy mood is currently open
+ * current_mood->name is NULL, the dummy mood is currently open.
  */
 static struct mood *current_mood;
 
-/**
- * Rough approximation to sqrt.
+/*
+ * Find the position of the most-significant set bit.
  *
- * \param x Integer of which to calculate the sqrt.
+ * Copied and slightly adapted from the linux source tree, version 4.9.39
+ * (2017-07).
+ */
+__a_const static uint32_t fls64(uint64_t v)
+{
+       int n = 63;
+       const uint64_t ones = ~(uint64_t)0U;
+
+       if ((v & (ones << 32)) == 0) {
+               n -= 32;
+               v <<= 32;
+       }
+       if ((v & (ones << (64 - 16))) == 0) {
+               n -= 16;
+               v <<= 16;
+       }
+       if ((v & (ones << (64 - 8))) == 0) {
+               n -= 8;
+               v <<= 8;
+       }
+       if ((v & (ones << (64 - 4))) == 0) {
+               n -= 4;
+               v <<= 4;
+       }
+       if ((v & (ones << (64 - 2))) == 0) {
+               n -= 2;
+               v <<= 2;
+       }
+       if ((v & (ones << (64 - 1))) == 0)
+               n -= 1;
+       return n;
+}
+
+/*
+ * Compute the integer square root floor(sqrt(x)).
  *
- * \return An integer res with res * res <= x.
+ * Taken 2007 from the linux source tree.
  */
 __a_const static uint64_t int_sqrt(uint64_t x)
 {
-       uint64_t op, res, one = 1;
-       op = x;
-       res = 0;
-
-       one = one << 62;
-       while (one > op)
-               one >>= 2;
+       uint64_t op = x, res = 0, one = 1;
 
+       one = one << (fls64(x) & ~one);
        while (one != 0) {
                if (op >= res + one) {
                        op = op - (res + one);
-                       res = res +  2 * one;
+                       res = res + 2 * one;
                }
                res /= 2;
                one /= 4;
        }
-//     PARA_NOTICE_LOG("sqrt(%llu) = %llu\n", x, res);
        return res;
 }
 
@@ -140,8 +173,8 @@ static bool get_item_score(struct mood_item *item, const struct afs_info *afsi,
 }
 
 /* returns 1 if row admissible, 0 if not, negative on errors */
-static int compute_mood_score(const struct osl_row *aft_row, struct mood *m,
-               long *result)
+static int row_is_admissible(const struct osl_row *aft_row, struct mood *m,
+               long *scorep)
 {
        struct mood_item *item;
        int ret;
@@ -153,14 +186,18 @@ static int compute_mood_score(const struct osl_row *aft_row, struct mood *m,
 
        if (!m)
                return -E_NO_MOOD;
+       if (m->parser_context) {
+               *scorep = 0;
+               return mp_eval_row(aft_row, m->parser_context);
+       }
        ret = get_afsi_of_row(aft_row, &afsi);
-       if (ret< 0)
+       if (ret < 0)
                return ret;
        ret = get_afhi_of_row(aft_row, &afhi);
-       if (ret< 0)
+       if (ret < 0)
                return ret;
        ret = get_audio_file_path_of_row(aft_row, &path);
-       if (ret< 0)
+       if (ret < 0)
                return ret;
        /* reject audio file if it matches any entry in the deny list */
        list_for_each_entry(item, &m->deny_list, mood_item_node) {
@@ -190,7 +227,7 @@ static int compute_mood_score(const struct osl_row *aft_row, struct mood *m,
        }
        if (score_arg_sum)
                score /= score_arg_sum;
-       *result = score;
+       *scorep = score;
        return 1;
 }
 
@@ -217,6 +254,7 @@ static void destroy_mood(struct mood *m)
        list_for_each_entry_safe(item, tmp, &m->score_list, mood_item_node)
                cleanup_list_entry(item);
        free(m->name);
+       mp_shutdown(m->parser_context);
        free(m);
 }
 
@@ -257,7 +295,6 @@ struct mood_line_parser_data {
  * <score> is either an integer or "random" which assigns a random score to
  * all matching files
  */
-
 static int parse_mood_line(char *mood_line, void *data)
 {
        struct mood_line_parser_data *mlpd = data;
@@ -364,16 +401,15 @@ success:
        ret = 1;
 out:
        free_argv(argv);
-       if (ret >= 0)
-               return ret;
-       if (mi) {
+       if (mi && (ret < 0 || !mlpd->m)) { /* mi was not added to any list */
                free(mi->parser_data);
                free(mi);
        }
        return ret;
 }
 
-static int load_mood(const struct osl_row *mood_row, struct mood **m)
+static int load_mood(const struct osl_row *mood_row, struct mood **m,
+               char **errmsg)
 {
        char *mood_name;
        struct osl_object mood_def;
@@ -382,22 +418,31 @@ static int load_mood(const struct osl_row *mood_row, struct mood **m)
 
        *m = NULL;
        ret = mood_get_name_and_def_by_row(mood_row, &mood_name, &mood_def);
-       if (ret < 0)
+       if (ret < 0) {
+               if (errmsg)
+                       *errmsg = make_message(
+                               "could not read mood definition");
                return ret;
-       if (!*mood_name)
-               return -E_DUMMY_ROW;
+       }
+       assert(*mood_name);
        mlpd.m = alloc_new_mood(mood_name);
        ret = for_each_line(FELF_READ_ONLY, mood_def.data, mood_def.size,
                parse_mood_line, &mlpd);
-       osl_close_disk_object(&mood_def);
        if (ret < 0) {
-               PARA_ERROR_LOG("unable to load mood %s: %s\n", mlpd.m->name,
-                       para_strerror(-ret));
-               destroy_mood(mlpd.m);
-               return ret;
+               PARA_INFO_LOG("opening version 2 mood %s\n", mlpd.m->name);
+               ret = mp_init(mood_def.data, mood_def.size, &mlpd.m->parser_context,
+                       errmsg);
+               if (ret < 0)
+                       destroy_mood(mlpd.m);
+       } else {
+               PARA_WARNING_LOG("loaded version 1 mood %s\n", mlpd.m->name);
+               PARA_WARNING_LOG("please convert to version 2\n");
+               ret = 1;
        }
-       *m = mlpd.m;
-       return 1;
+       osl_close_disk_object(&mood_def);
+       if (ret >= 0)
+               *m = mlpd.m;
+       return ret;
 }
 
 static int check_mood(struct osl_row *mood_row, void *data)
@@ -410,20 +455,30 @@ static int check_mood(struct osl_row *mood_row, void *data)
        int ret = mood_get_name_and_def_by_row(mood_row, &mood_name, &mood_def);
 
        if (ret < 0) {
-               para_printf(pb, "failed to get mood definition: %s\n",
-                       para_strerror(-ret));
+               para_printf(pb, "cannot read mood\n");
                return ret;
        }
        if (!*mood_name) /* ignore dummy row */
                goto out;
-       ret = para_printf(pb, "checking mood %s...\n", mood_name);
-       if (ret < 0)
-               goto out;
        ret = for_each_line(FELF_READ_ONLY, mood_def.data, mood_def.size,
                parse_mood_line, &mlpd);
-       if (ret < 0)
-               para_printf(pb, "%s line %u: %s\n", mood_name, mlpd.line_num,
-                       para_strerror(-ret));
+       if (ret < 0) {
+               char *errmsg;
+               struct mood *m = alloc_new_mood("check");
+               ret = mp_init(mood_def.data, mood_def.size, &m->parser_context,
+                       &errmsg);
+               if (ret < 0) {
+                       para_printf(pb, "%s: %s\n", mood_name, errmsg);
+                       free(errmsg);
+                       para_printf(pb, "%s\n", para_strerror(-ret));
+               } else
+                       destroy_mood(m);
+       } else {
+               para_printf(pb, "%s: v1 mood, please convert to v2\n",
+                       mood_name);
+
+       }
+       ret = 1; /* don't fail the loop on invalid mood definitions */
 out:
        osl_close_disk_object(&mood_def);
        return ret;
@@ -432,35 +487,23 @@ out:
 /**
  * Check all moods for syntax errors.
  *
- * \param fd The afs socket.
- * \param query Unused.
+ * \param aca Only ->pbout is used for diagnostics.
+ *
+ * \return Negative on fatal errors. Inconsistent mood definitions are not
+ * considered an error.
  */
-void mood_check_callback(int fd, __a_unused const struct osl_object *query)
+int mood_check_callback(struct afs_callback_arg *aca)
 {
-       struct para_buffer pb = {
-               .max_size = shm_get_shmmax(),
-               .private_data = &(struct afs_max_size_handler_data) {
-                       .fd = fd,
-                       .band = SBD_OUTPUT
-               },
-               .max_size_handler = afs_max_size_handler
-       };
-
-       int ret = para_printf(&pb, "checking moods...\n");
-       if (ret < 0)
-               return;
-       osl_rbtree_loop(moods_table, BLOBCOL_ID, &pb,
-               check_mood);
-       if (pb.offset)
-               pass_buffer_as_shm(fd, SBD_OUTPUT, pb.buf, pb.offset);
-       free(pb.buf);
+       para_printf(&aca->pbout, "checking moods...\n");
+       return osl(osl_rbtree_loop(moods_table, BLOBCOL_ID, &aca->pbout,
+               check_mood));
 }
 
 static int64_t normalized_value(int64_t x, int64_t n, int64_t sum, int64_t qd)
 {
        if (!n || !qd)
                return 0;
-       return 100 * (n * x - sum) / (int64_t)int_sqrt(n * qd);
+       return 100 * (n * x - sum) / (int64_t)int_sqrt(n) / (int64_t)int_sqrt(qd);
 }
 
 static long compute_score(struct afs_info *afsi, long mood_score)
@@ -474,7 +517,7 @@ static long compute_score(struct afs_info *afsi, long mood_score)
 
 static int add_afs_statistics(const struct osl_row *row)
 {
-       uint64_t n, x, s;
+       uint64_t n, x, s, q;
        struct afs_info afsi;
        int ret;
 
@@ -484,14 +527,18 @@ static int add_afs_statistics(const struct osl_row *row)
        n = statistics.num;
        x = afsi.last_played;
        s = statistics.last_played_sum;
-       if (n > 0)
-               statistics.last_played_qd += (x - s / n) * (x - s / n) * n / (n + 1);
+       if (n > 0) {
+               q = (x > s / n)? x - s / n : s / n - x;
+               statistics.last_played_qd += q * q * n / (n + 1);
+       }
        statistics.last_played_sum += x;
 
        x = afsi.num_played;
        s = statistics.num_played_sum;
-       if (n > 0)
-               statistics.num_played_qd += (x - s / n) * (x - s / n) * n / (n + 1);
+       if (n > 0) {
+               q = (x > s / n)? x - s / n : s / n - x;
+               statistics.num_played_qd += q * q * n / (n + 1);
+       }
        statistics.num_played_sum += x;
        statistics.num++;
        return 1;
@@ -532,21 +579,17 @@ static int del_afs_statistics(const struct osl_row *row)
        return 1;
 }
 
-/**
- * Structure used during mood_open().
- *
- * At mood open time, we look at each file in the audio file table in order to
- * determine whether it is admissible. If a file happens to be admissible, its
- * mood score is computed by calling each relevant mood_score_function. Next,
- * we update the afs_statistics and add a struct admissible_file_info to a
- * temporary array.
+/*
+ * At mood open time we determine the set of admissible files for the given
+ * mood. The mood score of each admissible file is computed by adding up all
+ * mood item scores. Next, we update the afs statistics and append a struct
+ * admissible_file_info to a temporary array.
  *
- * If all files have been processed that way, the final score of each
+ * When all files have been processed in this way, the final score of each
  * admissible file is computed by adding the dynamic score (which depends on
- * the afs_statistics) to the mood score.  Finally, all audio files in the
- * array are added to the score table and the admissible array is freed.
- *
- * \sa mood_method, admissible_array.
+ * the afs_statistics and the current time) to the mood score. Finally, all
+ * audio files in the temporary array are added to the score table and the
+ * array is freed.
  */
 struct admissible_file_info
 {
@@ -580,7 +623,7 @@ static int add_if_admissible(struct osl_row *aft_row, void *data)
        int ret;
        long score = 0;
 
-       ret = compute_mood_score(aft_row, aa->m, &score);
+       ret = row_is_admissible(aft_row, aa->m, &score);
        if (ret <= 0)
                return ret;
        if (statistics.num >= aa->size) {
@@ -618,7 +661,8 @@ static int add_if_admissible(struct osl_row *aft_row, void *data)
  * the last number a_n was replaced by b) may be computed in O(1) time in terms
  * of n, q, a_n, b, and S as
  *
- *     q' = q + d * s - (2 * S + d) * d / n,
+ *     q' = q + d * s - (2 * S + d) * d / n
+ *        = q + d * (s - 2 * S / n - d /n),
  *
  * where d = b - a_n, and s = b + a_n.
  *
@@ -635,10 +679,11 @@ _static_inline_ int64_t update_quadratic_deviation(int64_t n, int64_t old_qd,
 {
        int64_t delta = new_val - old_val;
        int64_t sigma = new_val + old_val;
-       return old_qd + delta * sigma - (2 * old_sum + delta) * delta / n;
+       return old_qd + delta * (sigma - 2 * old_sum / n - delta / n);
 }
 
-static int update_afs_statistics(struct afs_info *old_afsi, struct afs_info *new_afsi)
+static int update_afs_statistics(struct afs_info *old_afsi,
+               struct afs_info *new_afsi)
 {
        unsigned n;
        int ret = get_num_admissible_files(&n);
@@ -686,7 +731,7 @@ static int delete_from_statistics_and_score_table(const struct osl_row *aft_row)
  *
  * \return Positive on success, negative on errors.
  *
- * \sa score_delete().
+ * \sa \ref score_delete().
  */
 static int mood_delete_audio_file(const struct osl_row *aft_row)
 {
@@ -725,7 +770,7 @@ static int mood_update_audio_file(const struct osl_row *aft_row,
        if (ret < 0)
                return ret;
        was_admissible = ret;
-       ret = compute_mood_score(aft_row, current_mood, &score);
+       ret = row_is_admissible(aft_row, current_mood, &score);
        if (ret < 0)
                return ret;
        is_admissible = (ret > 0);
@@ -755,31 +800,41 @@ static int mood_update_audio_file(const struct osl_row *aft_row,
                percent = 100;
        else if (percent < 0)
                percent = 0;
-       PARA_DEBUG_LOG("moving from rank %u to %lu%%\n", rank, percent);
+       PARA_DEBUG_LOG("moving from rank %u to %li%%\n", rank, percent);
        return score_update(aft_row, percent);
 }
 
 static void log_statistics(void)
 {
        unsigned n = statistics.num;
+       int mean_days, sigma_days;
+       /*
+        * We can not use the "now" pointer from sched.c here because we are
+        * called before schedule(), which initializes "now".
+        */
+       struct timeval rnow;
 
+       assert(current_mood);
+       PARA_NOTICE_LOG("loaded mood %s\n", current_mood->name?
+               current_mood->name : "(dummy)");
        if (!n) {
-               PARA_NOTICE_LOG("no admissible files\n");
+               PARA_WARNING_LOG("no admissible files\n");
                return;
        }
-       PARA_INFO_LOG("last_played mean: %lli, last_played sigma: %llu\n",
-               (long long int)(statistics.last_played_sum / n),
-               (long long unsigned)int_sqrt(statistics.last_played_qd / n));
-       PARA_INFO_LOG("num_played mean: %lli, num_played sigma: %llu\n",
-               (long long int)statistics.num_played_sum / n,
+       PARA_NOTICE_LOG("%u admissible files\n", statistics.num);
+       clock_get_realtime(&rnow);
+       mean_days = (rnow.tv_sec - statistics.last_played_sum / n) / 3600 / 24;
+       sigma_days = int_sqrt(statistics.last_played_qd / n) / 3600 / 24;
+       PARA_NOTICE_LOG("last_played mean/sigma: %d/%d days\n", mean_days, sigma_days);
+       PARA_NOTICE_LOG("num_played mean/sigma: %llu/%llu\n",
+               (long long unsigned)statistics.num_played_sum / n,
                (long long unsigned)int_sqrt(statistics.num_played_qd / n));
 }
 
 /**
  * Close the current mood.
  *
- * Free all resources of the current mood which were allocated during
- * mood_open().
+ * Frees all resources of the current mood.
  */
 void close_current_mood(void)
 {
@@ -792,19 +847,22 @@ void close_current_mood(void)
  * Change the current mood.
  *
  * \param mood_name The name of the mood to open.
+ * \param errmsg Error description is returned here.
  *
  * If \a mood_name is \a NULL, load the dummy mood that accepts every audio file
  * and uses a scoring method based only on the \a last_played information.
  *
+ * The errmsg pointer may be NULL, in which case no error message will be
+ * returned. If a non-NULL pointer is given, the caller must free *errmsg.
+ *
  * If there is already an open mood, it will be closed first.
  *
  * \return Positive on success, negative on errors. Loading the dummy mood
  * always succeeds.
  *
- * \sa struct admissible_file_info, struct admissible_array, struct
- * afs_info::last_played, mood_close().
+ * \sa struct \ref afs_info::last_played, \ref mp_eval_row().
  */
-int change_current_mood(char *mood_name)
+int change_current_mood(const char *mood_name, char **errmsg)
 {
        int i, ret;
        struct admissible_array aa = {
@@ -816,15 +874,17 @@ int change_current_mood(char *mood_name)
                struct mood *m;
                struct osl_row *row;
                struct osl_object obj = {
-                       .data = mood_name,
+                       .data = (char *)mood_name,
                        .size = strlen(mood_name) + 1
                };
                ret = osl(osl_get_row(moods_table, BLOBCOL_NAME, &obj, &row));
                if (ret < 0) {
-                       PARA_NOTICE_LOG("no such mood: %s\n", mood_name);
+                       if (errmsg)
+                               *errmsg = make_message("no such mood: %s",
+                                       mood_name);
                        return ret;
                }
-               ret = load_mood(row, &m);
+               ret = load_mood(row, &m, errmsg);
                if (ret < 0)
                        return ret;
                close_current_mood();
@@ -836,40 +896,45 @@ int change_current_mood(char *mood_name)
        aa.m = current_mood;
        PARA_NOTICE_LOG("computing statistics of admissible files\n");
        ret = audio_file_loop(&aa, add_if_admissible);
-       if (ret < 0)
+       if (ret < 0) {
+               if (errmsg)
+                       *errmsg = make_message("audio file loop failed");
                return ret;
-       log_statistics();
-       PARA_INFO_LOG("%d admissible files\n", statistics.num);
+       }
        for (i = 0; i < statistics.num; i++) {
                struct admissible_file_info *a = aa.array + i;
                ret = add_to_score_table(a->aft_row, a->score);
-               if (ret < 0)
+               if (ret < 0) {
+                       if (errmsg)
+                               *errmsg = make_message(
+                                       "could not add row to score table");
                        goto out;
+               }
        }
-       PARA_NOTICE_LOG("loaded mood %s\n", mood_name? mood_name : "(dummy)");
+       log_statistics();
        ret = statistics.num;
 out:
        free(aa.array);
        return ret;
 }
-/**
+
+/*
  * Close and re-open the current mood.
  *
- * This function is used if changes to the audio file table or the
- * attribute table were made that render the current list of admissible
- * files useless. For example, if an attribute is removed from the
- * attribute table, this function is called.
+ * This function is called on events which render the current list of
+ * admissible files useless, for example if an attribute is removed from the
+ * attribute table.
  *
- * \return Positive on success, negative on errors. If no mood is currently
- * open, the function returns success.
- *
- * \sa mood_open(), mood_close().
+ * If no mood is currently open, the function returns success.
  */
 static int reload_current_mood(void)
 {
        int ret;
        char *mood_name = NULL;
 
+       ret = clear_score_table();
+       if (ret < 0)
+               return ret;
        if (!current_mood)
                return 1;
        PARA_NOTICE_LOG("reloading %s\n", current_mood->name?
@@ -877,7 +942,7 @@ static int reload_current_mood(void)
        if (current_mood->name)
                mood_name = para_strdup(current_mood->name);
        close_current_mood();
-       ret = change_current_mood(mood_name);
+       ret = change_current_mood(mood_name, NULL);
        free(mood_name);
        return ret;
 }
@@ -898,8 +963,6 @@ static int reload_current_mood(void)
 int moods_event_handler(enum afs_events event, __a_unused struct para_buffer *pb,
                void *data)
 {
-       int ret;
-
        if (!current_mood)
                return 0;
        switch (event) {
@@ -912,10 +975,6 @@ int moods_event_handler(enum afs_events event, __a_unused struct para_buffer *pb
        case BLOB_ADD:
                if (data == moods_table || data == playlists_table)
                        return 1; /* no reload necessary for these */
-               ret = clear_score_table();
-               if (ret < 0)
-                       PARA_CRIT_LOG("clear score table returned %s\n",
-                               para_strerror(-ret));
                return reload_current_mood();
        /* these also require reload of the score table */
        case ATTRIBUTE_ADD: