-/*
- * Copyright (C) 2007 Andre Noll <maan@tuebingen.mpg.de>
- *
- * 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 "afh.h"
#include "afs.h"
#include "list.h"
-#include "ipc.h"
#include "mm.h"
-#include "sideband.h"
#include "mood.h"
-#include "sched.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.
int64_t num_played_qd;
/** Quadratic deviation of last played time. */
int64_t last_played_qd;
+ /** Correction factor for the num played score. */
+ int64_t num_played_correction;
+ /** Correction factor for the last played score. */
+ int64_t last_played_correction;
+ /** Common divisor of the correction factors. */
+ int64_t normalization_divisor;
/** Number of admissible files */
unsigned num;
};
-static struct afs_statistics statistics;
+static struct afs_statistics statistics = {.normalization_divisor = 1};
/**
* Each line of the current mood corresponds to a mood_item.
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;
};
/*
}
/*
- * Rough approximation to sqrt.
+ * Compute the integer square root floor(sqrt(x)).
*
- * It returns an integer res with res * res <= x. Taken 2007 from the linux
- * source tree.
+ * 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;
+ 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;
}
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)
return ret;
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);
}
struct mood *m = para_calloc(sizeof(struct mood));
if (name)
m->name = para_strdup(name);
- INIT_LIST_HEAD(&m->accept_list);
- INIT_LIST_HEAD(&m->deny_list);
- INIT_LIST_HEAD(&m->score_list);
+ init_list_head(&m->accept_list);
+ init_list_head(&m->deny_list);
+ init_list_head(&m->score_list);
return m;
}
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;
*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)
}
if (!*mood_name) /* ignore dummy row */
goto out;
- para_printf(pb, "checking mood %s...\n", mood_name);
ret = for_each_line(FELF_READ_ONLY, mood_def.data, mood_def.size,
parse_mood_line, &mlpd);
- if (ret < 0)
- para_printf(pb, "mood %s: error in 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);
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) / (int64_t)int_sqrt(qd);
-}
-
+/*
+ * The normalized num_played and last_played values are defined as
+ *
+ * nn := -(np - mean_n) / sigma_n and nl := -(lp - mean_l) / sigma_l
+ *
+ * For a (hypothetical) file with np = 0 and lp = now we thus have
+ *
+ * nn = mean_n / sigma_n =: hn > 0
+ * nl = -(now - mean_l) / sigma_l =: hl < 0
+ *
+ * We design the score function so that both contributions get the same
+ * weight. Define the np and lp score of an arbitrary file as
+ *
+ * sn := nn * -hl and sl := nl * hn
+ *
+ * Example:
+ * num_played mean/sigma: 87/14
+ * last_played mean/sigma: 45/32 days
+ *
+ * We have hn = 87 / 14 = 6.21 and hl = -45 / 32 = -1.41. Multiplying
+ * nn of every file with the correction factor 1.41 and nl with
+ * 6.21 makes the weight of the two contributions equal.
+ *
+ * The total score s := sn + sl has the representation
+ *
+ * s = -cn * (np - mean_n) - cl * (lp - mean_l)
+ *
+ * with positive correction factors
+ *
+ * cn = (now - mean_l) / (sqrt(ql) * sqrt(qn) / n)
+ * cl = mean_n / (sqrt(ql) * sqrt(qn) / n)
+ *
+ * where ql and qn are the quadratic deviations stored in the statistics
+ * structure and n is the number of admissible files. To avoid integer
+ * overflows and rounding errors we store the common divisor of the
+ * correction factors separately.
+ */
static long compute_score(struct afs_info *afsi, long mood_score)
{
- mood_score -= normalized_value(afsi->num_played, statistics.num,
- statistics.num_played_sum, statistics.num_played_qd);
- mood_score -= normalized_value(afsi->last_played, statistics.num,
- statistics.last_played_sum, statistics.last_played_qd);
- return mood_score / 3;
+ int64_t mean_n, mean_l,score_n, score_l;
+
+ assert(statistics.normalization_divisor > 0);
+ assert(statistics.num > 0);
+ mean_n = statistics.num_played_sum / statistics.num;
+ mean_l = statistics.last_played_sum / statistics.num;
+
+ score_n = -((int64_t)afsi->num_played - mean_n)
+ * statistics.num_played_correction
+ / statistics.normalization_divisor;
+ score_l = -((int64_t)afsi->last_played - mean_l)
+ * statistics.last_played_correction
+ / statistics.normalization_divisor;
+ return (mood_score + score_n + score_l) / 3;
}
static int add_afs_statistics(const struct osl_row *row)
assert(n);
if (n == 1) {
memset(&statistics, 0, sizeof(statistics));
+ statistics.normalization_divisor = 1;
return 1;
}
return score_update(aft_row, percent);
}
-static void log_statistics(void)
+/* sse: seconds since epoch. */
+static void log_statistics(int64_t sse)
{
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?
return;
}
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;
+ mean_days = (sse - 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));
+ PARA_NOTICE_LOG("num_played mean/sigma: %" PRId64 "/%" PRIu64 "\n",
+ statistics.num_played_sum / n,
+ int_sqrt(statistics.num_played_qd / n));
+ PARA_NOTICE_LOG("num_played correction factor: %" PRId64 "\n",
+ statistics.num_played_correction);
+ PARA_NOTICE_LOG("last_played correction factor: %" PRId64 "\n",
+ statistics.last_played_correction);
+ PARA_NOTICE_LOG("normalization divisor: %" PRId64 "\n",
+ statistics.normalization_divisor);
}
/**
destroy_mood(current_mood);
current_mood = NULL;
memset(&statistics, 0, sizeof(statistics));
+ statistics.normalization_divisor = 1;
+}
+
+static void compute_correction_factors(int64_t sse)
+{
+ struct afs_statistics *s = &statistics;
+
+ if (s->num > 0) {
+ s->normalization_divisor = int_sqrt(s->last_played_qd)
+ * int_sqrt(s->num_played_qd) / s->num / 100;
+ s->num_played_correction = sse - s->last_played_sum / s->num;
+ s->last_played_correction = s->num_played_sum / s->num;
+ }
+ if (s->num_played_correction == 0)
+ s->num_played_correction = 1;
+ if (s->normalization_divisor == 0)
+ s->normalization_divisor = 1;
+ if (s->last_played_correction == 0)
+ s->last_played_correction = 1;
}
/**
* 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 \ref afs_info::last_played.
+ * \sa struct \ref afs_info::last_played, \ref mp_eval_row().
*/
-int change_current_mood(const char *mood_name)
+int change_current_mood(const char *mood_name, char **errmsg)
{
int i, ret;
struct admissible_array aa = {
.size = 0,
.array = NULL
};
+ /*
+ * We can not use the "now" pointer from sched.c here because we are
+ * called before schedule(), which initializes "now".
+ */
+ struct timeval rnow;
if (mood_name) {
struct mood *m;
};
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();
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;
+ }
+ clock_get_realtime(&rnow);
+ compute_correction_factors(rnow.tv_sec);
+ log_statistics(rnow.tv_sec);
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;
+ }
}
- log_statistics();
ret = statistics.num;
out:
free(aa.array);
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;
}