diff --git a/alsaaudio.c b/alsaaudio.c index dd8821d..345edbe 100644 --- a/alsaaudio.c +++ b/alsaaudio.c @@ -28,6 +28,7 @@ #include #include #include +#include #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 diff --git a/doc/libalsaaudio.rst b/doc/libalsaaudio.rst index 4e9a4e9..0452549 100644 --- a/doc/libalsaaudio.rst +++ b/doc/libalsaaudio.rst @@ -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`. diff --git a/mixertest.py b/mixertest.py index 92e8353..3dad561 100755 --- a/mixertest.py +++ b/mixertest.py @@ -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()