module/alsa: use channel’s dB range instead of raw volume, if available

For channels that have a defined dB range, use that instead of the raw
volume range when calculating the volume percent.

Also use the same logic as alsamixer when calculating the percent from
the dB values: assume a linear scale if the dB range is “small
enough”, and otherwise normalize it against a logarithmic scale.

With this, yambar’s “percent” value matches alsamixer’s exactly.

The ‘volume’ tag remains unchanged - it always reflects the raw volume
values.

Instead, we add a new tag ‘dB’, that reflects the dB values.

Closes #202
This commit is contained in:
Daniel Eklöf 2022-06-16 18:24:42 +02:00
parent a0c07d7836
commit 6c10eb2153
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
4 changed files with 141 additions and 20 deletions

View file

@ -25,10 +25,11 @@
* network: request link stats and expose under tags `dl-speed` and * network: request link stats and expose under tags `dl-speed` and
`ul-speed` when `poll-interval` is set. `ul-speed` when `poll-interval` is set.
* new module: disk-io. * new module: disk-io.
* alsa: `dB` tag ([#202][202])
[153]: https://codeberg.org/dnkl/yambar/issues/153 [153]: https://codeberg.org/dnkl/yambar/issues/153
[159]: https://codeberg.org/dnkl/yambar/issues/159 [159]: https://codeberg.org/dnkl/yambar/issues/159
[202]: https://codeberg.org/dnkl/yambar/issues/202
### Changed ### Changed
@ -42,6 +43,8 @@
(e.g. `string: {text: "{tx-bitrate:mb}"}`). (e.g. `string: {text: "{tx-bitrate:mb}"}`).
* i3: newly created, and **unfocused** workspaces are now considered * i3: newly created, and **unfocused** workspaces are now considered
non-empty ([#191][191]) non-empty ([#191][191])
* alsa: use dB instead of raw volume values, if possible, when
calculating the `percent` tag ([#202][202])
* **BREAKING CHANGE**: overhaul of the `map` particle. Instead of * **BREAKING CHANGE**: overhaul of the `map` particle. Instead of
specifying a `tag` and then an array of `values`, you must now specifying a `tag` and then an array of `values`, you must now
simply use an array of `conditions`, that consist of: simply use an array of `conditions`, that consist of:
@ -83,6 +86,7 @@
[137]: https://codeberg.org/dnkl/yambar/issues/137 [137]: https://codeberg.org/dnkl/yambar/issues/137
[175]: https://codeberg.org/dnkl/yambar/issues/172 [175]: https://codeberg.org/dnkl/yambar/issues/172
[191]: https://codeberg.org/dnkl/yambar/issues/191 [191]: https://codeberg.org/dnkl/yambar/issues/191
[202]: https://codeberg.org/dnkl/yambar/issues/202
### Deprecated ### Deprecated

View file

@ -11,12 +11,17 @@ alsa - Monitors an alsa soundcard for volume and mute/unmute changes
| online | online
: bool : bool
: True when the ALSA device has successfully been opened : True when the ALSA device has successfully been opened
| dB
: range
: Volume level (in dB), with min and max as start and end range
values.
| volume | volume
: range : range
: Volume level, with min and max as start and end range values : Volume level (raw), with min and max as start and end range values
| percent | percent
: range : range
: Volume level, as a percentage : Volume level, as a percentage. This value is based on the *dB* tag
if available, otherwise the *volume* tag.
| muted | muted
: bool : bool
: True if muted, otherwise false : True if muted, otherwise false

View file

@ -215,7 +215,7 @@ bar:
muted: {string: {text: , font: *awesome, foreground: ffffff66}} muted: {string: {text: , font: *awesome, foreground: ffffff66}}
~muted: ~muted:
ramp: ramp:
tag: volume tag: percent
items: items:
- string: {text: , font: *awesome} - string: {text: , font: *awesome}
- string: {text: , font: *awesome} - string: {text: , font: *awesome}

View file

@ -23,7 +23,9 @@ struct channel {
enum channel_type type; enum channel_type type;
char *name; char *name;
bool use_db;
long vol_cur; long vol_cur;
long db_cur;
bool muted; bool muted;
}; };
@ -42,10 +44,18 @@ struct private {
long playback_vol_min; long playback_vol_min;
long playback_vol_max; long playback_vol_max;
bool has_playback_db;
long playback_db_min;
long playback_db_max;
bool has_capture_volume; bool has_capture_volume;
long capture_vol_min; long capture_vol_min;
long capture_vol_max; long capture_vol_max;
long has_capture_db;
long capture_db_min;
long capture_db_max;
const struct channel *volume_chan; const struct channel *volume_chan;
const struct channel *muted_chan; const struct channel *muted_chan;
}; };
@ -94,30 +104,57 @@ content(struct module *mod)
bool muted = muted_chan != NULL ? muted_chan->muted : false; bool muted = muted_chan != NULL ? muted_chan->muted : false;
long vol_min = 0, vol_max = 0, vol_cur = 0; long vol_min = 0, vol_max = 0, vol_cur = 0;
long db_min = 0, db_max = 0, db_cur = 0;
bool use_db = false;
if (volume_chan != NULL) { if (volume_chan != NULL) {
if (volume_chan->type == CHANNEL_PLAYBACK) { if (volume_chan->type == CHANNEL_PLAYBACK) {
db_min = m->playback_db_min;
db_max = m->playback_db_max;
vol_min = m->playback_vol_min; vol_min = m->playback_vol_min;
vol_max = m->playback_vol_max; vol_max = m->playback_vol_max;
} else { } else {
db_min = m->capture_db_min;
db_max = m->capture_db_max;
vol_min = m->capture_vol_min; vol_min = m->capture_vol_min;
vol_max = m->capture_vol_max; vol_max = m->capture_vol_max;
} }
vol_cur = volume_chan->vol_cur; vol_cur = volume_chan->vol_cur;
db_cur = volume_chan->db_cur;
use_db = volume_chan->use_db;
} }
int percent = vol_max - vol_min > 0 int percent;
? round(100. * vol_cur / (vol_max - vol_min))
if (use_db) {
bool use_linear = db_max - db_min <= 24 * 100;
if (use_linear) {
percent = db_min - db_max > 0
? round(100. * (db_cur - db_min) / (db_max - db_min))
: 0; : 0;
} else {
double normalized = pow(10, (double)(db_cur - db_max) / 6000.);
if (db_min != SND_CTL_TLV_DB_GAIN_MUTE) {
double min_norm = pow(10, (double)(db_min - db_max) / 6000.);
normalized = (normalized - min_norm) / (1. - min_norm);
}
percent = round(100. * normalized);
}
} else {
percent = vol_max - vol_min > 0
? round(100. * (vol_cur - vol_min) / (vol_max - vol_min))
: 0;
}
struct tag_set tags = { struct tag_set tags = {
.tags = (struct tag *[]){ .tags = (struct tag *[]){
tag_new_bool(mod, "online", m->online), tag_new_bool(mod, "online", m->online),
tag_new_int_range(mod, "volume", vol_cur, vol_min, vol_max), tag_new_int_range(mod, "volume", vol_cur, vol_min, vol_max),
tag_new_int_range(mod, "dB", db_cur, db_min, db_max),
tag_new_int_range(mod, "percent", percent, 0, 100), tag_new_int_range(mod, "percent", percent, 0, 100),
tag_new_bool(mod, "muted", muted), tag_new_bool(mod, "muted", muted),
}, },
.count = 4, .count = 5,
}; };
mtx_unlock(&mod->lock); mtx_unlock(&mod->lock);
@ -132,6 +169,8 @@ update_state(struct module *mod, snd_mixer_elem_t *elem)
{ {
struct private *m = mod->private; struct private *m = mod->private;
mtx_lock(&mod->lock);
/* If volume level can be changed (i.e. this isn't just a switch; /* If volume level can be changed (i.e. this isn't just a switch;
* e.g. a digital channel), get current channel levels */ * e.g. a digital channel), get current channel levels */
tll_foreach(m->channels, it) { tll_foreach(m->channels, it) {
@ -139,14 +178,57 @@ update_state(struct module *mod, snd_mixer_elem_t *elem)
const bool has_volume = chan->type == CHANNEL_PLAYBACK const bool has_volume = chan->type == CHANNEL_PLAYBACK
? m->has_playback_volume : m->has_capture_volume; ? m->has_playback_volume : m->has_capture_volume;
const bool has_db = chan->type == CHANNEL_PLAYBACK
? m->has_playback_db : m->has_capture_db;
if (!has_volume && !has_db)
continue;
if (has_db) {
chan->use_db = true;
const long min = chan->type == CHANNEL_PLAYBACK
? m->playback_db_min : m->capture_db_min;
const long max = chan->type == CHANNEL_PLAYBACK
? m->playback_db_max : m->capture_db_max;
assert(min <= max);
int r = chan->type == CHANNEL_PLAYBACK
? snd_mixer_selem_get_playback_dB(elem, chan->id, &chan->db_cur)
: snd_mixer_selem_get_capture_dB(elem, chan->id, &chan->db_cur);
if (r < 0) {
LOG_ERR("%s,%s: %s: failed to get current dB",
m->card, m->mixer, chan->name);
}
if (chan->db_cur < min) {
LOG_WARN(
"%s,%s: %s: current dB is less than the indicated minimum: "
"%ld < %ld", m->card, m->mixer, chan->name, chan->db_cur, min);
chan->db_cur = min;
}
if (chan->db_cur > max) {
LOG_WARN(
"%s,%s: %s: current dB is greater than the indicated maximum: "
"%ld > %ld", m->card, m->mixer, chan->name, chan->db_cur, max);
chan->db_cur = max;
}
assert(chan->db_cur >= min);
assert(chan->db_cur <= max );
LOG_DBG("%s,%s: %s: dB: %ld",
m->card, m->mixer, chan->name, chan->db_cur);
} else
chan->use_db = false;
const long min = chan->type == CHANNEL_PLAYBACK const long min = chan->type == CHANNEL_PLAYBACK
? m->playback_vol_min : m->capture_vol_min; ? m->playback_vol_min : m->capture_vol_min;
const long max = chan->type == CHANNEL_PLAYBACK const long max = chan->type == CHANNEL_PLAYBACK
? m->playback_vol_max : m->capture_vol_max; ? m->playback_vol_max : m->capture_vol_max;
if (!has_volume)
continue;
assert(min <= max); assert(min <= max);
int r = chan->type == CHANNEL_PLAYBACK int r = chan->type == CHANNEL_PLAYBACK
@ -199,10 +281,9 @@ update_state(struct module *mod, snd_mixer_elem_t *elem)
LOG_DBG("%s,%s: %s: muted: %d", m->card, m->mixer, chan->name, !unmuted); LOG_DBG("%s,%s: %s: muted: %d", m->card, m->mixer, chan->name, !unmuted);
} }
mtx_lock(&mod->lock);
m->online = true; m->online = true;
mtx_unlock(&mod->lock);
mtx_unlock(&mod->lock);
mod->bar->refresh(mod->bar); mod->bar->refresh(mod->bar);
} }
@ -269,6 +350,16 @@ run_while_online(struct module *mod)
} }
} }
if (snd_mixer_selem_get_playback_dB_range(
elem, &m->playback_db_min, &m->playback_db_max) < 0)
{
LOG_WARN(
"%s,%s: failed to get playback dB range, "
"will use raw volume values instead", m->card, m->mixer);
m->has_playback_db = false;
} else
m->has_playback_db = true;
/* Get capture volume range */ /* Get capture volume range */
m->has_capture_volume = snd_mixer_selem_has_capture_volume(elem) > 0; m->has_capture_volume = snd_mixer_selem_has_capture_volume(elem) > 0;
if (m->has_capture_volume) { if (m->has_capture_volume) {
@ -290,6 +381,16 @@ run_while_online(struct module *mod)
} }
} }
if (snd_mixer_selem_get_capture_dB_range(
elem, &m->capture_db_min, &m->capture_db_max) < 0)
{
LOG_WARN(
"%s,%s: failed to get capture dB range, "
"will use raw volume values instead", m->card, m->mixer);
m->has_capture_db = false;
} else
m->has_capture_db = true;
/* Get available channels */ /* Get available channels */
for (size_t i = 0; i < SND_MIXER_SCHN_LAST; i++) { for (size_t i = 0; i < SND_MIXER_SCHN_LAST; i++) {
bool is_playback = snd_mixer_selem_has_playback_channel(elem, i) == 1; bool is_playback = snd_mixer_selem_has_playback_channel(elem, i) == 1;
@ -361,13 +462,24 @@ run_while_online(struct module *mod)
update_state(mod, elem); update_state(mod, elem);
LOG_INFO( LOG_INFO(
"%s,%s: volume range=%ld-%ld, current=%ld%s (sources: volume=%s, muted=%s)", "%s,%s: %s range=%ld-%ld, current=%ld%s (sources: volume=%s, muted=%s)",
m->card, m->mixer, m->card, m->mixer,
m->volume_chan->type == CHANNEL_PLAYBACK m->volume_chan->use_db ? "dB" : "volume",
? m->playback_vol_min : m->capture_vol_min, (m->volume_chan->type == CHANNEL_PLAYBACK
m->volume_chan->type == CHANNEL_PLAYBACK ? (m->volume_chan->use_db
? m->playback_vol_max : m->capture_vol_max, ? m->playback_db_min
m->volume_chan->vol_cur, : m->playback_vol_min)
: (m->volume_chan->use_db
? m->capture_db_min
: m->capture_vol_min)),
(m->volume_chan->type == CHANNEL_PLAYBACK
? (m->volume_chan->use_db
? m->playback_db_max
: m->playback_vol_max)
: (m->volume_chan->use_db
? m->capture_db_max
: m->capture_vol_max)),
m->volume_chan->use_db ? m->volume_chan->db_cur : m->volume_chan->vol_cur,
m->muted_chan->muted ? " (muted)" : "", m->muted_chan->muted ? " (muted)" : "",
m->volume_chan->name, m->muted_chan->name); m->volume_chan->name, m->muted_chan->name);