Compare commits

...

3 Commits

Author SHA1 Message Date
Lars Immisch
917a11b398 Report volume like alsamixer. Maybe.
I have no idea how they calculate dB.
2017-11-07 00:57:02 +01:00
Lars Immisch
ce84e69cc1 Fix kwargs, and modernize mixertest.py a bit
Preliminary error handling for dB volume settings
2017-11-06 23:32:24 +01:00
Lars Immisch
c2cfe0211b Add setting/getting volume in dB.
Potentially breaking change: getvolume now always returns a list of float values,
not integers as before.
2017-11-03 00:01:56 +01:00
3 changed files with 162 additions and 77 deletions

View File

@@ -77,6 +77,12 @@ typedef struct {
snd_mixer_t *handle; snd_mixer_t *handle;
} alsamixer_t; } alsamixer_t;
typedef enum {
unit_percent,
unit_dB,
unit_last
} volume_unit_t;
/******************************************/ /******************************************/
/* PCM object wrapper */ /* PCM object wrapper */
/******************************************/ /******************************************/
@@ -1567,21 +1573,31 @@ Possible values in this list are:\n\
- 'Capture Exclusive'\n"); - 'Capture Exclusive'\n");
static int alsamixer_getpercentage(long min, long max, long value) static double alsamixer_getpercentage(long min, long max, long value)
{ {
/* Convert from number in range to percentage */ /* Convert from number in range to percentage */
int range = max - min; int range = max - min;
int tmp;
if (range == 0) if (range == 0)
return 0; return 0;
value -= min; value -= min;
tmp = rint((double)value/(double)range * 100); return (double)value/(double)range * 100.0;
return tmp;
} }
static long alsamixer_getphysvolume(long min, long max, int percentage) static double alsamixer_getdB(long min, long max, long value)
{
/* Convert from number in range to dB */
int range = max - min;
if (range == 0)
return 0;
value -= min;
return log10((double)value/range) * 60.0;
}
static long alsamixer_getphysvolume(long min, long max, double percentage)
{ {
/* Convert from percentage to number in range */ /* Convert from percentage to number in range */
int range = max - min; int range = max - min;
@@ -1590,57 +1606,76 @@ static long alsamixer_getphysvolume(long min, long max, int percentage)
if (range == 0) if (range == 0)
return 0; return 0;
tmp = rint((double)range * ((double)percentage*.01)) + min; tmp = rint((double)range * (percentage * .01)) + min;
return tmp; return tmp;
} }
static PyObject * static PyObject *
alsamixer_getvolume(alsamixer_t *self, PyObject *args) alsamixer_getvolume(alsamixer_t *self, PyObject *args, PyObject *kw)
{ {
snd_mixer_elem_t *elem; snd_mixer_elem_t *elem;
int channel; int channel;
long ival; long ival;
PyObject *pcmtypeobj = NULL; PyObject *dirobj = NULL;
long pcmtype; long dir;
int unit = unit_percent;
PyObject *result; PyObject *result;
PyObject *item; PyObject *item;
if (!PyArg_ParseTuple(args,"|O:getvolume", &pcmtypeobj)) static char *kwlist[] = { "direction", "unit", NULL };
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Oi:getvolume", kwlist, &dirobj, &unit))
return NULL; return NULL;
if (unit >= unit_last) {
PyErr_SetString(PyExc_ValueError, "unit must be 'percent' or 'dB'");
return NULL;
}
dir = get_pcmtype(dirobj);
if (dir < 0) {
return NULL;
}
if (!self->handle) if (!self->handle)
{ {
PyErr_SetString(ALSAAudioError, "Mixer is closed"); PyErr_SetString(ALSAAudioError, "Mixer is closed");
return NULL; return NULL;
} }
pcmtype = get_pcmtype(pcmtypeobj);
if (pcmtype < 0) {
return NULL;
}
elem = alsamixer_find_elem(self->handle,self->controlname,self->controlid); elem = alsamixer_find_elem(self->handle,self->controlname,self->controlid);
result = PyList_New(0); result = PyList_New(0);
for (channel = 0; channel <= SND_MIXER_SCHN_LAST; channel++) { for (channel = 0; channel <= SND_MIXER_SCHN_LAST; channel++) {
if (pcmtype == SND_PCM_STREAM_PLAYBACK && if (dir == SND_PCM_STREAM_PLAYBACK &&
snd_mixer_selem_has_playback_channel(elem, channel)) snd_mixer_selem_has_playback_channel(elem, channel))
{ {
snd_mixer_selem_get_playback_volume(elem, channel, &ival); snd_mixer_selem_get_playback_volume(elem, channel, &ival);
item = PyLong_FromLong(alsamixer_getpercentage(self->pmin, if (unit == unit_percent) {
self->pmax, item = PyFloat_FromDouble(
ival)); alsamixer_getpercentage(self->pmin, self->pmax, ival));
}
else {
item = PyFloat_FromDouble(
alsamixer_getdB(self->pmin, self->pmax, ival));
}
PyList_Append(result, item); PyList_Append(result, item);
Py_DECREF(item); Py_DECREF(item);
} }
else if (pcmtype == SND_PCM_STREAM_CAPTURE else if (dir == SND_PCM_STREAM_CAPTURE
&& snd_mixer_selem_has_capture_channel(elem, channel) && snd_mixer_selem_has_capture_channel(elem, channel)
&& snd_mixer_selem_has_capture_volume(elem)) { && snd_mixer_selem_has_capture_volume(elem))
{
snd_mixer_selem_get_capture_volume(elem, channel, &ival); snd_mixer_selem_get_capture_volume(elem, channel, &ival);
item = PyLong_FromLong(alsamixer_getpercentage(self->cmin, if (unit == unit_percent) {
self->cmax, item = PyFloat_FromDouble(
ival)); alsamixer_getpercentage(self->cmin, self->cmax, ival));
}
else {
item = PyFloat_FromDouble(
alsamixer_getdB(self->cmin, self->cmax, ival));
}
PyList_Append(result, item); PyList_Append(result, item);
Py_DECREF(item); Py_DECREF(item);
} }
@@ -1650,15 +1685,17 @@ alsamixer_getvolume(alsamixer_t *self, PyObject *args)
} }
PyDoc_STRVAR(getvolume_doc, PyDoc_STRVAR(getvolume_doc,
"getvolume([pcmtype]) -> List of volume settings (int)\n\ "getvolume(direction=PCM_PLAYBACK, unit=Percent) -> List of volume settings (float)\n\
\n\ \n\
Returns a list with the current volume settings for each channel.\n\ Returns a list with the current volume settings for each channel.\n\
The list elements are integer percentages.\n\ The list elements are float percentages.\n\
\n\ \n\
The optional 'pcmtype' argument can be either PCM_PLAYBACK or\n\ The 'direction' argument can be either PCM_PLAYBACK or\n\
PCM_CAPTURE, which is relevant if the mixer can control both\n\ PCM_CAPTURE, which is relevant if the mixer can control both\n\
playback and capture volume. The default value is PCM_PLAYBACK\n\ playback and capture volume. The default value is PCM_PLAYBACK\n\
if the mixer has this capability, otherwise PCM_CAPTURE"); if the mixer has this capability, otherwise PCM_CAPTURE\
\n\
The optional 'unit' argument can be either 'percent' or 'dB'.");
static PyObject * static PyObject *
@@ -1984,29 +2021,53 @@ This method will fail if the mixer has no capture switch capabilities.");
static PyObject * static PyObject *
alsamixer_setvolume(alsamixer_t *self, PyObject *args) alsamixer_setvolume(alsamixer_t *self, PyObject *args, PyObject *kw)
{ {
snd_mixer_elem_t *elem; snd_mixer_elem_t *elem;
int i; int i;
long volume; double volume = 0.0;
PyObject *volumeobj = NULL;
int physvolume; int physvolume;
PyObject *pcmtypeobj = NULL; PyObject *dirobj = NULL;
long pcmtype; long dir;
int unit = unit_percent;
int channel = MIXER_CHANNEL_ALL; int channel = MIXER_CHANNEL_ALL;
int done = 0; int done = 0;
if (!PyArg_ParseTuple(args,"l|iO:setvolume", &volume, &channel, static char *kwlist[] = { "channel", "direction", "unit", NULL };
&pcmtypeobj))
return NULL;
if (volume < 0 || volume > 100)
{ if (!PyArg_ParseTupleAndKeywords(args, kw, "O|iOi:setvolume", kwlist, &volumeobj, &channel,
PyErr_SetString(ALSAAudioError, "Volume must be between 0 and 100"); &dirobj, &unit)) {
return NULL; return NULL;
} }
pcmtype = get_pcmtype(pcmtypeobj); // unit
if (pcmtype < 0) { if (unit >= unit_last) {
PyErr_SetString(PyExc_ValueError, "unit must be 'percent' or 'dB'");
return NULL;
}
if (PyLong_Check(volumeobj)) {
volume = (double)PyLong_AsLong(volumeobj);
}
else if (PyFloat_Check(volumeobj)) {
volume = PyFloat_AsDouble(volumeobj);
}
else {
PyErr_SetString(PyExc_ValueError, "Volume must be integer or float");
return NULL;
}
if (unit == unit_percent && (volume < 0.0 || volume > 100.0))
{
PyErr_SetString(PyExc_ValueError, "Volume in percent must be between 0 and 100");
return NULL;
}
// pcmtype
dir = get_pcmtype(dirobj);
if (dir < 0) {
return NULL; return NULL;
} }
@@ -2018,38 +2079,48 @@ alsamixer_setvolume(alsamixer_t *self, PyObject *args)
elem = alsamixer_find_elem(self->handle,self->controlname,self->controlid); elem = alsamixer_find_elem(self->handle,self->controlname,self->controlid);
if (!pcmtypeobj || (pcmtypeobj == Py_None)) if (!dirobj || (dirobj == Py_None))
{ {
if (self->pchannels) if (self->pchannels)
pcmtype = SND_PCM_STREAM_PLAYBACK; dir = SND_PCM_STREAM_PLAYBACK;
else else
pcmtype = SND_PCM_STREAM_CAPTURE; dir = SND_PCM_STREAM_CAPTURE;
} }
for (i = 0; i <= SND_MIXER_SCHN_LAST; i++) for (i = 0; i <= SND_MIXER_SCHN_LAST; i++)
{ {
if (channel == -1 || channel == i) if (channel == -1 || channel == i)
{ {
if (pcmtype == SND_PCM_STREAM_PLAYBACK && if (dir == SND_PCM_STREAM_PLAYBACK &&
snd_mixer_selem_has_playback_channel(elem, i)) { snd_mixer_selem_has_playback_channel(elem, i)) {
physvolume = alsamixer_getphysvolume(self->pmin, if (unit == unit_percent) {
self->pmax, volume); physvolume = alsamixer_getphysvolume(self->pmin,
snd_mixer_selem_set_playback_volume(elem, i, physvolume); self->pmax, volume);
snd_mixer_selem_set_playback_volume(elem, i, physvolume);
}
else {
snd_mixer_selem_set_playback_dB(elem, i, (long)(volume * 100.0), 0);
}
done++; done++;
} }
else if (pcmtype == SND_PCM_STREAM_CAPTURE else if (dir == SND_PCM_STREAM_CAPTURE
&& snd_mixer_selem_has_capture_channel(elem, i) && snd_mixer_selem_has_capture_channel(elem, i)
&& snd_mixer_selem_has_capture_volume(elem)) && snd_mixer_selem_has_capture_volume(elem))
{ {
physvolume = alsamixer_getphysvolume(self->cmin, self->cmax, if (unit == unit_percent) {
volume); physvolume = alsamixer_getphysvolume(self->cmin, self->cmax,
snd_mixer_selem_set_capture_volume(elem, i, physvolume); volume);
snd_mixer_selem_set_capture_volume(elem, i, physvolume);
}
else {
snd_mixer_selem_set_capture_dB(elem, i, (long)(volume * 100), 0);
}
done++; done++;
} }
} }
} }
if(!done) if (!done)
{ {
PyErr_Format(ALSAAudioError, "No such channel [%s]", PyErr_Format(ALSAAudioError, "No such channel [%s]",
self->cardname); self->cardname);
@@ -2061,19 +2132,21 @@ alsamixer_setvolume(alsamixer_t *self, PyObject *args)
} }
PyDoc_STRVAR(setvolume_doc, PyDoc_STRVAR(setvolume_doc,
"setvolume(volume[[, channel] [, pcmtype]])\n\ "setvolume(volume, channel=MIXER_CHANNEL_ALL, direction=PCM_PLAYBACK, unit='percent')\n\
\n\ \n\
Change the current volume settings for this mixer. The volume argument\n\ Change the current volume settings for this mixer. The volume argument\n\
controls the new volume setting as an integer percentage.\n\ controls the new volume setting as a percentage.\n\
If the optional argument channel is present, the volume is set only for\n\ 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\ this channel. This assumes that the mixer can control the volume for the\n\
channels independently.\n\ channels independently.\n\
\n\ \n\
The optional direction argument can be either PCM_PLAYBACK or PCM_CAPTURE.\n\ The optional 'direction' argument can be either PCM_PLAYBACK or PCM_CAPTURE.\n\
It is relevant if the mixer has independent playback and capture volume\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\ capabilities, and controls which of the volumes will be changed.\n\
The default is 'playback' if the mixer has this capability, otherwise\n\ The default is PCM_PLAYBACK if the mixer has this capability, otherwise\n\
'capture'."); PCM_CAPTURE.\n\
\n\
The optional 'unit' argument can be either 'percent' (the default) or 'dB'.");
static PyObject * static PyObject *
@@ -2084,6 +2157,7 @@ alsamixer_setmute(alsamixer_t *self, PyObject *args)
int mute = 0; int mute = 0;
int done = 0; int done = 0;
int channel = MIXER_CHANNEL_ALL; int channel = MIXER_CHANNEL_ALL;
if (!PyArg_ParseTuple(args,"i|i:setmute", &mute, &channel)) if (!PyArg_ParseTuple(args,"i|i:setmute", &mute, &channel))
return NULL; return NULL;
@@ -2291,13 +2365,13 @@ static PyMethodDef alsamixer_methods[] = {
switchcap_doc}, switchcap_doc},
{"volumecap", (PyCFunction)alsamixer_volumecap, METH_VARARGS, {"volumecap", (PyCFunction)alsamixer_volumecap, METH_VARARGS,
volumecap_doc}, volumecap_doc},
{"getvolume", (PyCFunction)alsamixer_getvolume, METH_VARARGS, {"getvolume", (PyCFunction)alsamixer_getvolume, METH_VARARGS | METH_KEYWORDS,
getvolume_doc}, getvolume_doc},
{"getrange", (PyCFunction)alsamixer_getrange, METH_VARARGS, getrange_doc}, {"getrange", (PyCFunction)alsamixer_getrange, METH_VARARGS, getrange_doc},
{"getenum", (PyCFunction)alsamixer_getenum, METH_VARARGS, getenum_doc}, {"getenum", (PyCFunction)alsamixer_getenum, METH_VARARGS, getenum_doc},
{"getmute", (PyCFunction)alsamixer_getmute, METH_VARARGS, getmute_doc}, {"getmute", (PyCFunction)alsamixer_getmute, METH_VARARGS, getmute_doc},
{"getrec", (PyCFunction)alsamixer_getrec, METH_VARARGS, getrec_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}, setvolume_doc},
{"setenum", (PyCFunction)alsamixer_setenum, METH_VARARGS, setenum_doc}, {"setenum", (PyCFunction)alsamixer_setenum, METH_VARARGS, setenum_doc},
{"setmute", (PyCFunction)alsamixer_setmute, METH_VARARGS, setmute_doc}, {"setmute", (PyCFunction)alsamixer_setmute, METH_VARARGS, setmute_doc},
@@ -2447,6 +2521,9 @@ PyObject *PyInit_alsaaudio(void)
Py_INCREF(ALSAAudioError); Py_INCREF(ALSAAudioError);
PyModule_AddObject(m, "ALSAAudioError", ALSAAudioError); PyModule_AddObject(m, "ALSAAudioError", ALSAAudioError);
_EXPORT_INT(m, "Percent", unit_percent);
_EXPORT_INT(m, "dB", unit_dB);
_EXPORT_INT(m, "PCM_PLAYBACK",SND_PCM_STREAM_PLAYBACK); _EXPORT_INT(m, "PCM_PLAYBACK",SND_PCM_STREAM_PLAYBACK);
_EXPORT_INT(m, "PCM_CAPTURE",SND_PCM_STREAM_CAPTURE); _EXPORT_INT(m, "PCM_CAPTURE",SND_PCM_STREAM_CAPTURE);

View File

@@ -443,31 +443,35 @@ Mixer objects have the following methods:
This method will fail if the mixer has no capture switch capabilities. This method will fail if the mixer has no capture switch capabilities.
.. method:: Mixer.getvolume([direction]) .. method:: Mixer.getvolume(direction=PCM_PLAYBACK, unit=Percent)
Returns a list with the current volume settings for each channel. The list Returns a list with the current volume settings for each channel. The list
elements are integer percentages. elements are percentages or dB values, depending on *unit*.
The optional *direction* argument can be either :const:`PCM_PLAYBACK` or The *direction* argument can be either :const:`PCM_PLAYBACK` or
:const:`PCM_CAPTURE`, which is relevant if the mixer can control both :const:`PCM_CAPTURE`, which is relevant if the mixer can control both
playback and capture volume. The default value is :const:`PCM_PLAYBACK` playback and capture volume. The default value is :const:`PCM_PLAYBACK`
if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`. if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`.
.. method:: Mixer.setvolume(volume, [channel], [direction]) .. method:: Mixer.setvolume(volume, channel=MIXER_CHANNEL_ALL, direction=PCM_PLAYBACK, unit=Percent)
Change the current volume settings for this mixer. The *volume* argument Change the current volume settings for this mixer. The *volume* argument
controls the new volume setting as an integer percentage. controls the new volume setting as either a percentage or a dB value. Both
integer and floating point values can be given.
If the optional argument *channel* is present, the volume is set The *channel* argument can be used to restrict the channels for which the volume is
only for this channel. This assumes that the mixer can control the set. By default, the volume of all channels is adjusted. This assumes that the mixer
volume for the channels independently. can control the volume for the channels independently.
The optional *direction* argument can be either :const:`PCM_PLAYBACK` or The *direction* argument can be either :const:`PCM_PLAYBACK` or
:const:`PCM_CAPTURE`, which is relevant if the mixer can control both :const:`PCM_CAPTURE`, which is relevant if the mixer can control both
playback and capture volume. The default value is :const:`PCM_PLAYBACK` playback and capture volume. The default value is :const:`PCM_PLAYBACK`
if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`. if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`.
The *unit* argument determines how the volume value is interpreted, as a prcentage
or as a dB value.
.. method:: Mixer.setmute(mute, [channel]) .. method:: Mixer.setmute(mute, [channel])
Sets the mute flag to a new value. The *mute* argument is either 0 for not Sets the mute flag to a new value. The *mute* argument is either 0 for not

View File

@@ -46,13 +46,17 @@ def show_mixer(name, kwargs):
print("Capabilities: %s %s" % (' '.join(mixer.volumecap()), print("Capabilities: %s %s" % (' '.join(mixer.volumecap()),
' '.join(mixer.switchcap()))) ' '.join(mixer.switchcap())))
volumes = mixer.getvolume() volumes = mixer.getvolume()
for i in range(len(volumes)): for i, v in enumerate(volumes):
print("Channel %i volume: %i%%" % (i,volumes[i])) print("Channel %i volume: %.02f%%" % (i, v))
volumes = mixer.getvolume(unit=alsaaudio.dB)
for i, v in enumerate(volumes):
print("Channel %i volume: %.02fdB" % (i, v))
try: try:
mutes = mixer.getmute() mutes = mixer.getmute()
for i in range(len(mutes)): for i, m in enumerate(mutes):
if mutes[i]: if m:
print("Channel %i is muted" % i) print("Channel %i is muted" % i)
except alsaaudio.ALSAAudioError: except alsaaudio.ALSAAudioError:
# May not support muting # May not support muting
@@ -60,8 +64,8 @@ def show_mixer(name, kwargs):
try: try:
recs = mixer.getrec() recs = mixer.getrec()
for i in range(len(recs)): for i, r in enumerate(recs):
if recs[i]: if r:
print("Channel %i is recording" % i) print("Channel %i is recording" % i)
except alsaaudio.ALSAAudioError: except alsaaudio.ALSAAudioError:
# May not support recording # May not support recording