From 4842077a1e62af4f6be22fc5479a0a3eef7e6045 Mon Sep 17 00:00:00 2001 From: Tanu Kaskinen Date: Fri, 11 Mar 2011 13:34:24 +0200 Subject: alsa-mixer: Add DecibelFix section to the profile set config file format. This commit only implements the parser, the decibel fix data is not yet used for anything. --- src/modules/alsa/alsa-mixer.c | 223 ++++++++++++++++++++++- src/modules/alsa/alsa-mixer.h | 19 ++ src/modules/alsa/mixer/profile-sets/default.conf | 61 +++++-- src/modules/alsa/module-alsa-card.c | 8 + 4 files changed, 297 insertions(+), 14 deletions(-) (limited to 'src/modules') diff --git a/src/modules/alsa/alsa-mixer.c b/src/modules/alsa/alsa-mixer.c index fa07674b..c0bbaa0a 100644 --- a/src/modules/alsa/alsa-mixer.c +++ b/src/modules/alsa/alsa-mixer.c @@ -2640,6 +2640,15 @@ static void profile_free(pa_alsa_profile *p) { pa_xfree(p); } +static void decibel_fix_free(pa_alsa_decibel_fix *db_fix) { + pa_assert(db_fix); + + pa_xfree(db_fix->name); + pa_xfree(db_fix->db_values); + + pa_xfree(db_fix); +} + void pa_alsa_profile_set_free(pa_alsa_profile_set *ps) { pa_assert(ps); @@ -2661,6 +2670,15 @@ void pa_alsa_profile_set_free(pa_alsa_profile_set *ps) { pa_hashmap_free(ps->mappings, NULL, NULL); } + if (ps->decibel_fixes) { + pa_alsa_decibel_fix *db_fix; + + while ((db_fix = pa_hashmap_steal_first(ps->decibel_fixes))) + decibel_fix_free(db_fix); + + pa_hashmap_free(ps->decibel_fixes, NULL, NULL); + } + pa_xfree(ps); } @@ -2705,6 +2723,26 @@ static pa_alsa_profile *profile_get(pa_alsa_profile_set *ps, const char *name) { return p; } +static pa_alsa_decibel_fix *decibel_fix_get(pa_alsa_profile_set *ps, const char *name) { + pa_alsa_decibel_fix *db_fix; + + if (!pa_startswith(name, "DecibelFix ")) + return NULL; + + name += 11; + + if ((db_fix = pa_hashmap_get(ps->decibel_fixes, name))) + return db_fix; + + db_fix = pa_xnew0(pa_alsa_decibel_fix, 1); + db_fix->profile_set = ps; + db_fix->name = pa_xstrdup(name); + + pa_hashmap_put(ps->decibel_fixes, db_fix->name, db_fix); + + return db_fix; +} + static int mapping_parse_device_strings( const char *filename, unsigned line, @@ -2975,6 +3013,130 @@ static int profile_parse_skip_probe( return 0; } +static int decibel_fix_parse_db_values( + const char *filename, + unsigned line, + const char *section, + const char *lvalue, + const char *rvalue, + void *data, + void *userdata) { + + pa_alsa_profile_set *ps = userdata; + pa_alsa_decibel_fix *db_fix; + char **items; + char *item; + long *db_values; + unsigned n = 8; /* Current size of the db_values table. */ + unsigned min_step = 0; + unsigned max_step = 0; + unsigned i = 0; /* Index to the items table. */ + unsigned prev_step = 0; + double prev_db = 0; + + pa_assert(filename); + pa_assert(section); + pa_assert(lvalue); + pa_assert(rvalue); + pa_assert(ps); + + if (!(db_fix = decibel_fix_get(ps, section))) { + pa_log("[%s:%u] %s invalid in section %s", filename, line, lvalue, section); + return -1; + } + + if (!(items = pa_split_spaces_strv(rvalue))) { + pa_log("[%s:%u] Value missing", pa_strnull(filename), line); + return -1; + } + + db_values = pa_xnew(long, n); + + while ((item = items[i++])) { + char *s = item; /* Step value string. */ + char *d = item; /* dB value string. */ + uint32_t step; + double db; + + /* Move d forward until it points to a colon or to the end of the item. */ + for (; *d && *d != ':'; ++d); + + if (d == s) { + /* item started with colon. */ + pa_log("[%s:%u] No step value found in %s", filename, line, item); + goto fail; + } + + if (!*d || !*(d + 1)) { + /* No colon found, or it was the last character in item. */ + pa_log("[%s:%u] No dB value found in %s", filename, line, item); + goto fail; + } + + /* pa_atou() needs a null-terminating string. Let's replace the colon + * with a zero byte. */ + *d++ = '\0'; + + if (pa_atou(s, &step) < 0) { + pa_log("[%s:%u] Invalid step value: %s", filename, line, s); + goto fail; + } + + if (pa_atod(d, &db) < 0) { + pa_log("[%s:%u] Invalid dB value: %s", filename, line, d); + goto fail; + } + + if (step <= prev_step && i != 1) { + pa_log("[%s:%u] Step value %u not greater than the previous value %u", filename, line, step, prev_step); + goto fail; + } + + if (db < prev_db && i != 1) { + pa_log("[%s:%u] Decibel value %0.2f less than the previous value %0.2f", filename, line, db, prev_db); + goto fail; + } + + if (i == 1) { + min_step = step; + db_values[0] = (long) (db * 100.0); + prev_step = step; + prev_db = db; + } else { + /* Interpolate linearly. */ + double db_increment = (db - prev_db) / (step - prev_step); + + for (; prev_step < step; ++prev_step, prev_db += db_increment) { + + /* Reallocate the db_values table if it's about to overflow. */ + if (prev_step + 1 - min_step == n) { + n *= 2; + db_values = pa_xrenew(long, db_values, n); + } + + db_values[prev_step + 1 - min_step] = (long) ((prev_db + db_increment) * 100.0); + } + } + + max_step = step; + } + + db_fix->min_step = min_step; + db_fix->max_step = max_step; + pa_xfree(db_fix->db_values); + db_fix->db_values = db_values; + + pa_xstrfreev(items); + + return 0; + +fail: + pa_xstrfreev(items); + pa_xfree(db_values); + + return -1; +} + static int mapping_verify(pa_alsa_mapping *m, const pa_channel_map *bonus) { static const struct description_map well_known_descriptions[] = { @@ -3012,7 +3174,7 @@ static int mapping_verify(pa_alsa_mapping *m, const pa_channel_map *bonus) { if ((m->input_path_names && m->input_element) || (m->output_path_names && m->output_element)) { - pa_log("Mapping %s must have either mixer path or mixer elment, not both.", m->name); + pa_log("Mapping %s must have either mixer path or mixer element, not both.", m->name); return -1; } @@ -3257,10 +3419,52 @@ void pa_alsa_profile_dump(pa_alsa_profile *p) { pa_log_debug("Output %s", m->name); } +static int decibel_fix_verify(pa_alsa_decibel_fix *db_fix) { + pa_assert(db_fix); + + /* Check that the dB mapping has been configured. Since "db-values" is + * currently the only option in the DecibelFix section, and decibel fix + * objects don't get created if a DecibelFix section is empty, this is + * actually a redundant check. Having this may prevent future bugs, + * however. */ + if (!db_fix->db_values) { + pa_log("Decibel fix for element %s lacks the dB values.", db_fix->name); + return -1; + } + + return 0; +} + +void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix) { + char *db_values = NULL; + + pa_assert(db_fix); + + if (db_fix->db_values) { + pa_strbuf *buf; + long i; + long max_i = db_fix->max_step - db_fix->min_step; + + buf = pa_strbuf_new(); + pa_strbuf_printf(buf, "[%li]:%0.2f", db_fix->min_step, db_fix->db_values[0] / 100.0); + + for (i = 1; i <= max_i; ++i) + pa_strbuf_printf(buf, " [%li]:%0.2f", i + db_fix->min_step, db_fix->db_values[i] / 100.0); + + db_values = pa_strbuf_tostring_free(buf); + } + + pa_log_debug("Decibel fix %s, min_step=%li, max_step=%li, db_values=%s", + db_fix->name, db_fix->min_step, db_fix->max_step, pa_strnull(db_values)); + + pa_xfree(db_values); +} + pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus) { pa_alsa_profile_set *ps; pa_alsa_profile *p; pa_alsa_mapping *m; + pa_alsa_decibel_fix *db_fix; char *fn; int r; void *state; @@ -3286,12 +3490,16 @@ pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel { "input-mappings", profile_parse_mappings, NULL, NULL }, { "output-mappings", profile_parse_mappings, NULL, NULL }, { "skip-probe", profile_parse_skip_probe, NULL, NULL }, + + /* [DecibelFix ...] */ + { "db-values", decibel_fix_parse_db_values, NULL, NULL }, { NULL, NULL, NULL, NULL } }; ps = pa_xnew0(pa_alsa_profile_set, 1); ps->mappings = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); ps->profiles = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + ps->decibel_fixes = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); items[0].data = &ps->auto_profiles; @@ -3321,6 +3529,10 @@ pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel if (profile_verify(p) < 0) goto fail; + PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state) + if (decibel_fix_verify(db_fix) < 0) + goto fail; + return ps; fail: @@ -3501,23 +3713,28 @@ void pa_alsa_profile_set_probe( void pa_alsa_profile_set_dump(pa_alsa_profile_set *ps) { pa_alsa_profile *p; pa_alsa_mapping *m; + pa_alsa_decibel_fix *db_fix; void *state; pa_assert(ps); - pa_log_debug("Profile set %p, auto_profiles=%s, probed=%s, n_mappings=%u, n_profiles=%u", + pa_log_debug("Profile set %p, auto_profiles=%s, probed=%s, n_mappings=%u, n_profiles=%u, n_decibel_fixes=%u", (void*) ps, pa_yes_no(ps->auto_profiles), pa_yes_no(ps->probed), pa_hashmap_size(ps->mappings), - pa_hashmap_size(ps->profiles)); + pa_hashmap_size(ps->profiles), + pa_hashmap_size(ps->decibel_fixes)); PA_HASHMAP_FOREACH(m, ps->mappings, state) pa_alsa_mapping_dump(m); PA_HASHMAP_FOREACH(p, ps->profiles, state) pa_alsa_profile_dump(p); + + PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state) + pa_alsa_decibel_fix_dump(db_fix); } void pa_alsa_add_ports(pa_hashmap **p, pa_alsa_path_set *ps) { diff --git a/src/modules/alsa/alsa-mixer.h b/src/modules/alsa/alsa-mixer.h index 0fcfc0a4..75da1389 100644 --- a/src/modules/alsa/alsa-mixer.h +++ b/src/modules/alsa/alsa-mixer.h @@ -46,6 +46,7 @@ typedef struct pa_alsa_path pa_alsa_path; typedef struct pa_alsa_path_set pa_alsa_path_set; typedef struct pa_alsa_mapping pa_alsa_mapping; typedef struct pa_alsa_profile pa_alsa_profile; +typedef struct pa_alsa_decibel_fix pa_alsa_decibel_fix; typedef struct pa_alsa_profile_set pa_alsa_profile_set; typedef struct pa_alsa_port_data pa_alsa_port_data; @@ -266,9 +267,26 @@ struct pa_alsa_profile { pa_idxset *output_mappings; }; +struct pa_alsa_decibel_fix { + pa_alsa_profile_set *profile_set; + + char *name; /* Alsa volume element name. */ + long min_step; + long max_step; + + /* An array that maps alsa volume element steps to decibels. The steps can + * be used as indices to this array, after substracting min_step from the + * real value. + * + * The values are actually stored as integers representing millibels, + * because that's the format the alsa API uses. */ + long *db_values; +}; + struct pa_alsa_profile_set { pa_hashmap *mappings; pa_hashmap *profiles; + pa_hashmap *decibel_fixes; pa_bool_t auto_profiles; pa_bool_t probed:1; @@ -276,6 +294,7 @@ struct pa_alsa_profile_set { void pa_alsa_mapping_dump(pa_alsa_mapping *m); void pa_alsa_profile_dump(pa_alsa_profile *p); +void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix); pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus); void pa_alsa_profile_set_probe(pa_alsa_profile_set *ps, const char *dev_id, const pa_sample_spec *ss, unsigned default_n_fragments, unsigned default_fragment_size_msec); diff --git a/src/modules/alsa/mixer/profile-sets/default.conf b/src/modules/alsa/mixer/profile-sets/default.conf index cf207bdb..9f7b5f2b 100644 --- a/src/modules/alsa/mixer/profile-sets/default.conf +++ b/src/modules/alsa/mixer/profile-sets/default.conf @@ -16,17 +16,27 @@ ; Default profile definitions for the ALSA backend of PulseAudio. This ; is used as fallback for all cards that have no special mapping -; assigned. (and should be good enough for the vast majority of -; cards). Use the udev property PULSE_PROFILE_SET to assign a -; different profile set than this one to a device. So what is this -; about? Simply, what we do here is map ALSA devices to how they are -; exposed in PA. We say which ALSA device string to use to open a -; device, which channel mapping to use then, and which mixer path to -; use. This is encoded in a 'mapping'. Multiple of these mappings can -; be bound together in a 'profile' which is then directly exposed in -; the UI as a card profile. Each mapping assigned to a profile will -; result in one sink/source to be created if the profile is selected -; for the card. +; assigned (and should be good enough for the vast majority of +; cards). If you want to assign a different profile set than this one +; to a device, either set the udev property PULSE_PROFILE_SET for the +; card, or use the "profile_set" module argument when loading +; module-alsa-card. +; +; So what is this about? Simply, what we do here is map ALSA devices +; to how they are exposed in PA. We say which ALSA device string to +; use to open a device, which channel mapping to use then, and which +; mixer path to use. This is encoded in a 'mapping'. Multiple of these +; mappings can be bound together in a 'profile' which is then directly +; exposed in the UI as a card profile. Each mapping assigned to a +; profile will result in one sink/source to be created if the profile +; is selected for the card. +; +; Additionally, the path set configuration files can describe the +; decibel values assigned to the steps of the volume elements. This +; can be used to work around situations when the alsa driver doesn't +; provide any decibel information, or when the information is +; incorrect. + ; [General] ; auto-profiles = no | yes # Instead of defining all profiles manually, autogenerate @@ -55,6 +65,35 @@ ; skip-probe = no | yes # Skip probing for availability? If this is yes then this profile ; # will be assumed as working without probing. Makes initialization ; # a bit faster but only works if the card is really known well. +; +; [DecibelFix element] # Decibel fixes can be used to work around missing or incorrect dB +; # information from alsa. A decibel fix is a table that maps volume steps +; # to decibel values for one volume element. The "element" part in the +; # section title is the name of the volume element. +; # +; # NOTE: This feature is meant just as a help for figuring out the correct +; # decibel values. Pulseaudio is not the correct place to maintain the +; # decibel mappings! +; # +; # If you need this feature, then you should make sure that when you have +; # the correct values figured out, the alsa driver developers get informed +; # too, so that they can fix the driver. +; +; db-values = ... # The option value consists of pairs of step numbers and decibel values. +; # The pairs are separated with whitespace, and steps are separated from +; # the corresponding decibel values with a colon. The values must be in an +; # increasing order. Here's an example of a valid string: +; # +; # "0:-40.50 1:-38.70 3:-33.00 11:0" +; # +; # The lowest step imposes a lower limit for hardware volume and the +; # highest step correspondingly imposes a higher limit. That means that +; # that the mixer will never be set outside those values - the rest of the +; # volume scale is done using software volume. +; # +; # As can be seen in the example, you don't need to specify a dB value for +; # each step. The dB values for skipped steps will be linearly interpolated +; # using the nearest steps that are given. [General] auto-profiles = yes diff --git a/src/modules/alsa/module-alsa-card.c b/src/modules/alsa/module-alsa-card.c index 3f8576d7..c2e5ffaf 100644 --- a/src/modules/alsa/module-alsa-card.c +++ b/src/modules/alsa/module-alsa-card.c @@ -400,6 +400,14 @@ int pa__init(pa_module *m) { if (reserve) pa_reserve_wrapper_unref(reserve); + if (!pa_hashmap_isempty(u->profile_set->decibel_fixes)) + pa_log_warn("Card %s uses decibel fixes (i.e. overrides the decibel information for some alsa volume elements). " + "Please note that this feature is meant just as a help for figuring out the correct decibel values. " + "Pulseaudio is not the correct place to maintain the decibel mappings! The fixed decibel values " + "should be sent to ALSA developers so that they can fix the driver. If it turns out that this feature " + "is abused (i.e. fixes are not pushed to ALSA), the decibel fix feature may be removed in some future " + "Pulseaudio version.", u->card->name); + return 0; fail: -- cgit