X-Git-Url: http://git.tuebingen.mpg.de/?p=paraslash.git;a=blobdiff_plain;f=mood.c;h=1e15ef0e081480381fcbc4fdad2179848f429c1f;hp=5268e77fe03c7e56d1b146935f683e30dea9a2f6;hb=HEAD;hpb=19c6002ccd47b720b53410d0cbbecdae6ba80223 diff --git a/mood.c b/mood.c index 5268e77f..1e15ef0e 100644 --- a/mood.c +++ b/mood.c @@ -12,8 +12,6 @@ #include "afh.h" #include "afs.h" #include "list.h" -#include "mm.h" -#include "mood.h" /* * Mood parser API. It's overkill to have an own header file for @@ -39,55 +37,40 @@ struct afs_statistics { 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; /** - * Each line of the current mood corresponds to a mood_item. - */ -struct mood_item { - /** The method this line is referring to. */ - const struct mood_method *method; - /** The data structure computed by the mood parser. */ - void *parser_data; - /** The given score value, or zero if none was given. */ - int32_t score_arg; - /** Non-zero if random scoring was requested. */ - int random_score; - /** Whether the "not" keyword was given in the mood line. */ - int logical_not; - /** The position in the list of items. */ - struct list_head mood_item_node; -}; - -/* - * Created from the mood definition by \ref change_current_mood(). + * Stores an instance of a loaded mood (parser and statistics). * - * When a mood is opened, each line of its definition is investigated, and a - * 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. + * A structure of this type is allocated and initialized when a mood is loaded. */ -struct mood { - /** The name of this mood. */ +struct mood_instance { + /** NULL means that this is the "dummy" mood. */ char *name; - /** The list of mood items of type \p accept. */ - struct list_head accept_list; - /** The list of mood items of type \p deny. */ - 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. */ + /** Bison's abstract syntax tree, used to determine admissibility. */ struct mp_context *parser_context; + /** To compute the score. */ + struct afs_statistics stats; + /** NULL means to operate on the global score table. */ + struct osl_table *score_table; }; /* - * If current_mood is NULL then no mood is currently open. If - * current_mood->name is NULL, the dummy mood is currently open. + * If current_mood is NULL then no mood is currently loaded. If + * current_mood->name is NULL, the current mood is the dummy mood. + * + * The statistics are adjusted dynamically through this pointer as files are + * added, removed or played. */ -static struct mood *current_mood; +static struct mood_instance *current_mood; /* * Find the position of the most-significant set bit. @@ -146,338 +129,76 @@ __a_const static uint64_t int_sqrt(uint64_t x) return res; } -/* - * Returns true if row matches, false if it does not match. In any case score - * and score_arg_sum are set/increased accordingly. - */ -static bool get_item_score(struct mood_item *item, const struct afs_info *afsi, - const struct afh_info *afhi, const char *path, long *score, - long *score_arg_sum) -{ - int ret; - bool match = true; - - *score_arg_sum += item->random_score? 100 : PARA_ABS(item->score_arg); - ret = 100; - if (item->method) { - ret = item->method->score_function(path, afsi, afhi, - item->parser_data); - if ((ret < 0 && !item->logical_not) || (ret >= 0 && item->logical_not)) - match = false; - } - if (item->random_score) - *score = PARA_ABS(ret) * para_random(100); - else - *score = PARA_ABS(ret) * item->score_arg; - return match; -} - -/* returns 1 if row admissible, 0 if not, negative on errors */ -static int row_is_admissible(const struct osl_row *aft_row, struct mood *m, - long *scorep) +static void destroy_mood(struct mood_instance *m) { - struct mood_item *item; - int ret; - bool match; - long score_arg_sum = 0, score = 0, item_score; - struct afs_info afsi; - struct afh_info afhi; - char *path; - - 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; - ret = get_afhi_of_row(aft_row, &afhi); - if (ret < 0) - return ret; - ret = get_audio_file_path_of_row(aft_row, &path); - 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) { - match = get_item_score(item, &afsi, &afhi, path, &item_score, - &score_arg_sum); - if (match) /* not admissible */ - return 0; - score += item_score; - } - match = false; - list_for_each_entry(item, &m->accept_list, mood_item_node) { - ret = get_item_score(item, &afsi, &afhi, path, &item_score, - &score_arg_sum); - if (ret == 0) - continue; - match = true; - score += item_score; - } - /* reject if there is no matching entry in the accept list */ - if (!match && !list_empty(&m->accept_list)) - return 0; - list_for_each_entry(item, &m->score_list, mood_item_node) { - match = get_item_score(item, &afsi, &afhi, path, &item_score, - &score_arg_sum); - if (match) - score += item_score; - } - if (score_arg_sum) - score /= score_arg_sum; - *scorep = score; - return 1; -} - -static void cleanup_list_entry(struct mood_item *item) -{ - if (item->method && item->method->cleanup) - item->method->cleanup(item->parser_data); - else - free(item->parser_data); - list_del(&item->mood_item_node); - free(item); -} - -static void destroy_mood(struct mood *m) -{ - struct mood_item *tmp, *item; - if (!m) return; - list_for_each_entry_safe(item, tmp, &m->accept_list, mood_item_node) - cleanup_list_entry(item); - list_for_each_entry_safe(item, tmp, &m->deny_list, mood_item_node) - cleanup_list_entry(item); - 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); + if (m->score_table) + score_close(m->score_table); + free(m->name); free(m); } -static struct mood *alloc_new_mood(const char *name) +static struct mood_instance *alloc_new_mood(const char *name) { - struct mood *m = para_calloc(sizeof(struct mood)); + struct mood_instance *m = zalloc(sizeof(*m)); + 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); + m->stats.normalization_divisor = 1; return m; } -/** The different types of a mood line. */ -enum mood_line_type { - /** Invalid. */ - ML_INVALID, - /** Accept line. */ - ML_ACCEPT, - /** Deny line. */ - ML_DENY, - /** Score line. */ - ML_SCORE -}; - -/** Data passed to the parser of a mood line. */ -struct mood_line_parser_data { - /** The mood this mood line belongs to. */ - struct mood *m; - /** The line number in the mood definition. */ - unsigned line_num; -}; - -/* - * ] | deny [with score ] | score > - * [if] [not] [options] - * 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; - char **argv; - unsigned num_words; - char **w; - int i, ret; - enum mood_line_type mlt = ML_INVALID; - struct mood_item *mi = NULL; - - mlpd->line_num++; - ret = create_argv(mood_line, " \t", &argv); - if (ret < 0) - return ret; - num_words = ret; - if (!num_words) /* empty line */ - goto out; - w = argv; - if (**w == '#') /* comment */ - goto out; - if (!strcmp(*w, "accept")) - mlt = ML_ACCEPT; - else if (!strcmp(*w, "deny")) - mlt = ML_DENY; - else if (!strcmp(*w, "score")) - mlt = ML_SCORE; - ret = -E_MOOD_SYNTAX; - if (mlt == ML_INVALID) - goto out; - mi = para_calloc(sizeof(struct mood_item)); - if (mlt != ML_SCORE) { - ret = -E_MOOD_SYNTAX; - w++; - if (!*w) - goto out; - if (strcmp(*w, "with")) - goto check_for_if; - w++; - if (!*w) - goto out; - if (strcmp(*w, "score")) - goto out; - } - if (mlt == ML_SCORE || !strcmp(*w, "score")) { - ret = -E_MOOD_SYNTAX; - w++; - if (!*w) - goto out; - if (strcmp(*w, "random")) { - mi->random_score = 0; - ret = para_atoi32(*w, &mi->score_arg); - if (ret < 0) - goto out; - } else { - mi->random_score = 1; - if (!*(w + 1)) - goto success; /* the line "score random" is valid */ - } - } else - mi->score_arg = 0; - ret = -E_MOOD_SYNTAX; - w++; - if (!*w) - goto out; -check_for_if: - if (!strcmp(*w, "if")) { - ret = -E_MOOD_SYNTAX; - w++; - if (!*w) - goto out; - } - if (!strcmp(*w, "not")) { - ret = -E_MOOD_SYNTAX; - w++; - if (!*w) - goto out; - mi->logical_not = 1; - } else - mi->logical_not = 0; - for (i = 0; mood_methods[i].parser; i++) { - if (strcmp(*w, mood_methods[i].name)) - continue; - break; - } - ret = -E_MOOD_SYNTAX; - if (!mood_methods[i].parser) - goto out; - ret = mood_methods[i].parser(num_words - 1 - (w - argv), w, - &mi->parser_data); - if (ret < 0) - goto out; - mi->method = &mood_methods[i]; -success: - if (mlpd->m) { - if (mlt == ML_ACCEPT) - para_list_add(&mi->mood_item_node, &mlpd->m->accept_list); - else if (mlt == ML_DENY) - para_list_add(&mi->mood_item_node, &mlpd->m->deny_list); - else - para_list_add(&mi->mood_item_node, &mlpd->m->score_list); - } - PARA_DEBUG_LOG("%s entry added, method: %p\n", mlt == ML_ACCEPT? "accept" : - (mlt == ML_DENY? "deny" : "score"), mi->method); - ret = 1; -out: - free_argv(argv); - 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, - char **errmsg) +static int init_mood_parser(const char *mood_name, struct mood_instance **m, + char **err) { - char *mood_name; struct osl_object mood_def; - struct mood_line_parser_data mlpd = {.line_num = 0}; int ret; - *m = NULL; - ret = mood_get_name_and_def_by_row(mood_row, &mood_name, &mood_def); - if (ret < 0) { - if (errmsg) - *errmsg = make_message( - "could not read mood definition"); - return ret; + if (!*mood_name) { + if (err) + *err = make_message("empty mood name\n"); + return -ERRNO_TO_PARA_ERROR(EINVAL); } - 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); + ret = mood_get_def_by_name(mood_name, &mood_def); if (ret < 0) { - 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; + if (err) + *err = make_message("could not read mood definition\n"); + return ret; } + *m = alloc_new_mood(mood_name); + PARA_INFO_LOG("loading mood %s\n", mood_name); + ret = mp_init(mood_def.data, mood_def.size, &(*m)->parser_context, err); osl_close_disk_object(&mood_def); - if (ret >= 0) - *m = mlpd.m; + if (ret < 0) + destroy_mood(*m); return ret; } static int check_mood(struct osl_row *mood_row, void *data) { - struct para_buffer *pb = data; - char *mood_name; + struct afs_callback_arg *aca = data; + char *mood_name, *errmsg; struct osl_object mood_def; - struct mood_line_parser_data mlpd = {.line_num = 0}; - + struct mood_instance *m; int ret = mood_get_name_and_def_by_row(mood_row, &mood_name, &mood_def); if (ret < 0) { - para_printf(pb, "cannot read mood\n"); + afs_error(aca, "cannot read mood\n"); return ret; } if (!*mood_name) /* ignore dummy row */ goto out; - ret = for_each_line(FELF_READ_ONLY, mood_def.data, mood_def.size, - parse_mood_line, &mlpd); + m = alloc_new_mood("check"); + ret = mp_init(mood_def.data, mood_def.size, &m->parser_context, + &errmsg); 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); - - } + afs_error(aca, "%s: %s\n%s\n", mood_name, errmsg, + para_strerror(-ret)); + free(errmsg); + } else + destroy_mood(m); ret = 1; /* don't fail the loop on invalid mood definitions */ out: osl_close_disk_object(&mood_def); @@ -487,7 +208,7 @@ out: /** * Check all moods for syntax errors. * - * \param aca Only ->pbout is used for diagnostics. + * \param aca Output goes to ->pbout, errors to ->fd on the error band. * * \return Negative on fatal errors. Inconsistent mood definitions are not * considered an error. @@ -495,27 +216,67 @@ out: int mood_check_callback(struct afs_callback_arg *aca) { para_printf(&aca->pbout, "checking moods...\n"); - return osl(osl_rbtree_loop(moods_table, BLOBCOL_ID, &aca->pbout, - check_mood)); + return osl(osl_rbtree_loop(moods_table, BLOBCOL_ID, aca, 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); -} - -static long compute_score(struct afs_info *afsi, long mood_score) +/* + * 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, + const struct afs_statistics *stats) { - 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(stats->normalization_divisor > 0); + assert(stats->num > 0); + mean_n = stats->num_played_sum / stats->num; + mean_l = stats->last_played_sum / stats->num; + + score_n = -((int64_t)afsi->num_played - mean_n) + * stats->num_played_correction + / stats->normalization_divisor; + score_l = -((int64_t)afsi->last_played - mean_l) + * stats->last_played_correction + / stats->normalization_divisor; + return (score_n + score_l) / 2; } -static int add_afs_statistics(const struct osl_row *row) +static int add_afs_statistics(const struct osl_row *row, + struct afs_statistics *stats) { uint64_t n, x, s, q; struct afs_info afsi; @@ -524,120 +285,100 @@ static int add_afs_statistics(const struct osl_row *row) ret = get_afsi_of_row(row, &afsi); if (ret < 0) return ret; - n = statistics.num; + n = stats->num; x = afsi.last_played; - s = statistics.last_played_sum; + s = stats->last_played_sum; if (n > 0) { q = (x > s / n)? x - s / n : s / n - x; - statistics.last_played_qd += q * q * n / (n + 1); + stats->last_played_qd += q * q * n / (n + 1); } - statistics.last_played_sum += x; + stats->last_played_sum += x; x = afsi.num_played; - s = statistics.num_played_sum; + s = stats->num_played_sum; if (n > 0) { q = (x > s / n)? x - s / n : s / n - x; - statistics.num_played_qd += q * q * n / (n + 1); + stats->num_played_qd += q * q * n / (n + 1); } - statistics.num_played_sum += x; - statistics.num++; + stats->num_played_sum += x; + stats->num++; return 1; } static int del_afs_statistics(const struct osl_row *row) { + struct afs_statistics *stats = ¤t_mood->stats; uint64_t n, s, q, a, new_s; struct afs_info afsi; int ret; ret = get_afsi_of_row(row, &afsi); if (ret < 0) return ret; - n = statistics.num; + n = stats->num; assert(n); if (n == 1) { - memset(&statistics, 0, sizeof(statistics)); + memset(stats, 0, sizeof(*stats)); + stats->normalization_divisor = 1; return 1; } - s = statistics.last_played_sum; - q = statistics.last_played_qd; + s = stats->last_played_sum; + q = stats->last_played_qd; a = afsi.last_played; new_s = s - a; - statistics.last_played_sum = new_s; - statistics.last_played_qd = q + s * s / n - a * a + stats->last_played_sum = new_s; + stats->last_played_qd = q + s * s / n - a * a - new_s * new_s / (n - 1); - s = statistics.num_played_sum; - q = statistics.num_played_qd; + s = stats->num_played_sum; + q = stats->num_played_qd; a = afsi.num_played; new_s = s - a; - statistics.num_played_sum = new_s; - statistics.num_played_qd = q + s * s / n - a * a + stats->num_played_sum = new_s; + stats->num_played_qd = q + s * s / n - a * a - new_s * new_s / (n - 1); - statistics.num--; + stats->num--; return 1; } /* - * 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. - * - * 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 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. + * At mood load time we determine the set of admissible files for the given + * mood where each file is identified by a pointer to a row of the audio file + * table. In the first pass the pointers are added to a temporary array and + * statistics are computed. When all admissible files have been processed in + * this way, the score of each admissible file is computed and the (row, score) + * pair is added to the score table. This has to be done in a second pass + * since the score depends on the statistics. Finally, the array is freed. */ -struct admissible_file_info -{ - /** The admissible audio file. */ - struct osl_row *aft_row; - /** Its score. */ - long score; -}; - -/** The temporary array of admissible files. */ struct admissible_array { /** Files are admissible wrt. this mood. */ - struct mood *m; + struct mood_instance *m; /** The size of the array */ unsigned size; /** Pointer to the array of admissible files. */ - struct admissible_file_info *array; + struct osl_row **array; }; -/** - * Add an entry to the array of admissible files. - * - * \param aft_row The audio file to be added. - * \param private_data Pointer to a struct admissible_file_info. - * - * \return 1 if row admissible, 0 if not, negative on errors. +/* + * Check whether the given audio file is admissible. If it is, add it to array + * of admissible files. */ static int add_if_admissible(struct osl_row *aft_row, void *data) { struct admissible_array *aa = data; - int ret; - long score = 0; + struct afs_statistics *stats = &aa->m->stats; - ret = row_is_admissible(aft_row, aa->m, &score); - if (ret <= 0) - return ret; - if (statistics.num >= aa->size) { + if (!mp_eval_row(aft_row, aa->m->parser_context)) + return 0; + if (stats->num >= aa->size) { aa->size *= 2; aa->size += 100; - aa->array = para_realloc(aa->array, - aa->size * sizeof(struct admissible_file_info)); + aa->array = arr_realloc(aa->array, aa->size, + sizeof(struct osl_row *)); } - aa->array[statistics.num].aft_row = aft_row; - aa->array[statistics.num].score = score; - ret = add_afs_statistics(aft_row); - if (ret < 0) - return ret; - return 1; + aa->array[stats->num] = aft_row; + return add_afs_statistics(aft_row, stats); } /** @@ -682,29 +423,25 @@ _static_inline_ int64_t update_quadratic_deviation(int64_t n, int64_t old_qd, return old_qd + delta * (sigma - 2 * old_sum / n - delta / n); } -static int update_afs_statistics(struct afs_info *old_afsi, +static void update_afs_statistics(struct afs_info *old_afsi, struct afs_info *new_afsi) { - unsigned n; - int ret = get_num_admissible_files(&n); - - if (ret < 0) - return ret; - assert(n); - - statistics.last_played_qd = update_quadratic_deviation(n, - statistics.last_played_qd, old_afsi->last_played, - new_afsi->last_played, statistics.last_played_sum); - statistics.last_played_sum += new_afsi->last_played - old_afsi->last_played; - - statistics.num_played_qd = update_quadratic_deviation(n, - statistics.num_played_qd, old_afsi->num_played, - new_afsi->num_played, statistics.num_played_sum); - statistics.num_played_sum += new_afsi->num_played - old_afsi->num_played; - return 1; + struct afs_statistics *stats = ¤t_mood->stats; + + assert(stats->num > 0); + stats->last_played_qd = update_quadratic_deviation(stats->num, + stats->last_played_qd, old_afsi->last_played, + new_afsi->last_played, stats->last_played_sum); + stats->last_played_sum += new_afsi->last_played - old_afsi->last_played; + + stats->num_played_qd = update_quadratic_deviation(stats->num, + stats->num_played_qd, old_afsi->num_played, + new_afsi->num_played, stats->num_played_sum); + stats->num_played_sum += new_afsi->num_played - old_afsi->num_played; } -static int add_to_score_table(const struct osl_row *aft_row, long mood_score) +static int add_to_score_table(const struct osl_row *aft_row, + struct mood_instance *m) { long score; struct afs_info afsi; @@ -712,8 +449,8 @@ static int add_to_score_table(const struct osl_row *aft_row, long mood_score) if (ret < 0) return ret; - score = compute_score(&afsi, mood_score); - return score_add(aft_row, score); + score = compute_score(&afsi, &m->stats); + return score_add(aft_row, score, m->score_table); } static int delete_from_statistics_and_score_table(const struct osl_row *aft_row) @@ -725,23 +462,18 @@ static int delete_from_statistics_and_score_table(const struct osl_row *aft_row) } /** - * Delete one entry from the statistics and from the score table. + * Delete an audio file from the score table and update mood statistics. * - * \param aft_row The audio file which is no longer admissible. + * \param aft_row Identifies the row to delete. * - * \return Positive on success, negative on errors. + * \return Standard. * * \sa \ref score_delete(). */ static int mood_delete_audio_file(const struct osl_row *aft_row) { - int ret; - - ret = row_belongs_to_score_table(aft_row, NULL); - if (ret < 0) - return ret; - if (!ret) /* not admissible, nothing to do */ - return 1; + if (!row_belongs_to_score_table(aft_row)) + return 0; return delete_from_statistics_and_score_table(aft_row); } @@ -760,194 +492,225 @@ static int mood_update_audio_file(const struct osl_row *aft_row, struct afs_info *old_afsi) { long score, percent; - int ret, is_admissible, was_admissible = 0; + int ret; + bool is_admissible, was_admissible; struct afs_info afsi; - unsigned rank; if (!current_mood) return 1; /* nothing to do */ - ret = row_belongs_to_score_table(aft_row, &rank); - if (ret < 0) - return ret; - was_admissible = ret; - ret = row_is_admissible(aft_row, current_mood, &score); - if (ret < 0) - return ret; - is_admissible = (ret > 0); + was_admissible = row_belongs_to_score_table(aft_row); + is_admissible = mp_eval_row(aft_row, current_mood->parser_context); if (!was_admissible && !is_admissible) return 1; if (was_admissible && !is_admissible) return delete_from_statistics_and_score_table(aft_row); if (!was_admissible && is_admissible) { - ret = add_afs_statistics(aft_row); + ret = add_afs_statistics(aft_row, ¤t_mood->stats); if (ret < 0) return ret; - return add_to_score_table(aft_row, score); + return add_to_score_table(aft_row, current_mood); } /* update score */ ret = get_afsi_of_row(aft_row, &afsi); if (ret < 0) return ret; - if (old_afsi) { - ret = update_afs_statistics(old_afsi, &afsi); - if (ret < 0) - return ret; - } - score = compute_score(&afsi, score); + if (old_afsi) + update_afs_statistics(old_afsi, &afsi); + score = compute_score(&afsi, ¤t_mood->stats); PARA_DEBUG_LOG("score: %li\n", score); percent = (score + 100) / 3; if (percent > 100) percent = 100; else if (percent < 0) percent = 0; - PARA_DEBUG_LOG("moving from rank %u to %li%%\n", rank, percent); + PARA_DEBUG_LOG("moving to %li%%\n", percent); return score_update(aft_row, percent); } -static void log_statistics(void) +/* sse: seconds since epoch. */ +static char *get_statistics(struct mood_instance *m, int64_t sse) { - unsigned n = statistics.num; + unsigned n = m->stats.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_WARNING_LOG("no admissible files\n"); - 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; - 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)); + if (n == 0) + return make_message("no admissible files\n"); + mean_days = (sse - m->stats.last_played_sum / n) / 3600 / 24; + sigma_days = int_sqrt(m->stats.last_played_qd / n) / 3600 / 24; + return make_message( + "loaded mood %s (%u files)\n" + "last_played mean/sigma: %d/%d days\n" + "num_played mean/sigma: %" PRId64 "/%" PRIu64 "\n" + "correction factor ratio: %.2lf\n" + , + m->name? m->name : "(dummy)", + n, + mean_days, sigma_days, + m->stats.num_played_sum / n, + int_sqrt(m->stats.num_played_qd / n), + 86400.0 * m->stats.last_played_correction / + m->stats.num_played_correction + ); } /** - * Close the current mood. + * Free all resources of a mood instance. + * + * \param m As obtained by \ref mood_load(). If NULL, unload the current mood. * - * Frees all resources of the current mood. + * It's OK to call this with m == NULL even if no current mood is loaded. */ -void close_current_mood(void) +void mood_unload(struct mood_instance *m) { + if (m) + return destroy_mood(m); destroy_mood(current_mood); current_mood = NULL; - memset(&statistics, 0, sizeof(statistics)); +} + +static void compute_correction_factors(int64_t sse, struct afs_statistics *s) +{ + 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. + * Populate a score table with admissible files for the given mood. * - * \param mood_name The name of the mood to open. - * \param errmsg Error description is returned here. + * This consults the mood table to initialize the mood parser with the mood + * expression stored in the blob object which corresponds to the given name. A + * score table is allocated and populated with references to those entries of + * the audio file table which evaluate as admissible with respect to the mood + * expression. For each audio file a score value is computed and stored along + * with the file reference. * - * 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. + * \param mood_name The name of the mood to load. + * \param result Opaque, refers to the mood parser and the score table. + * \param msg Error message or mood info is returned here. * - * 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 the mood name is NULL, the dummy mood is loaded. This mood regards every + * audio file as admissible. * - * If there is already an open mood, it will be closed first. + * A NULL result pointer instructs the function to operate on the current mood. + * That is, on the mood instance which is used by the server to select the next + * audio file for streaming. In this mode of operation, the mood which was + * active before the call, if any, is unloaded on success. * - * \return Positive on success, negative on errors. + * If result is not NULL, the current mood is unaffected and *result points to + * an initialized mood instance on success. The caller can pass this reference + * to \ref mood_loop() to iterate over the admissible files, and should call + * \ref mood_unload() to free the mood instance afterwards. + * + * If the message pointer is not NULL, a suitable message is returned there in + * all cases. The caller must free this string. + * + * \return The number of admissible files on success, negative on errors. On + * errors, the current mood remains unaffected even if result is NULL. It is + * not considered an error if no files are admissible. * - * \sa struct \ref afs_info::last_played, \ref mp_eval_row(). + * \sa \ref mp_eval_row(). */ -int change_current_mood(const char *mood_name, char **errmsg) +int mood_load(const char *mood_name, struct mood_instance **result, char **msg) { int i, ret; - struct admissible_array aa = { - .size = 0, - .array = NULL - }; + struct admissible_array aa = {.size = 0}; + /* + * 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; - struct osl_row *row; - struct osl_object obj; - - if (!*mood_name) { - *errmsg = make_message("empty mood name"); - return -ERRNO_TO_PARA_ERROR(EINVAL); - } - obj.data = (char *)mood_name; - obj.size = strlen(mood_name) + 1; - ret = osl(osl_get_row(moods_table, BLOBCOL_NAME, &obj, &row)); - if (ret < 0) { - if (errmsg) - *errmsg = make_message("no such mood: %s", - mood_name); - return ret; - } - ret = load_mood(row, &m, errmsg); + ret = init_mood_parser(mood_name, &aa.m, msg); if (ret < 0) return ret; - close_current_mood(); - current_mood = m; - } else { /* load dummy mood */ - close_current_mood(); - current_mood = alloc_new_mood(NULL); - } - aa.m = current_mood; + } else /* load dummy mood */ + aa.m = alloc_new_mood(NULL); PARA_NOTICE_LOG("computing statistics of admissible files\n"); ret = audio_file_loop(&aa, add_if_admissible); if (ret < 0) { - if (errmsg) - *errmsg = make_message("audio file loop failed"); + if (msg) /* false if we are called via the event handler */ + *msg = make_message("audio file loop failed\n"); goto out; } - 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); + clock_get_realtime(&rnow); + compute_correction_factors(rnow.tv_sec, &aa.m->stats); + if (result) + score_open(&aa.m->score_table); + for (i = 0; i < aa.m->stats.num; i++) { + ret = add_to_score_table(aa.array[i], aa.m); if (ret < 0) { - if (errmsg) - *errmsg = make_message( - "could not add row to score table"); + if (msg) + *msg = make_message( + "could not add row to score table\n"); goto out; } } - log_statistics(); - ret = statistics.num; + /* success */ + if (msg) + *msg = get_statistics(aa.m, rnow.tv_sec); + ret = aa.m->stats.num; + if (result) + *result = aa.m; + else { + mood_unload(NULL); + current_mood = aa.m; + } + ret = 1; out: free(aa.array); - if (ret < 0) - close_current_mood(); + if (ret <= 0) /* error, or no admissible files */ + destroy_mood(aa.m); return ret; } -/* - * Close and re-open the current mood. +/** + * Iterate over the admissible files of a mood instance. + * + * This wrapper around \ref score_loop() is the mood counterpart of \ref + * playlist_loop(). * - * 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. + * \param m Determines the score table to iterate. Must not be NULL. + * \param func See \ref score_loop(). + * \param data See \ref score_loop(). * - * If no mood is currently open, the function returns success. + * \return See \ref score_loop(), \ref playlist_loop(). + */ +int mood_loop(struct mood_instance *m, osl_rbtree_loop_func *func, void *data) +{ + return score_loop(func, m->score_table, data); +} + +/* + * Empty the score table and start over. + * + * This function is called on events which render the current set of admissible + * files invalid, for example if an attribute is removed from the attribute + * table. */ 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; + assert(current_mood); + score_clear(); PARA_NOTICE_LOG("reloading %s\n", current_mood->name? current_mood->name : "(dummy)"); if (current_mood->name) mood_name = para_strdup(current_mood->name); - close_current_mood(); - ret = change_current_mood(mood_name, NULL); + mood_unload(NULL); + ret = mood_load(mood_name, NULL, NULL); free(mood_name); return ret; } @@ -959,9 +722,17 @@ static int reload_current_mood(void) * \param pb Unused. * \param data Its type depends on the event. * - * This function performs actions required due to the occurrence of the given - * event. Possible actions include reload of the current mood and update of the - * score of an audio file. + * This function updates the score table according to the event that has + * occurred. Two actions are possible: (a) reload the current mood, or (b) + * add/remove/update the row of the score table which corresponds to the audio + * file that has been modified or whose afs info has been changed. It depends + * on the type of the event which action (if any) is performed. + * + * The callbacks of command handlers such as com_add() or com_touch() which + * modify the audio file table call this function. The virtual streaming system + * also calls this after it has updated the afs info of the file it is about to + * stream (the one with the highest score). If the file stays admissible, its + * score is recomputed so that a different file is picked next time. * * \return Standard. */