Support decibel, percentage, and raw volumes in getvolume, setvolume, and getrange (#109)

* Use `pcmtype` keyword for range

Update methods that accept a `direction` argument (i.e.
capture/playback) to get this via positional _or_ keyword arguments.

Code using keyword arguments can be more robust; however the main reason
for this change is to prepare the way for an extra `units` argument to
many of these methods.

Update documentation to consistently use `pcmtype` instead of
a mixture of that and `direction`.

* Support units
This commit is contained in:
Chris Diamand
2022-03-28 20:46:40 +01:00
committed by GitHub
parent 4d9f6e5b50
commit 3f6fb9844d
3 changed files with 207 additions and 49 deletions

View File

@@ -28,6 +28,7 @@
#include <alsa/asoundlib.h>
#include <alsa/version.h>
#include <stdio.h>
#include <stdbool.h>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof *(a))
static const snd_pcm_format_t ALSAFormats[] = {
@@ -88,6 +89,12 @@ static const unsigned ALSARates[] = {
192000
};
typedef enum volume_units_t {
VOLUME_UNITS_PERCENTAGE,
VOLUME_UNITS_RAW,
VOLUME_UNITS_DB,
} volume_units_t;
PyDoc_STRVAR(alsaaudio_module_doc,
"This modules provides support for the ALSA audio API.\n"
"\n"
@@ -129,10 +136,13 @@ typedef struct {
unsigned int cchannels;
/* min and max values for playback and capture volumes */
long pmin;
long pmax;
long cmin;
long cmax;
long pmin, pmax;
long cmin, cmax;
/* min and max values for playback and capture volumes, in dB * 100 as
* reported by ALSA */
long pmin_dB, pmax_dB;
long cmin_dB, cmax_dB;
snd_mixer_t *handle;
} alsamixer_t;
@@ -181,6 +191,16 @@ get_pcmtype(PyObject *obj)
return -1;
}
static bool is_value_volume_unit(long unit)
{
if (unit == VOLUME_UNITS_PERCENTAGE ||
unit == VOLUME_UNITS_RAW ||
unit == VOLUME_UNITS_DB) {
return true;
}
return false;
}
static PyObject *
alsacard_list(PyObject *self, PyObject *args)
{
@@ -306,7 +326,7 @@ Return the card name and long name for card 'card_index'.");
static PyObject *
alsapcm_list(PyObject *self, PyObject *args)
alsapcm_list(PyObject *self, PyObject *args, PyObject *kwds)
{
PyObject *pcmtypeobj = NULL;
long pcmtype;
@@ -316,8 +336,11 @@ alsapcm_list(PyObject *self, PyObject *args)
char *name, *io;
const char *filter;
if (!PyArg_ParseTuple(args,"|O:pcms", &pcmtypeobj))
char *kw[] = { "pcmtype", NULL };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O:pcms", kw, &pcmtypeobj)) {
return NULL;
}
pcmtype = get_pcmtype(pcmtypeobj);
if (pcmtype < 0) {
@@ -2079,6 +2102,8 @@ alsamixer_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
}
snd_mixer_selem_get_playback_volume_range(elem, &self->pmin, &self->pmax);
snd_mixer_selem_get_capture_volume_range(elem, &self->cmin, &self->cmax);
snd_mixer_selem_get_playback_dB_range(elem, &self->pmin_dB, &self->pmax_dB);
snd_mixer_selem_get_capture_dB_range(elem, &self->cmin_dB, &self->cmax_dB);
return (PyObject *)self;
}
@@ -2350,18 +2375,22 @@ static long alsamixer_getphysvolume(long min, long max, int percentage)
}
static PyObject *
alsamixer_getvolume(alsamixer_t *self, PyObject *args)
alsamixer_getvolume(alsamixer_t *self, PyObject *args, PyObject *kwds)
{
snd_mixer_elem_t *elem;
int channel;
long ival;
PyObject *pcmtypeobj = NULL;
long pcmtype;
int iunits = VOLUME_UNITS_PERCENTAGE;
PyObject *result;
PyObject *item;
if (!PyArg_ParseTuple(args,"|O:getvolume", &pcmtypeobj))
char *kw[] = { "pcmtype", "units", NULL };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi:getvolume", kw, &pcmtypeobj, &iunits)) {
return NULL;
}
if (!self->handle)
{
@@ -2374,6 +2403,12 @@ alsamixer_getvolume(alsamixer_t *self, PyObject *args)
return NULL;
}
if (!is_value_volume_unit(iunits)) {
PyErr_SetString(ALSAAudioError, "Invalid volume units");
return NULL;
}
volume_units_t units = iunits;
elem = alsamixer_find_elem(self->handle,self->controlname,self->controlid);
if (!pcmtypeobj || (pcmtypeobj == Py_None)) {
@@ -2391,20 +2426,42 @@ alsamixer_getvolume(alsamixer_t *self, PyObject *args)
if (pcmtype == SND_PCM_STREAM_PLAYBACK &&
snd_mixer_selem_has_playback_channel(elem, channel))
{
snd_mixer_selem_get_playback_volume(elem, channel, &ival);
item = PyLong_FromLong(alsamixer_getpercentage(self->pmin,
self->pmax,
ival));
switch (units)
{
case VOLUME_UNITS_PERCENTAGE:
snd_mixer_selem_get_playback_volume(elem, channel, &ival);
ival = alsamixer_getpercentage(self->pmin, self->pmax, ival);
break;
case VOLUME_UNITS_RAW:
snd_mixer_selem_get_playback_volume(elem, channel, &ival);
break;
case VOLUME_UNITS_DB:
snd_mixer_selem_get_playback_dB(elem, channel, &ival);
break;
}
item = PyLong_FromLong(ival);
PyList_Append(result, item);
Py_DECREF(item);
}
else if (pcmtype == SND_PCM_STREAM_CAPTURE
&& snd_mixer_selem_has_capture_channel(elem, channel)
&& snd_mixer_selem_has_capture_volume(elem)) {
snd_mixer_selem_get_capture_volume(elem, channel, &ival);
item = PyLong_FromLong(alsamixer_getpercentage(self->cmin,
self->cmax,
ival));
switch (units)
{
case VOLUME_UNITS_PERCENTAGE:
snd_mixer_selem_get_capture_volume(elem, channel, &ival);
ival = alsamixer_getpercentage(self->cmin, self->cmax, ival);
break;
case VOLUME_UNITS_RAW:
snd_mixer_selem_get_capture_volume(elem, channel, &ival);
break;
case VOLUME_UNITS_DB:
snd_mixer_selem_get_capture_dB(elem, channel, &ival);
break;
}
item = PyLong_FromLong(ival);
PyList_Append(result, item);
Py_DECREF(item);
}
@@ -2426,13 +2483,17 @@ if the mixer has this capability, otherwise PCM_CAPTURE");
static PyObject *
alsamixer_getrange(alsamixer_t *self, PyObject *args)
alsamixer_getrange(alsamixer_t *self, PyObject *args, PyObject *kwds)
{
snd_mixer_elem_t *elem;
PyObject *pcmtypeobj = NULL;
int iunits = VOLUME_UNITS_RAW;
long pcmtype;
long min = -1, max = -1;
if (!PyArg_ParseTuple(args,"|O:getrange", &pcmtypeobj)) {
char *kw[] = { "pcmtype", "units", NULL };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi:getrange", kw, &pcmtypeobj, &iunits)) {
return NULL;
}
@@ -2447,6 +2508,12 @@ alsamixer_getrange(alsamixer_t *self, PyObject *args)
return NULL;
}
if (!is_value_volume_unit(iunits)) {
PyErr_SetString(ALSAAudioError, "Invalid volume units");
return NULL;
}
volume_units_t units = iunits;
elem = alsamixer_find_elem(self->handle, self->controlname,
self->controlid);
@@ -2464,7 +2531,22 @@ alsamixer_getrange(alsamixer_t *self, PyObject *args)
{
if (snd_mixer_selem_has_playback_channel(elem, 0))
{
return Py_BuildValue("[ii]", self->pmin, self->pmax);
switch (units)
{
case VOLUME_UNITS_PERCENTAGE:
min = 0;
max = 100;
break;
case VOLUME_UNITS_RAW:
min = self->pmin;
max = self->pmax;
break;
case VOLUME_UNITS_DB:
min = self->pmin_dB;
max = self->pmax_dB;
break;
}
return Py_BuildValue("[ii]", min, max);
}
PyErr_Format(ALSAAudioError, "Mixer %s,%d has no playback channel [%s]",
@@ -2475,7 +2557,22 @@ alsamixer_getrange(alsamixer_t *self, PyObject *args)
{
if (snd_mixer_selem_has_capture_channel(elem, 0)
&& snd_mixer_selem_has_capture_volume(elem)) {
return Py_BuildValue("[ii]", self->cmin, self->cmax);
switch (units)
{
case VOLUME_UNITS_PERCENTAGE:
min = 0;
max = 100;
break;
case VOLUME_UNITS_RAW:
min = self->cmin;
max = self->cmax;
break;
case VOLUME_UNITS_DB:
min = self->cmin_dB;
max = self->cmax_dB;
break;
}
return Py_BuildValue("[ii]", min, max);
}
PyErr_Format(ALSAAudioError, "Mixer %s,%d has no capture channel "
@@ -2494,7 +2591,7 @@ PyDoc_STRVAR(getrange_doc,
\n\
Returns a list of tuples with the volume range (ints).\n\
\n\
The optional 'direction' argument can be either PCM_PLAYBACK or\n\
The optional 'pcmtype' argument can be either PCM_PLAYBACK or\n\
PCM_CAPTURE, which is relevant if the mixer can control both\n\
playback and capture volume. The default value is 'playback'\n\
if the mixer has this capability, otherwise 'capture'");
@@ -2748,7 +2845,7 @@ This method will fail if the mixer has no capture switch capabilities.");
static PyObject *
alsamixer_setvolume(alsamixer_t *self, PyObject *args)
alsamixer_setvolume(alsamixer_t *self, PyObject *args, PyObject *kwds)
{
snd_mixer_elem_t *elem;
int i;
@@ -2756,16 +2853,14 @@ alsamixer_setvolume(alsamixer_t *self, PyObject *args)
int physvolume;
PyObject *pcmtypeobj = NULL;
long pcmtype;
int iunits = VOLUME_UNITS_PERCENTAGE;
int channel = MIXER_CHANNEL_ALL;
int done = 0;
if (!PyArg_ParseTuple(args,"l|iO:setvolume", &volume, &channel,
&pcmtypeobj))
return NULL;
char *kw[] = { "volume", "channel", "pcmtype", "units", NULL };
if (volume < 0 || volume > 100)
{
PyErr_SetString(ALSAAudioError, "Volume must be between 0 and 100");
if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|iOi:setvolume", kw, &volume,
&channel, &pcmtypeobj, &iunits)) {
return NULL;
}
@@ -2774,6 +2869,18 @@ alsamixer_setvolume(alsamixer_t *self, PyObject *args)
return NULL;
}
if (!is_value_volume_unit(iunits)) {
PyErr_SetString(ALSAAudioError, "Invalid volume units");
return NULL;
}
volume_units_t units = iunits;
if (units == VOLUME_UNITS_PERCENTAGE && (volume < 0 || volume > 100))
{
PyErr_SetString(ALSAAudioError, "Volume out of range");
return NULL;
}
if (!self->handle)
{
PyErr_SetString(ALSAAudioError, "Mixer is closed");
@@ -2796,18 +2903,40 @@ alsamixer_setvolume(alsamixer_t *self, PyObject *args)
{
if (pcmtype == SND_PCM_STREAM_PLAYBACK &&
snd_mixer_selem_has_playback_channel(elem, i)) {
physvolume = alsamixer_getphysvolume(self->pmin,
self->pmax, volume);
snd_mixer_selem_set_playback_volume(elem, i, physvolume);
switch (units)
{
case VOLUME_UNITS_PERCENTAGE:
physvolume = alsamixer_getphysvolume(self->pmin,
self->pmax, volume);
snd_mixer_selem_set_playback_volume(elem, i, physvolume);
break;
case VOLUME_UNITS_RAW:
snd_mixer_selem_set_playback_volume(elem, i, volume);
break;
case VOLUME_UNITS_DB:
snd_mixer_selem_set_playback_dB(elem, i, volume, 0);
break;
}
done++;
}
else if (pcmtype == SND_PCM_STREAM_CAPTURE
&& snd_mixer_selem_has_capture_channel(elem, i)
&& snd_mixer_selem_has_capture_volume(elem))
{
physvolume = alsamixer_getphysvolume(self->cmin, self->cmax,
volume);
snd_mixer_selem_set_capture_volume(elem, i, physvolume);
switch (units)
{
case VOLUME_UNITS_PERCENTAGE:
physvolume = alsamixer_getphysvolume(self->cmin, self->cmax,
volume);
snd_mixer_selem_set_capture_volume(elem, i, physvolume);
break;
case VOLUME_UNITS_RAW:
snd_mixer_selem_set_capture_volume(elem, i, volume);
break;
case VOLUME_UNITS_DB:
snd_mixer_selem_set_capture_dB(elem, i, volume, 0);
break;
}
done++;
}
}
@@ -2833,7 +2962,7 @@ If the optional argument channel is present, the volume is set only for\n\
this channel. This assumes that the mixer can control the volume for the\n\
channels independently.\n\
\n\
The optional direction argument can be either PCM_PLAYBACK or PCM_CAPTURE.\n\
The optional 'pcmtype' argument can be either PCM_PLAYBACK or PCM_CAPTURE.\n\
It is relevant if the mixer has independent playback and capture volume\n\
capabilities, and controls which of the volumes will be changed.\n\
The default is 'playback' if the mixer has this capability, otherwise\n\
@@ -3055,13 +3184,13 @@ static PyMethodDef alsamixer_methods[] = {
switchcap_doc},
{"volumecap", (PyCFunction)alsamixer_volumecap, METH_VARARGS,
volumecap_doc},
{"getvolume", (PyCFunction)alsamixer_getvolume, METH_VARARGS,
{"getvolume", (PyCFunction)alsamixer_getvolume, METH_VARARGS | METH_KEYWORDS,
getvolume_doc},
{"getrange", (PyCFunction)alsamixer_getrange, METH_VARARGS, getrange_doc},
{"getrange", (PyCFunction)alsamixer_getrange, METH_VARARGS | METH_KEYWORDS, getrange_doc},
{"getenum", (PyCFunction)alsamixer_getenum, METH_VARARGS, getenum_doc},
{"getmute", (PyCFunction)alsamixer_getmute, METH_VARARGS, getmute_doc},
{"getrec", (PyCFunction)alsamixer_getrec, METH_VARARGS, getrec_doc},
{"setvolume", (PyCFunction)alsamixer_setvolume, METH_VARARGS,
{"setvolume", (PyCFunction)alsamixer_setvolume, METH_VARARGS | METH_KEYWORDS,
setvolume_doc},
{"setenum", (PyCFunction)alsamixer_setenum, METH_VARARGS, setenum_doc},
{"setmute", (PyCFunction)alsamixer_setmute, METH_VARARGS, setmute_doc},
@@ -3137,7 +3266,7 @@ static PyMethodDef alsaaudio_methods[] = {
{ "card_indexes", (PyCFunction)alsacard_list_indexes, METH_VARARGS, card_indexes_doc},
{ "card_name", (PyCFunction)alsacard_name, METH_VARARGS, card_name_doc},
{ "cards", (PyCFunction)alsacard_list, METH_VARARGS, cards_doc},
{ "pcms", (PyCFunction)alsapcm_list, METH_VARARGS, pcms_doc},
{ "pcms", (PyCFunction)alsapcm_list, METH_VARARGS|METH_KEYWORDS, pcms_doc},
{ "mixers", (PyCFunction)alsamixer_list, METH_VARARGS|METH_KEYWORDS, mixers_doc},
{ 0, 0 },
};
@@ -3287,6 +3416,10 @@ PyObject *PyInit_alsaaudio(void)
_EXPORT_INT(m, "MIXER_SCHN_MONO", SND_MIXER_SCHN_MONO);
#endif
_EXPORT_INT(m, "VOLUME_UNITS_PERCENTAGE", VOLUME_UNITS_PERCENTAGE)
_EXPORT_INT(m, "VOLUME_UNITS_RAW", VOLUME_UNITS_RAW)
_EXPORT_INT(m, "VOLUME_UNITS_DB", VOLUME_UNITS_DB)
#if PY_MAJOR_VERSION >= 3
return m;
#endif

View File

@@ -33,13 +33,13 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
.. % should be enclosed in \var{...}.
.. function:: pcms([type=PCM_PLAYBACK])
.. function:: pcms(pcmtype=PCM_PLAYBACK)
List available PCM devices by name.
Arguments are:
* *type* - can be either :const:`PCM_CAPTURE` or :const:`PCM_PLAYBACK`
* *pcmtype* - can be either :const:`PCM_CAPTURE` or :const:`PCM_PLAYBACK`
(default).
**Note:**
@@ -466,11 +466,11 @@ Mixer objects have the following methods:
This method will fail if the mixer has no playback switch capabilities.
.. method:: Mixer.getrange([direction])
.. method:: Mixer.getrange(pcmtype=PCM_PLAYBACK)
Return the volume range of the ALSA mixer controlled by this object.
The optional *direction* argument can be either :const:`PCM_PLAYBACK` or
The optional *pcmtype* argument can be either :const:`PCM_PLAYBACK` or
:const:`PCM_CAPTURE`, which is relevant if the mixer can control both
playback and capture volume. The default value is :const:`PCM_PLAYBACK`
if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`.
@@ -484,18 +484,18 @@ Mixer objects have the following methods:
This method will fail if the mixer has no capture switch capabilities.
.. method:: Mixer.getvolume([direction])
.. method:: Mixer.getvolume(pcmtype=PCM_PLAYBACK)
Returns a list with the current volume settings for each channel. The list
elements are integer percentages.
The optional *direction* argument can be either :const:`PCM_PLAYBACK` or
The optional *pcmtype* argument can be either :const:`PCM_PLAYBACK` or
:const:`PCM_CAPTURE`, which is relevant if the mixer can control both
playback and capture volume. The default value is :const:`PCM_PLAYBACK`
if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`.
.. method:: Mixer.setvolume(volume, [channel], [direction])
.. method:: Mixer.setvolume(volume, channel=None, pcmtype=PCM_PLAYBACK)
Change the current volume settings for this mixer. The *volume* argument
controls the new volume setting as an integer percentage.
@@ -504,7 +504,7 @@ Mixer objects have the following methods:
only for this channel. This assumes that the mixer can control the
volume for the channels independently.
The optional *direction* argument can be either :const:`PCM_PLAYBACK` or
The optional *pcmtype* argument can be either :const:`PCM_PLAYBACK` or
:const:`PCM_CAPTURE`, which is relevant if the mixer can control both
playback and capture volume. The default value is :const:`PCM_PLAYBACK`
if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`.

View File

@@ -43,11 +43,36 @@ def show_mixer(name, kwargs):
sys.exit(1)
print("Mixer name: '%s'" % mixer.mixer())
print("Capabilities: %s %s" % (' '.join(mixer.volumecap()),
volcap = mixer.volumecap()
print("Capabilities: %s %s" % (' '.join(volcap),
' '.join(mixer.switchcap())))
if "Volume" in volcap or "Joined Volume" in volcap or "Playback Volume" in volcap:
pmin, pmax = mixer.getrange(alsaaudio.PCM_PLAYBACK)
pmin_keyword, pmax_keyword = mixer.getrange(pcmtype=alsaaudio.PCM_PLAYBACK, units=alsaaudio.VOLUME_UNITS_RAW)
pmin_default, pmax_default = mixer.getrange()
assert pmin == pmin_keyword
assert pmax == pmax_keyword
assert pmin == pmin_default
assert pmax == pmax_default
print("Raw playback volume range {}-{}".format(pmin, pmax))
pmin_dB, pmax_dB = mixer.getrange(units=alsaaudio.VOLUME_UNITS_DB)
print("dB playback volume range {}-{}".format(pmin_dB / 100.0, pmax_dB / 100.0))
if "Capture Volume" in volcap or "Joined Capture Volume" in volcap:
# Check that `getrange` works with keyword and positional arguments
cmin, cmax = mixer.getrange(alsaaudio.PCM_CAPTURE)
cmin_keyword, cmax_keyword = mixer.getrange(pcmtype=alsaaudio.PCM_CAPTURE, units=alsaaudio.VOLUME_UNITS_RAW)
assert cmin == cmin_keyword
assert cmax == cmax_keyword
print("Raw capture volume range {}-{}".format(cmin, cmax))
cmin_dB, cmax_dB = mixer.getrange(pcmtype=alsaaudio.PCM_CAPTURE, units=alsaaudio.VOLUME_UNITS_DB)
print("dB capture volume range {}-{}".format(cmin_dB / 100.0, cmax_dB / 100.0))
volumes = mixer.getvolume()
volumes_dB = mixer.getvolume(units=alsaaudio.VOLUME_UNITS_DB)
for i in range(len(volumes)):
print("Channel %i volume: %i%%" % (i,volumes[i]))
print("Channel %i volume: %i%% (%.1f dB)" % (i, volumes[i], volumes_dB[i] / 100.0))
try:
mutes = mixer.getmute()