mirror of
https://github.com/larsimmisch/pyalsaaudio.git
synced 2026-04-17 16:35:31 +00:00
Compare commits
17 Commits
larsimmisc
...
main-pre-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0625934bb | ||
|
|
788fcb7c69 | ||
|
|
345260241f | ||
|
|
67adbf0524 | ||
|
|
cd44517ed5 | ||
|
|
3743cf5bd5 | ||
|
|
f374adb269 | ||
|
|
7cc1dadcb2 | ||
|
|
d3bb44517d | ||
|
|
c3dba90ff2 | ||
|
|
e3ec2d1628 | ||
|
|
a66cf02a4c | ||
|
|
d5e81f2604 | ||
|
|
8f7e8fbdfe | ||
|
|
dc3d6c6183 | ||
|
|
c6a0c8016d | ||
|
|
196ca87a05 |
19
CHANGES.md
19
CHANGES.md
@@ -1,3 +1,16 @@
|
|||||||
|
# Version 0.10.1
|
||||||
|
- restore previous xrun behaviour, #131
|
||||||
|
- type hints
|
||||||
|
|
||||||
|
# Version 0.10.0
|
||||||
|
- assorted improvements (#123 from @ossilator)
|
||||||
|
- support for `periods` in the `PCM` constructor.
|
||||||
|
- new functions `PCM.state()`, `PCM.drop()` and `PCM.drain()`
|
||||||
|
- improved underrun/overrun handling
|
||||||
|
- documentation improvements/consolidation (docstrings were removed in favour of online documentation)
|
||||||
|
- more sampling rates
|
||||||
|
- bug fixes
|
||||||
|
|
||||||
# Version 0.9.2
|
# Version 0.9.2
|
||||||
- Fix alsamixer_getvolume (#112 from @stephensp)
|
- Fix alsamixer_getvolume (#112 from @stephensp)
|
||||||
|
|
||||||
@@ -15,15 +28,15 @@
|
|||||||
supported formats - e.g. `{"U8": 1, "S16_LE": 2}`,
|
supported formats - e.g. `{"U8": 1, "S16_LE": 2}`,
|
||||||
- `getchannels()` returns a list of the supported channel numbers, e.g. `[1, 2]`,
|
- `getchannels()` returns a list of the supported channel numbers, e.g. `[1, 2]`,
|
||||||
- `getrates()` returns supported sample rates for the device, e.g. `[48000]`,
|
- `getrates()` returns supported sample rates for the device, e.g. `[48000]`,
|
||||||
- `getratebounds()` returns the device's official minimum and maximum supported
|
- `getratebounds()` returns the device's official minimum and maximum supported
|
||||||
sample rates as a tuple, e.g. `(4000, 48000)`.
|
sample rates as a tuple, e.g. `(4000, 48000)`.
|
||||||
|
|
||||||
(#82 contributed by @jdstmporter)
|
(#82 contributed by @jdstmporter)
|
||||||
|
|
||||||
- Prevent hang on close after capturing audio (#80 contributed by @daym)
|
- Prevent hang on close after capturing audio (#80 contributed by @daym)
|
||||||
|
|
||||||
# Version 0.8.5:
|
# Version 0.8.5:
|
||||||
- Return an empty string/bytestring when `read()` detects an
|
- Return an empty string/bytestring when `read()` detects an
|
||||||
overrun. Previously the returned data was undefined (contributed by @jcea)
|
overrun. Previously the returned data was undefined (contributed by @jcea)
|
||||||
|
|
||||||
- Unlimited setperiod buffer size when reading frames (contributed by @jcea)
|
- Unlimited setperiod buffer size when reading frames (contributed by @jcea)
|
||||||
|
|||||||
839
alsaaudio.c
839
alsaaudio.c
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,3 @@
|
|||||||
.. alsaaudio documentation documentation master file, created by
|
|
||||||
sphinx-quickstart on Thu Mar 30 23:52:21 2017.
|
|
||||||
You can adapt this file completely to your liking, but it should at least
|
|
||||||
contain the root `toctree` directive.
|
|
||||||
|
|
||||||
alsaaudio documentation
|
alsaaudio documentation
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
@@ -18,15 +13,13 @@ Download
|
|||||||
========
|
========
|
||||||
|
|
||||||
* `Download from pypi <https://pypi.python.org/pypi/pyalsaaudio>`_
|
* `Download from pypi <https://pypi.python.org/pypi/pyalsaaudio>`_
|
||||||
|
|
||||||
|
|
||||||
Github
|
Github
|
||||||
======
|
======
|
||||||
|
|
||||||
* `Repository <https://github.com/larsimmisch/pyalsaaudio/>`_
|
* `Repository <https://github.com/larsimmisch/pyalsaaudio/>`_
|
||||||
* `Bug tracker <https://github.com/larsimmisch/pyalsaaudio/issues>`_
|
* `Bug tracker <https://github.com/larsimmisch/pyalsaaudio/issues>`_
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
||||||
@@ -34,5 +27,3 @@ Indices and tables
|
|||||||
* :ref:`modindex`
|
* :ref:`modindex`
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,38 +5,15 @@
|
|||||||
.. module:: alsaaudio
|
.. module:: alsaaudio
|
||||||
:platform: Linux
|
:platform: Linux
|
||||||
|
|
||||||
|
|
||||||
.. % \declaremodule{builtin}{alsaaudio} % standard library, in C
|
|
||||||
.. % not standard, in C
|
|
||||||
|
|
||||||
.. moduleauthor:: Casper Wilstrup <cwi@aves.dk>
|
.. moduleauthor:: Casper Wilstrup <cwi@aves.dk>
|
||||||
.. moduleauthor:: Lars Immisch <lars@ibp.de>
|
.. moduleauthor:: Lars Immisch <lars@ibp.de>
|
||||||
|
|
||||||
.. % Author of the module code;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The :mod:`alsaaudio` module defines functions and classes for using ALSA.
|
The :mod:`alsaaudio` module defines functions and classes for using ALSA.
|
||||||
|
|
||||||
.. % ---- 3.1. ----
|
|
||||||
.. % For each function, use a ``funcdesc'' block. This has exactly two
|
|
||||||
.. % parameters (each parameters is contained in a set of curly braces):
|
|
||||||
.. % the first parameter is the function name (this automatically
|
|
||||||
.. % generates an index entry); the second parameter is the function's
|
|
||||||
.. % argument list. If there are no arguments, use an empty pair of
|
|
||||||
.. % curly braces. If there is more than one argument, separate the
|
|
||||||
.. % arguments with backslash-comma. Optional parts of the parameter
|
|
||||||
.. % list are contained in \optional{...} (this generates a set of square
|
|
||||||
.. % brackets around its parameter). Arguments are automatically set in
|
|
||||||
.. % italics in the parameter list. Each argument should be mentioned at
|
|
||||||
.. % least once in the description; each usage (even inside \code{...})
|
|
||||||
.. % should be enclosed in \var{...}.
|
|
||||||
|
|
||||||
|
|
||||||
.. function:: pcms(pcmtype=PCM_PLAYBACK)
|
.. function:: pcms(pcmtype=PCM_PLAYBACK)
|
||||||
|
|
||||||
List available PCM devices by name.
|
List available PCM devices by name.
|
||||||
|
|
||||||
Arguments are:
|
Arguments are:
|
||||||
|
|
||||||
* *pcmtype* - can be either :const:`PCM_CAPTURE` or :const:`PCM_PLAYBACK`
|
* *pcmtype* - can be either :const:`PCM_CAPTURE` or :const:`PCM_PLAYBACK`
|
||||||
@@ -62,7 +39,13 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
|
|||||||
List the available ALSA cards by name. This function is only moderately
|
List the available ALSA cards by name. This function is only moderately
|
||||||
useful. If you want to see a list of available PCM devices, use :func:`pcms`
|
useful. If you want to see a list of available PCM devices, use :func:`pcms`
|
||||||
instead.
|
instead.
|
||||||
|
|
||||||
|
..
|
||||||
|
Omitted by intention due to being superseded by cards():
|
||||||
|
|
||||||
|
.. function:: card_indexes()
|
||||||
|
.. function:: card_name()
|
||||||
|
|
||||||
.. function:: mixers(cardindex=-1, device='default')
|
.. function:: mixers(cardindex=-1, device='default')
|
||||||
|
|
||||||
List the available mixers. The arguments are:
|
List the available mixers. The arguments are:
|
||||||
@@ -72,12 +55,14 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
|
|||||||
the `device` keyword argument is ignored. ``0`` is the first hardware sound
|
the `device` keyword argument is ignored. ``0`` is the first hardware sound
|
||||||
card.
|
card.
|
||||||
|
|
||||||
|
**Note:** This should not be used, as it bypasses most of ALSA's configuration.
|
||||||
|
|
||||||
* *device* - the name of the device on which the mixer resides. The default
|
* *device* - the name of the device on which the mixer resides. The default
|
||||||
is ``'default'``.
|
is ``'default'``.
|
||||||
|
|
||||||
**Note:** For a list of available controls, you can also use ``amixer`` on
|
**Note:** For a list of available controls, you can also use ``amixer`` on
|
||||||
the commandline::
|
the commandline::
|
||||||
|
|
||||||
$ amixer
|
$ amixer
|
||||||
|
|
||||||
To elaborate the example, calling :func:`mixers` with the argument
|
To elaborate the example, calling :func:`mixers` with the argument
|
||||||
@@ -91,7 +76,7 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
|
|||||||
$ amixer -D foo
|
$ amixer -D foo
|
||||||
|
|
||||||
*Changed in 0.8*:
|
*Changed in 0.8*:
|
||||||
|
|
||||||
- The keyword argument `device` is new and can be used to
|
- The keyword argument `device` is new and can be used to
|
||||||
select virtual devices. As a result, the default behaviour has subtly
|
select virtual devices. As a result, the default behaviour has subtly
|
||||||
changed. Since 0.8, this functions returns the mixers for the default
|
changed. Since 0.8, this functions returns the mixers for the default
|
||||||
@@ -101,6 +86,7 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
|
|||||||
|
|
||||||
Return a Python string containing the ALSA version found.
|
Return a Python string containing the ALSA version found.
|
||||||
|
|
||||||
|
|
||||||
.. _pcm-objects:
|
.. _pcm-objects:
|
||||||
|
|
||||||
PCM Objects
|
PCM Objects
|
||||||
@@ -110,7 +96,7 @@ PCM objects in :mod:`alsaaudio` can play or capture (record) PCM
|
|||||||
sound through speakers or a microphone. The PCM constructor takes the
|
sound through speakers or a microphone. The PCM constructor takes the
|
||||||
following arguments:
|
following arguments:
|
||||||
|
|
||||||
.. class:: PCM(type=PCM_PLAYBACK, mode=PCM_NORMAL, rate=44100, channels=2, format=PCM_FORMAT_S16_LE, periodsize=32, device='default', cardindex=-1)
|
.. class:: PCM(type=PCM_PLAYBACK, mode=PCM_NORMAL, rate=44100, channels=2, format=PCM_FORMAT_S16_LE, periodsize=32, periods=4, device='default', cardindex=-1)
|
||||||
|
|
||||||
This class is used to represent a PCM device (either for playback and
|
This class is used to represent a PCM device (either for playback and
|
||||||
recording). The arguments are:
|
recording). The arguments are:
|
||||||
@@ -123,7 +109,7 @@ following arguments:
|
|||||||
* *channels* - the number of channels. The default value is 2 (stereo).
|
* *channels* - the number of channels. The default value is 2 (stereo).
|
||||||
* *format* - the data format. This controls how the PCM device interprets data for playback, and how data is encoded in captures.
|
* *format* - the data format. This controls how the PCM device interprets data for playback, and how data is encoded in captures.
|
||||||
The default value is :const:`PCM_FORMAT_S16_LE`.
|
The default value is :const:`PCM_FORMAT_S16_LE`.
|
||||||
|
|
||||||
========================= ===============
|
========================= ===============
|
||||||
Format Description
|
Format Description
|
||||||
========================= ===============
|
========================= ===============
|
||||||
@@ -156,7 +142,11 @@ following arguments:
|
|||||||
``PCM_FORMAT_U24_3BE`` Unsigned 24 bit samples for each channel (Big Endian byte order in 3 bytes)
|
``PCM_FORMAT_U24_3BE`` Unsigned 24 bit samples for each channel (Big Endian byte order in 3 bytes)
|
||||||
========================= ===============
|
========================= ===============
|
||||||
|
|
||||||
* *periodsize* - the period size in frames. Each write should consist of *periodsize* frames. The default value is 32.
|
* *periodsize* - the period size in frames.
|
||||||
|
Make sure you understand :ref:`the meaning of periods <term-period>`.
|
||||||
|
The default value is 32, which is below the actual minimum of most devices,
|
||||||
|
and will therefore likely be larger in practice.
|
||||||
|
* *periods* - the number of periods in the buffer. The default value is 4.
|
||||||
* *device* - the name of the PCM device that should be used (for example
|
* *device* - the name of the PCM device that should be used (for example
|
||||||
a value from the output of :func:`pcms`). The default value is
|
a value from the output of :func:`pcms`). The default value is
|
||||||
``'default'``.
|
``'default'``.
|
||||||
@@ -165,14 +155,20 @@ following arguments:
|
|||||||
the `device` keyword argument is ignored.
|
the `device` keyword argument is ignored.
|
||||||
``0`` is the first hardware sound card.
|
``0`` is the first hardware sound card.
|
||||||
|
|
||||||
|
**Note:** This should not be used, as it bypasses most of ALSA's configuration.
|
||||||
|
|
||||||
This will construct a PCM object with the given settings.
|
This will construct a PCM object with the given settings.
|
||||||
|
|
||||||
|
*Changed in 0.10:*
|
||||||
|
|
||||||
|
- Added the optional named parameter `periods`.
|
||||||
|
|
||||||
*Changed in 0.9:*
|
*Changed in 0.9:*
|
||||||
|
|
||||||
- Added the optional named parameters `rate`, `channels`, `format` and `periodsize`.
|
- Added the optional named parameters `rate`, `channels`, `format` and `periodsize`.
|
||||||
|
|
||||||
*Changed in 0.8:*
|
*Changed in 0.8:*
|
||||||
|
|
||||||
- The `card` keyword argument is still supported,
|
- The `card` keyword argument is still supported,
|
||||||
but deprecated. Please use `device` instead.
|
but deprecated. Please use `device` instead.
|
||||||
|
|
||||||
@@ -180,8 +176,7 @@ following arguments:
|
|||||||
|
|
||||||
The `card` keyword is deprecated because it guesses the real ALSA
|
The `card` keyword is deprecated because it guesses the real ALSA
|
||||||
name of the card. This was always fragile and broke some legitimate usecases.
|
name of the card. This was always fragile and broke some legitimate usecases.
|
||||||
|
|
||||||
|
|
||||||
PCM objects have the following methods:
|
PCM objects have the following methods:
|
||||||
|
|
||||||
.. method:: PCM.info()
|
.. method:: PCM.info()
|
||||||
@@ -235,17 +230,43 @@ PCM objects have the following methods:
|
|||||||
Returns the type of PCM object. Either :const:`PCM_CAPTURE` or
|
Returns the type of PCM object. Either :const:`PCM_CAPTURE` or
|
||||||
:const:`PCM_PLAYBACK`.
|
:const:`PCM_PLAYBACK`.
|
||||||
|
|
||||||
|
|
||||||
.. method:: PCM.pcmmode()
|
.. method:: PCM.pcmmode()
|
||||||
|
|
||||||
Return the mode of the PCM object. One of :const:`PCM_NONBLOCK`,
|
Return the mode of the PCM object. One of :const:`PCM_NONBLOCK`,
|
||||||
:const:`PCM_ASYNC`, or :const:`PCM_NORMAL`
|
:const:`PCM_ASYNC`, or :const:`PCM_NORMAL`
|
||||||
|
|
||||||
|
|
||||||
.. method:: PCM.cardname()
|
.. method:: PCM.cardname()
|
||||||
|
|
||||||
Return the name of the sound card used by this PCM object.
|
Return the name of the sound card used by this PCM object.
|
||||||
|
|
||||||
|
..
|
||||||
|
Omitted by intention due to not really fitting the c'tor-based setup concept:
|
||||||
|
|
||||||
|
.. method:: PCM.getchannels()
|
||||||
|
|
||||||
|
Returns list of the device's supported channel counts.
|
||||||
|
|
||||||
|
.. method:: PCM.getratebounds()
|
||||||
|
|
||||||
|
Returns the card's minimum and maximum supported sample rates as
|
||||||
|
a tuple of integers.
|
||||||
|
|
||||||
|
.. method:: PCM.getrates()
|
||||||
|
|
||||||
|
Returns the sample rates supported by the device.
|
||||||
|
The returned value can be of one of the following, depending on
|
||||||
|
the card's properties:
|
||||||
|
* Card supports only a single rate: returns the rate
|
||||||
|
* Card supports a continuous range of rates: returns a tuple of
|
||||||
|
the range's lower and upper bounds (inclusive)
|
||||||
|
* Card supports a collection of well-known rates: returns a list of
|
||||||
|
the supported rates
|
||||||
|
|
||||||
|
.. method:: PCM.getformats()
|
||||||
|
|
||||||
|
Returns a dictionary of supported format codes (integers) keyed by
|
||||||
|
their standard ALSA names (strings).
|
||||||
|
|
||||||
.. method:: PCM.setchannels(nchannels)
|
.. method:: PCM.setchannels(nchannels)
|
||||||
|
|
||||||
.. deprecated:: 0.9 Use the `channels` named argument to :func:`PCM`.
|
.. deprecated:: 0.9 Use the `channels` named argument to :func:`PCM`.
|
||||||
@@ -255,13 +276,42 @@ PCM objects have the following methods:
|
|||||||
.. deprecated:: 0.9 Use the `rate` named argument to :func:`PCM`.
|
.. deprecated:: 0.9 Use the `rate` named argument to :func:`PCM`.
|
||||||
|
|
||||||
.. method:: PCM.setformat(format)
|
.. method:: PCM.setformat(format)
|
||||||
|
|
||||||
.. deprecated:: 0.9 Use the `format` named argument to :func:`PCM`.
|
.. deprecated:: 0.9 Use the `format` named argument to :func:`PCM`.
|
||||||
|
|
||||||
.. method:: PCM.setperiodsize(period)
|
.. method:: PCM.setperiodsize(period)
|
||||||
|
|
||||||
.. deprecated:: 0.9 Use the `periodsize` named argument to :func:`PCM`.
|
.. deprecated:: 0.9 Use the `periodsize` named argument to :func:`PCM`.
|
||||||
|
|
||||||
|
.. method:: PCM.info()
|
||||||
|
|
||||||
|
Returns a dictionary with the PCM object's configured parameters.
|
||||||
|
|
||||||
|
Values are retrieved from the ALSA library if they are available;
|
||||||
|
otherwise they represent those stored by pyalsaaudio, and their keys
|
||||||
|
are prefixed with ' (call value) '.
|
||||||
|
|
||||||
|
*New in 0.9.1*
|
||||||
|
|
||||||
|
.. method:: PCM.dumpinfo()
|
||||||
|
|
||||||
|
Dumps the PCM object's configured parameters to stdout.
|
||||||
|
|
||||||
|
.. method:: PCM.state()
|
||||||
|
|
||||||
|
Returs the current state of the stream, which can be one of
|
||||||
|
:const:`PCM_STATE_OPEN` (this should not actually happen),
|
||||||
|
:const:`PCM_STATE_SETUP` (after :func:`drop` or :func:`drain`),
|
||||||
|
:const:`PCM_STATE_PREPARED` (after construction),
|
||||||
|
:const:`PCM_STATE_RUNNING`,
|
||||||
|
:const:`PCM_STATE_XRUN`,
|
||||||
|
:const:`PCM_STATE_DRAINING`,
|
||||||
|
:const:`PCM_STATE_PAUSED`,
|
||||||
|
:const:`PCM_STATE_SUSPENDED`, and
|
||||||
|
:const:`PCM_STATE_DISCONNECTED`.
|
||||||
|
|
||||||
|
*New in 0.10*
|
||||||
|
|
||||||
.. method:: PCM.read()
|
.. method:: PCM.read()
|
||||||
|
|
||||||
In :const:`PCM_NORMAL` mode, this function blocks until a full period is
|
In :const:`PCM_NORMAL` mode, this function blocks until a full period is
|
||||||
@@ -294,17 +344,42 @@ PCM objects have the following methods:
|
|||||||
return value of zero, if the buffer is full. In this case, the data
|
return value of zero, if the buffer is full. In this case, the data
|
||||||
should be written at a later time.
|
should be written at a later time.
|
||||||
|
|
||||||
|
Note that this call completing means only that the samples were buffered
|
||||||
|
in the kernel, and playout will continue afterwards. Make sure that the
|
||||||
|
stream is drained before discarding the PCM handle.
|
||||||
|
|
||||||
.. method:: PCM.pause([enable=True])
|
.. method:: PCM.pause([enable=True])
|
||||||
|
|
||||||
If *enable* is :const:`True`, playback or capture is paused.
|
If *enable* is :const:`True`, playback or capture is paused.
|
||||||
Otherwise, playback/capture is resumed.
|
Otherwise, playback/capture is resumed.
|
||||||
|
|
||||||
|
.. method:: PCM.drop()
|
||||||
|
|
||||||
|
Stop the stream and drop residual buffered frames.
|
||||||
|
|
||||||
|
*New in 0.9*
|
||||||
|
|
||||||
|
.. method:: PCM.drain()
|
||||||
|
|
||||||
|
For :const:`PCM_PLAYBACK` PCM objects, play residual buffered frames
|
||||||
|
and then stop the stream. In :const:`PCM_NORMAL` mode,
|
||||||
|
this function blocks until all pending playback is drained.
|
||||||
|
|
||||||
|
For :const:`PCM_CAPTURE` PCM objects, this function is not very useful.
|
||||||
|
|
||||||
|
*New in 0.10*
|
||||||
|
|
||||||
|
.. method:: PCM.close()
|
||||||
|
|
||||||
|
Closes the PCM device.
|
||||||
|
|
||||||
|
For :const:`PCM_PLAYBACK` PCM objects in :const:`PCM_NORMAL` mode,
|
||||||
|
this function blocks until all pending playback is drained.
|
||||||
|
|
||||||
.. method:: PCM.polldescriptors()
|
.. method:: PCM.polldescriptors()
|
||||||
|
|
||||||
Returns a tuple of *(file descriptor, eventmask)* that can be used to
|
Returns a list of tuples of *(file descriptor, eventmask)* that can be
|
||||||
wait for changes on the PCM with *select.poll*.
|
used to wait for changes on the PCM with *select.poll*.
|
||||||
|
|
||||||
The *eventmask* value is compatible with `poll.register`__ in the Python
|
The *eventmask* value is compatible with `poll.register`__ in the Python
|
||||||
:const:`select` module.
|
:const:`select` module.
|
||||||
@@ -347,7 +422,7 @@ PCM objects have the following methods:
|
|||||||
``PCM_TSTAMP_TYPE_MONOTONIC_RAW`` Monotonic time from an unspecified starting
|
``PCM_TSTAMP_TYPE_MONOTONIC_RAW`` Monotonic time from an unspecified starting
|
||||||
time using only the system clock.
|
time using only the system clock.
|
||||||
================================= ===========================================
|
================================= ===========================================
|
||||||
|
|
||||||
The timestamp mode is controlled by the tstamp_mode, as described in the table below.
|
The timestamp mode is controlled by the tstamp_mode, as described in the table below.
|
||||||
|
|
||||||
================================= ===========================================
|
================================= ===========================================
|
||||||
@@ -358,9 +433,6 @@ PCM objects have the following methods:
|
|||||||
update.
|
update.
|
||||||
================================= ===========================================
|
================================= ===========================================
|
||||||
|
|
||||||
|
|
||||||
__ poll_objects_
|
|
||||||
|
|
||||||
**A few hints on using PCM devices for playback**
|
**A few hints on using PCM devices for playback**
|
||||||
|
|
||||||
The most common reason for problems with playback of PCM audio is that writes
|
The most common reason for problems with playback of PCM audio is that writes
|
||||||
@@ -396,11 +468,10 @@ Mixer Objects
|
|||||||
|
|
||||||
Mixer objects provides access to the ALSA mixer API.
|
Mixer objects provides access to the ALSA mixer API.
|
||||||
|
|
||||||
|
|
||||||
.. class:: Mixer(control='Master', id=0, cardindex=-1, device='default')
|
.. class:: Mixer(control='Master', id=0, cardindex=-1, device='default')
|
||||||
|
|
||||||
Arguments are:
|
Arguments are:
|
||||||
|
|
||||||
* *control* - specifies which control to manipulate using this mixer
|
* *control* - specifies which control to manipulate using this mixer
|
||||||
object. The list of available controls can be found with the
|
object. The list of available controls can be found with the
|
||||||
:mod:`alsaaudio`.\ :func:`mixers` function. The default value is
|
:mod:`alsaaudio`.\ :func:`mixers` function. The default value is
|
||||||
@@ -416,30 +487,27 @@ Mixer objects provides access to the ALSA mixer API.
|
|||||||
|
|
||||||
* *device* - the name of the device on which the mixer resides. The default
|
* *device* - the name of the device on which the mixer resides. The default
|
||||||
value is ``'default'``.
|
value is ``'default'``.
|
||||||
|
|
||||||
*Changed in 0.8*:
|
*Changed in 0.8*:
|
||||||
|
|
||||||
- The keyword argument `device` is new and can be used to select virtual
|
- The keyword argument `device` is new and can be used to select virtual
|
||||||
devices.
|
devices.
|
||||||
|
|
||||||
Mixer objects have the following methods:
|
Mixer objects have the following methods:
|
||||||
|
|
||||||
.. method:: Mixer.cardname()
|
.. method:: Mixer.cardname()
|
||||||
|
|
||||||
Return the name of the sound card used by this Mixer object
|
Return the name of the sound card used by this Mixer object
|
||||||
|
|
||||||
|
|
||||||
.. method:: Mixer.mixer()
|
.. method:: Mixer.mixer()
|
||||||
|
|
||||||
Return the name of the specific mixer controlled by this object, For example
|
Return the name of the specific mixer controlled by this object, For example
|
||||||
``'Master'`` or ``'PCM'``
|
``'Master'`` or ``'PCM'``
|
||||||
|
|
||||||
|
|
||||||
.. method:: Mixer.mixerid()
|
.. method:: Mixer.mixerid()
|
||||||
|
|
||||||
Return the ID of the ALSA mixer controlled by this object.
|
Return the ID of the ALSA mixer controlled by this object.
|
||||||
|
|
||||||
|
|
||||||
.. method:: Mixer.switchcap()
|
.. method:: Mixer.switchcap()
|
||||||
|
|
||||||
Returns a list of the switches which are defined by this specific mixer.
|
Returns a list of the switches which are defined by this specific mixer.
|
||||||
@@ -460,7 +528,6 @@ Mixer objects have the following methods:
|
|||||||
To manipulate these switches use the :meth:`setrec` or
|
To manipulate these switches use the :meth:`setrec` or
|
||||||
:meth:`setmute` methods
|
:meth:`setmute` methods
|
||||||
|
|
||||||
|
|
||||||
.. method:: Mixer.volumecap()
|
.. method:: Mixer.volumecap()
|
||||||
|
|
||||||
Returns a list of the volume control capabilities of this
|
Returns a list of the volume control capabilities of this
|
||||||
@@ -476,7 +543,7 @@ Mixer objects have the following methods:
|
|||||||
'Capture Volume' Manipulate sound capture volume
|
'Capture Volume' Manipulate sound capture volume
|
||||||
'Joined Capture Volume' Manipulate sound capture volume for all channels at a time
|
'Joined Capture Volume' Manipulate sound capture volume for all channels at a time
|
||||||
======================== ================
|
======================== ================
|
||||||
|
|
||||||
.. method:: Mixer.getenum()
|
.. method:: Mixer.getenum()
|
||||||
|
|
||||||
For enumerated controls, return the currently selected item and the list of
|
For enumerated controls, return the currently selected item and the list of
|
||||||
@@ -503,48 +570,43 @@ Mixer objects have the following methods:
|
|||||||
This method will return an empty tuple if the mixer is not an enumerated
|
This method will return an empty tuple if the mixer is not an enumerated
|
||||||
control.
|
control.
|
||||||
|
|
||||||
|
.. method:: Mixer.setenum(index)
|
||||||
|
|
||||||
.. method:: Mixer.getmute()
|
For enumerated controls, sets the currently selected item.
|
||||||
|
*index* is an index into the list of available enumerated items returned
|
||||||
|
by :func:`getenum`.
|
||||||
|
|
||||||
Return a list indicating the current mute setting for each
|
.. method:: Mixer.getrange(pcmtype=PCM_PLAYBACK, units=VOLUME_UNITS_RAW)
|
||||||
channel. 0 means not muted, 1 means muted.
|
|
||||||
|
|
||||||
This method will fail if the mixer has no playback switch capabilities.
|
|
||||||
|
|
||||||
|
|
||||||
.. method:: Mixer.getrange(pcmtype=PCM_PLAYBACK)
|
|
||||||
|
|
||||||
Return the volume range of the ALSA mixer controlled by this object.
|
Return the volume range of the ALSA mixer controlled by this object.
|
||||||
|
The value is a tuple of integers whose meaning is determined by the
|
||||||
|
*units* argument.
|
||||||
|
|
||||||
The optional *pcmtype* 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
|
: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 optional *units* argument can be one of :const:`VOLUME_UNITS_PERCENTAGE`,
|
||||||
|
:const:`VOLUME_UNITS_RAW`, or :const:`VOLUME_UNITS_DB`.
|
||||||
|
|
||||||
.. method:: Mixer.getrec()
|
.. method:: Mixer.getvolume(pcmtype=PCM_PLAYBACK, units=VOLUME_UNITS_PERCENTAGE)
|
||||||
|
|
||||||
Return a list indicating the current record mute setting for each channel. 0
|
|
||||||
means not recording, 1 means recording.
|
|
||||||
|
|
||||||
This method will fail if the mixer has no capture switch capabilities.
|
|
||||||
|
|
||||||
|
|
||||||
.. method:: Mixer.getvolume(pcmtype=PCM_PLAYBACK)
|
|
||||||
|
|
||||||
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 integers whose meaning is determined by the *units* argument.
|
||||||
|
|
||||||
The optional *pcmtype* 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
|
: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 optional *units* argument can be one of :const:`VOLUME_UNITS_PERCENTAGE`,
|
||||||
|
:const:`VOLUME_UNITS_RAW`, or :const:`VOLUME_UNITS_DB`.
|
||||||
|
|
||||||
.. method:: Mixer.setvolume(volume, channel=None, pcmtype=PCM_PLAYBACK)
|
.. method:: Mixer.setvolume(volume, channel=None, pcmtype=PCM_PLAYBACK, units=VOLUME_UNITS_PERCENTAGE)
|
||||||
|
|
||||||
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.
|
is an integer whose meaning is determined by the *units* argument.
|
||||||
|
|
||||||
If the optional argument *channel* is present, the volume is set
|
If the optional argument *channel* is present, the volume is set
|
||||||
only for this channel. This assumes that the mixer can control the
|
only for this channel. This assumes that the mixer can control the
|
||||||
@@ -555,6 +617,16 @@ Mixer objects have the following methods:
|
|||||||
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 optional *units* argument can be one of :const:`VOLUME_UNITS_PERCENTAGE`,
|
||||||
|
:const:`VOLUME_UNITS_RAW`, or :const:`VOLUME_UNITS_DB`.
|
||||||
|
|
||||||
|
.. method:: Mixer.getmute()
|
||||||
|
|
||||||
|
Return a list indicating the current mute setting for each channel.
|
||||||
|
0 means not muted, 1 means muted.
|
||||||
|
|
||||||
|
This method will fail if the mixer has no playback switch capabilities.
|
||||||
|
|
||||||
.. 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
|
||||||
@@ -565,6 +637,12 @@ Mixer objects have the following methods:
|
|||||||
|
|
||||||
This method will fail if the mixer has no playback mute capabilities
|
This method will fail if the mixer has no playback mute capabilities
|
||||||
|
|
||||||
|
.. method:: Mixer.getrec()
|
||||||
|
|
||||||
|
Return a list indicating the current record mute setting for each channel.
|
||||||
|
0 means not recording, 1 means recording.
|
||||||
|
|
||||||
|
This method will fail if the mixer has no capture switch capabilities.
|
||||||
|
|
||||||
.. method:: Mixer.setrec(capture, [channel])
|
.. method:: Mixer.setrec(capture, [channel])
|
||||||
|
|
||||||
@@ -578,20 +656,22 @@ Mixer objects have the following methods:
|
|||||||
|
|
||||||
.. method:: Mixer.polldescriptors()
|
.. method:: Mixer.polldescriptors()
|
||||||
|
|
||||||
Returns a tuple of *(file descriptor, eventmask)* that can be used to
|
Returns a list of tuples of *(file descriptor, eventmask)* that can be
|
||||||
wait for changes on the mixer with *select.poll*.
|
used to wait for changes on the mixer with *select.poll*.
|
||||||
|
|
||||||
The *eventmask* value is compatible with `poll.register`__ in the Python
|
The *eventmask* value is compatible with `poll.register`__ in the Python
|
||||||
:const:`select` module.
|
:const:`select` module.
|
||||||
|
|
||||||
__ poll_objects_
|
|
||||||
|
|
||||||
.. method:: Mixer.handleevents()
|
.. method:: Mixer.handleevents()
|
||||||
|
|
||||||
Acknowledge events on the *polldescriptors* file descriptors
|
Acknowledge events on the :func:`polldescriptors` file descriptors
|
||||||
to prevent subsequent polls from returning the same events again.
|
to prevent subsequent polls from returning the same events again.
|
||||||
Returns the number of events that were acknowledged.
|
Returns the number of events that were acknowledged.
|
||||||
|
|
||||||
|
.. method:: Mixer.close()
|
||||||
|
|
||||||
|
Closes the Mixer device.
|
||||||
|
|
||||||
**A rant on the ALSA Mixer API**
|
**A rant on the ALSA Mixer API**
|
||||||
|
|
||||||
The ALSA mixer API is extremely complicated - and hardly documented at all.
|
The ALSA mixer API is extremely complicated - and hardly documented at all.
|
||||||
@@ -614,8 +694,6 @@ Unfortunately, I'm not able to create such a HOWTO myself, since I only
|
|||||||
understand half of the API, and that which I do understand has come from a
|
understand half of the API, and that which I do understand has come from a
|
||||||
painful trial and error process.
|
painful trial and error process.
|
||||||
|
|
||||||
.. % ==== 4. ====
|
|
||||||
|
|
||||||
|
|
||||||
.. _pcm-example:
|
.. _pcm-example:
|
||||||
|
|
||||||
@@ -657,6 +735,7 @@ To test PCM playback (on your default soundcard), run::
|
|||||||
|
|
||||||
recordtest.py and playbacktest.py
|
recordtest.py and playbacktest.py
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
**recordtest.py** and **playbacktest.py** will record and play a raw
|
**recordtest.py** and **playbacktest.py** will record and play a raw
|
||||||
sound file in CD quality.
|
sound file in CD quality.
|
||||||
|
|
||||||
@@ -678,7 +757,7 @@ Without arguments, **mixertest.py** will list all available *controls* on the
|
|||||||
default soundcard.
|
default soundcard.
|
||||||
|
|
||||||
The output might look like this::
|
The output might look like this::
|
||||||
|
|
||||||
$ ./mixertest.py
|
$ ./mixertest.py
|
||||||
Available mixer controls:
|
Available mixer controls:
|
||||||
'Master'
|
'Master'
|
||||||
@@ -726,9 +805,3 @@ argument::
|
|||||||
Capabilities: Playback Volume Playback Mute
|
Capabilities: Playback Volume Playback Mute
|
||||||
Channel 0 volume: 61%
|
Channel 0 volume: 61%
|
||||||
Channel 1 volume: 61%
|
Channel 1 volume: 61%
|
||||||
|
|
||||||
.. rubric:: Footnotes
|
|
||||||
|
|
||||||
.. [#f1] ALSA also allows ``PCM_ASYNC``, but this is not supported yet.
|
|
||||||
|
|
||||||
.. _poll_objects: http://docs.python.org/library/select.html#poll-objects
|
|
||||||
|
|||||||
@@ -7,33 +7,19 @@ Introduction
|
|||||||
|
|
||||||
.. |release| replace:: version
|
.. |release| replace:: version
|
||||||
|
|
||||||
.. % At minimum, give your name and an email address. You can include a
|
|
||||||
.. % snail-mail address if you like.
|
|
||||||
|
|
||||||
.. % This makes the Abstract go on a separate page in the HTML version;
|
|
||||||
.. % if a copyright notice is used, it should go immediately after this.
|
|
||||||
.. %
|
|
||||||
|
|
||||||
|
|
||||||
.. _front:
|
.. _front:
|
||||||
|
|
||||||
This software is licensed under the PSF license - the same one used by the
|
This software is licensed under the PSF license - the same one used by the
|
||||||
majority of the python distribution. Basically you can use it for anything you
|
majority of the python distribution. Basically you can use it for anything you
|
||||||
wish (even commercial purposes). There is no warranty whatsoever.
|
wish (even commercial purposes). There is no warranty whatsoever.
|
||||||
|
|
||||||
.. % Copyright statement should go here, if needed.
|
|
||||||
|
|
||||||
.. % The abstract should be a paragraph or two long, and describe the
|
|
||||||
.. % scope of the document.
|
|
||||||
|
|
||||||
|
|
||||||
.. topic:: Abstract
|
.. topic:: Abstract
|
||||||
|
|
||||||
This package contains wrappers for accessing the ALSA API from Python. It is
|
This package contains wrappers for accessing the ALSA API from Python. It is
|
||||||
currently fairly complete for PCM devices and Mixer access. MIDI sequencer
|
currently fairly complete for PCM devices and Mixer access. MIDI sequencer
|
||||||
support is low on our priority list, but volunteers are welcome.
|
support is low on our priority list, but volunteers are welcome.
|
||||||
|
|
||||||
If you find bugs in the wrappers please use thegithub issue tracker.
|
If you find bugs in the wrappers please use the github issue tracker.
|
||||||
Please don't send bug reports regarding ALSA specifically. There are several
|
Please don't send bug reports regarding ALSA specifically. There are several
|
||||||
bugs in this API, and those should be reported to the ALSA team - not me.
|
bugs in this API, and those should be reported to the ALSA team - not me.
|
||||||
|
|
||||||
@@ -64,8 +50,8 @@ More information about ALSA may be found on the project homepage
|
|||||||
ALSA and Python
|
ALSA and Python
|
||||||
===============
|
===============
|
||||||
|
|
||||||
The older Linux sound API (OSS) which is now deprecated is well supported from
|
The older Linux sound API (OSS) -- which is now deprecated -- is well supported
|
||||||
the standard Python library, through the ossaudiodev module. No native ALSA
|
by the standard Python library, through the ossaudiodev module. No native ALSA
|
||||||
support exists in the standard library.
|
support exists in the standard library.
|
||||||
|
|
||||||
There are a few other "ALSA for Python" projects available, including at least
|
There are a few other "ALSA for Python" projects available, including at least
|
||||||
@@ -106,6 +92,7 @@ And then as root: --- ::
|
|||||||
|
|
||||||
# python setup.py install
|
# python setup.py install
|
||||||
|
|
||||||
|
|
||||||
*******
|
*******
|
||||||
Testing
|
Testing
|
||||||
*******
|
*******
|
||||||
@@ -130,7 +117,7 @@ with ``Ctl-C``.
|
|||||||
|
|
||||||
Play back the recording with::
|
Play back the recording with::
|
||||||
|
|
||||||
$ python playbacktest.py-d <device> <filename>
|
$ python playbacktest.py -d <device> <filename>
|
||||||
|
|
||||||
There is a minimal test suite in :code:`test.py`, but it is
|
There is a minimal test suite in :code:`test.py`, but it is
|
||||||
a bit dependent on the ALSA configuration and may fail without indicating
|
a bit dependent on the ALSA configuration and may fail without indicating
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Sample
|
|||||||
|
|
||||||
Musically, the sample size determines the dynamic range. The
|
Musically, the sample size determines the dynamic range. The
|
||||||
dynamic range is the difference between the quietest and the
|
dynamic range is the difference between the quietest and the
|
||||||
loudest signal that can be resproduced.
|
loudest signal that can be reproduced.
|
||||||
|
|
||||||
Frame
|
Frame
|
||||||
A frame consists of exactly one sample per channel. If there is only one
|
A frame consists of exactly one sample per channel. If there is only one
|
||||||
@@ -28,9 +28,9 @@ Frame
|
|||||||
|
|
||||||
Frame size
|
Frame size
|
||||||
This is the size in bytes of each frame. This can vary a lot: if each sample
|
This is the size in bytes of each frame. This can vary a lot: if each sample
|
||||||
is 8 bits, and we're handling mono sound, the frame size is one byte.
|
is 8 bits, and we're handling mono sound, the frame size is one byte.
|
||||||
Similarly in 6 channel audio with 64 bit floating point samples, the frame
|
For six channel audio with 64 bit floating point samples, the frame size
|
||||||
size is 48 bytes
|
is 48 bytes.
|
||||||
|
|
||||||
Rate
|
Rate
|
||||||
PCM sound consists of a flow of sound frames. The sound rate controls how
|
PCM sound consists of a flow of sound frames. The sound rate controls how
|
||||||
@@ -38,7 +38,7 @@ Rate
|
|||||||
means that a new frame is played or captured 8000 times per second.
|
means that a new frame is played or captured 8000 times per second.
|
||||||
|
|
||||||
Data rate
|
Data rate
|
||||||
This is the number of bytes, which must be recorded or provided per
|
This is the number of bytes which must be consumed or provided per
|
||||||
second at a certain frame size and rate.
|
second at a certain frame size and rate.
|
||||||
|
|
||||||
8000 Hz mono sound with 8 bit (1 byte) samples has a data rate of
|
8000 Hz mono sound with 8 bit (1 byte) samples has a data rate of
|
||||||
@@ -46,24 +46,40 @@ Data rate
|
|||||||
|
|
||||||
At the other end of the scale, 96000 Hz, 6 channel sound with 64
|
At the other end of the scale, 96000 Hz, 6 channel sound with 64
|
||||||
bit (8 bytes) samples has a data rate of 96000 \* 6 \* 8 = 4608
|
bit (8 bytes) samples has a data rate of 96000 \* 6 \* 8 = 4608
|
||||||
kb/s (almost 5 MB sound data per second)
|
kb/s (almost 5 MB sound data per second).
|
||||||
|
|
||||||
|
If the data rate requirement is not met, an overrun (on capture) or
|
||||||
|
underrun (on playback) occurs; the term "xrun" is used to refer to
|
||||||
|
either event.
|
||||||
|
|
||||||
|
.. _term-period:
|
||||||
|
|
||||||
Period
|
Period
|
||||||
When the hardware processes data this is done in chunks of frames. The time
|
The CPU processes sample data in chunks of frames, so-called periods
|
||||||
interval between each processing (A/D or D/A conversion) is known
|
(also called fragments by some systems). The operating system kernel's
|
||||||
as the period.
|
sample buffer must hold at least two periods (at any given time, one
|
||||||
The size of the period has direct implication on the latency of the
|
is processed by the sound hardware, and one by the CPU).
|
||||||
sound input or output. For low-latency the period size should be
|
|
||||||
very small, while low CPU resource usage would usually demand
|
The completion of a *period* triggers a CPU interrupt, which causes
|
||||||
larger period sizes. With ALSA, the CPU utilization is not impacted
|
processing and context switching overhead. Therefore, a smaller period
|
||||||
much by the period size, since the kernel layer buffers multiple
|
size causes higher CPU resource usage at a given data rate.
|
||||||
periods internally, so each period generates an interrupt and a
|
|
||||||
memory copy, but userspace can be slower and read or write multiple
|
A bigger size of the *buffer* improves the system's resilience to xruns.
|
||||||
periods at the same time.
|
The buffer being split into a bigger number of smaller periods also does
|
||||||
|
that, as it allows it to be drained / topped up sooner.
|
||||||
|
|
||||||
|
On the other hand, a bigger size of the *buffer* also increases the
|
||||||
|
playback latency, that is, the time it takes for a frame from being
|
||||||
|
sent out by the application to being actually audible.
|
||||||
|
|
||||||
|
Similarly, a bigger *period* size increases the capture latency.
|
||||||
|
|
||||||
|
The trade-off between latency, xrun resilience, and resource usage
|
||||||
|
must be made depending on the application.
|
||||||
|
|
||||||
Period size
|
Period size
|
||||||
This is the size of each period in Hz. *Not bytes, but Hz!.* In
|
This is the size of each period in frames. *Not bytes, but frames!*
|
||||||
:mod:`alsaaudio` the period size is set directly, and it is
|
In :mod:`alsaaudio` the period size is set directly, and it is
|
||||||
therefore important to understand the significance of this
|
therefore important to understand the significance of this
|
||||||
number. If the period size is configured to for example 32,
|
number. If the period size is configured to for example 32,
|
||||||
each write should contain exactly 32 frames of sound data, and each
|
each write should contain exactly 32 frames of sound data, and each
|
||||||
|
|||||||
397
loopback.py
Normal file
397
loopback.py
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import select
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from alsaaudio import (PCM, pcms, PCM_PLAYBACK, PCM_CAPTURE, PCM_NONBLOCK, Mixer,
|
||||||
|
PCM_STATE_OPEN, PCM_STATE_SETUP, PCM_STATE_PREPARED, PCM_STATE_RUNNING, PCM_STATE_XRUN, PCM_STATE_DRAINING,
|
||||||
|
PCM_STATE_PAUSED, PCM_STATE_SUSPENDED, ALSAAudioError)
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
poll_names = {
|
||||||
|
select.POLLIN: 'POLLIN',
|
||||||
|
select.POLLPRI: 'POLLPRI',
|
||||||
|
select.POLLOUT: 'POLLOUT',
|
||||||
|
select.POLLERR: 'POLLERR',
|
||||||
|
select.POLLHUP: 'POLLHUP',
|
||||||
|
select.POLLRDHUP: 'POLLRDHUP',
|
||||||
|
select.POLLNVAL: 'POLLNVAL'
|
||||||
|
}
|
||||||
|
|
||||||
|
state_names = {
|
||||||
|
PCM_STATE_OPEN: 'PCM_STATE_OPEN',
|
||||||
|
PCM_STATE_SETUP: 'PCM_STATE_SETUP',
|
||||||
|
PCM_STATE_PREPARED: 'PCM_STATE_PREPARED',
|
||||||
|
PCM_STATE_RUNNING: 'PCM_STATE_RUNNING',
|
||||||
|
PCM_STATE_XRUN: 'PCM_STATE_XRUN',
|
||||||
|
PCM_STATE_DRAINING: 'PCM_STATE_DRAINING',
|
||||||
|
PCM_STATE_PAUSED: 'PCM_STATE_PAUSED',
|
||||||
|
PCM_STATE_SUSPENDED: 'PCM_STATE_SUSPENDED'
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll_desc(mask):
|
||||||
|
return '|'.join([poll_names[bit] for bit, name in poll_names.items() if mask & bit])
|
||||||
|
|
||||||
|
class PollDescriptor(object):
|
||||||
|
'''File Descriptor, event mask and a name for logging'''
|
||||||
|
def __init__(self, name, fd, mask):
|
||||||
|
self.name = name
|
||||||
|
self.fd = fd
|
||||||
|
self.mask = mask
|
||||||
|
|
||||||
|
def as_tuple(self):
|
||||||
|
return (self.fd, self.mask)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_alsa_object(cls, name, alsaobject, mask=None):
|
||||||
|
# TODO maybe refactor: we ignore objects that have more then one polldescriptor
|
||||||
|
fd, alsamask = alsaobject.polldescriptors()[0]
|
||||||
|
|
||||||
|
if mask is None:
|
||||||
|
mask = alsamask
|
||||||
|
|
||||||
|
return cls(name, fd, mask)
|
||||||
|
|
||||||
|
class Loopback(object):
|
||||||
|
'''Loopback state and event handling'''
|
||||||
|
|
||||||
|
def __init__(self, capture, playback_args, volume_handler, run_after_stop=None, run_before_start=None):
|
||||||
|
self.playback_args = playback_args
|
||||||
|
self.playback = None
|
||||||
|
self.volume_handler = volume_handler
|
||||||
|
self.capture_started = None
|
||||||
|
self.last_capture_event = None
|
||||||
|
|
||||||
|
self.capture = capture
|
||||||
|
self.capture_pd = PollDescriptor.from_alsa_object('capture', capture)
|
||||||
|
|
||||||
|
self.run_after_stop = run_after_stop.split(' ')
|
||||||
|
self.run_before_start = run_before_start.split(' ')
|
||||||
|
self.run_after_stop_did_run = False
|
||||||
|
|
||||||
|
self.waitBeforeOpen = False
|
||||||
|
self.queue = []
|
||||||
|
|
||||||
|
self.period_size = 0
|
||||||
|
self.silent_periods = 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compute_energy(data):
|
||||||
|
values = struct.unpack(f'{len(data)//2}h', data)
|
||||||
|
e = 0
|
||||||
|
for v in values:
|
||||||
|
e = e + v * v
|
||||||
|
|
||||||
|
return e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def run_command(cmd):
|
||||||
|
if cmd:
|
||||||
|
rc = subprocess.run(cmd)
|
||||||
|
if rc.returncode:
|
||||||
|
logging.warning(f'run {cmd}, return code {rc.returncode}')
|
||||||
|
else:
|
||||||
|
logging.info(f'run {cmd}, return code {rc.returncode}')
|
||||||
|
|
||||||
|
def register(self, reactor):
|
||||||
|
reactor.register_timeout_handler(self.timeout_handler)
|
||||||
|
reactor.register(self.capture_pd, self)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
# start reading data
|
||||||
|
size, data = self.capture.read()
|
||||||
|
if size:
|
||||||
|
self.queue.append(data)
|
||||||
|
|
||||||
|
def timeout_handler(self):
|
||||||
|
if self.playback and self.capture_started:
|
||||||
|
if self.last_capture_event:
|
||||||
|
if datetime.now() - self.last_capture_event > timedelta(seconds=2):
|
||||||
|
logging.info('timeout - closing playback device')
|
||||||
|
self.playback.close()
|
||||||
|
self.playback = None
|
||||||
|
self.capture_started = None
|
||||||
|
if self.volume_handler:
|
||||||
|
self.volume_handler.stop()
|
||||||
|
self.run_command(self.run_after_stop)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.waitBeforeOpen = False
|
||||||
|
|
||||||
|
if not self.run_after_stop_did_run and not self.playback:
|
||||||
|
if self.volume_handler:
|
||||||
|
self.volume_handler.stop()
|
||||||
|
self.run_command(self.run_after_stop)
|
||||||
|
self.run_after_stop_did_run = True
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
if len(self.queue):
|
||||||
|
return self.queue.pop()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle_capture_event(self, eventmask, name):
|
||||||
|
'''called when data is available for reading'''
|
||||||
|
self.last_capture_event = datetime.now()
|
||||||
|
size, data = self.capture.read()
|
||||||
|
if not size:
|
||||||
|
logging.warning(f'capture event but no data')
|
||||||
|
return False
|
||||||
|
|
||||||
|
energy = self.compute_energy(data)
|
||||||
|
logging.debug(f'energy: {energy}')
|
||||||
|
|
||||||
|
# the usecase is a USB capture device where we get perfect silence when it's idle
|
||||||
|
if energy == 0:
|
||||||
|
self.silent_periods = self.silent_periods + 1
|
||||||
|
|
||||||
|
# turn off playback after two seconds of silence
|
||||||
|
# 2 channels * 2 seconds * 2 bytes per sample
|
||||||
|
fps = self.playback_args['rate'] * 8 // (self.playback_args['periodsize'] * self.playback_args['periods'])
|
||||||
|
|
||||||
|
logging.debug(f'{self.silent_periods} of {fps} silent periods: {self.playback}')
|
||||||
|
|
||||||
|
if self.silent_periods > fps and self.playback:
|
||||||
|
logging.info(f'closing playback due to silence')
|
||||||
|
self.playback.close()
|
||||||
|
self.playback = None
|
||||||
|
if self.volume_handler:
|
||||||
|
self.volume_handler.stop()
|
||||||
|
self.run_command(self.run_after_stop)
|
||||||
|
self.run_after_stop_did_run = True
|
||||||
|
|
||||||
|
if not self.playback:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.silent_periods = 0
|
||||||
|
|
||||||
|
if not self.playback:
|
||||||
|
if self.waitBeforeOpen:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
if self.volume_handler:
|
||||||
|
self.volume_handler.start()
|
||||||
|
self.run_command(self.run_before_start)
|
||||||
|
self.playback = PCM(**self.playback_args)
|
||||||
|
self.period_size = self.playback.info()['period_size']
|
||||||
|
logging.info(f'opened playback device with period_size {self.period_size}')
|
||||||
|
except ALSAAudioError as e:
|
||||||
|
logging.info('opening PCM playback device failed: %s', e)
|
||||||
|
self.waitBeforeOpen = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.capture_started = datetime.now()
|
||||||
|
logging.info(f'{self.playback} capture started: {self.capture_started}')
|
||||||
|
|
||||||
|
self.queue.append(data)
|
||||||
|
|
||||||
|
if len(self.queue) <= 2:
|
||||||
|
logging.info(f'buffering: {len(self.queue)}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.pop()
|
||||||
|
if data:
|
||||||
|
space = self.playback.avail()
|
||||||
|
written = self.playback.write(data)
|
||||||
|
logging.debug(f'wrote {written} bytes while space was {space}')
|
||||||
|
except ALSAAudioError:
|
||||||
|
logging.error('underrun', exc_info=1)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __call__(self, fd, eventmask, name):
|
||||||
|
|
||||||
|
if fd == self.capture_pd.fd:
|
||||||
|
real_mask = self.capture.polldescriptors_revents([self.capture_pd.as_tuple()])
|
||||||
|
if real_mask:
|
||||||
|
return self.handle_capture_event(real_mask, name)
|
||||||
|
else:
|
||||||
|
logging.debug('null capture event')
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
real_mask = self.playback.polldescriptors_revents([self.playback_pd.as_tuple()])
|
||||||
|
if real_mask:
|
||||||
|
return self.handle_playback_event(real_mask, name)
|
||||||
|
else:
|
||||||
|
logging.debug('null playback event')
|
||||||
|
return False
|
||||||
|
|
||||||
|
class VolumeForwarder(object):
|
||||||
|
'''Volume control event handling'''
|
||||||
|
|
||||||
|
def __init__(self, capture_control, playback_control):
|
||||||
|
self.playback_control = playback_control
|
||||||
|
self.capture_control = capture_control
|
||||||
|
self.active = True
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.active = True
|
||||||
|
if self.volume:
|
||||||
|
self.volume = playback_control.setvolume(self.volume)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.active = False
|
||||||
|
self.volume = self.playback_control.getvolume(pcmtype=PCM_CAPTURE)[0]
|
||||||
|
|
||||||
|
def __call__(self, fd, eventmask, name):
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
|
||||||
|
volume = self.capture_control.getvolume(pcmtype=PCM_CAPTURE)
|
||||||
|
# indicate that we've handled the event
|
||||||
|
self.capture_control.handleevents()
|
||||||
|
logging.info(f'{name} adjusting volume to {volume}')
|
||||||
|
if volume:
|
||||||
|
self.playback_control.setvolume(volume[0])
|
||||||
|
|
||||||
|
|
||||||
|
class Reactor(object):
|
||||||
|
'''A wrapper around select.poll'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.poll = select.poll()
|
||||||
|
self.descriptors = {}
|
||||||
|
self.timeout_handlers = set()
|
||||||
|
|
||||||
|
def register(self, polldescriptor, callable):
|
||||||
|
logging.debug(f'registered {polldescriptor.name}: {poll_desc(polldescriptor.mask)}')
|
||||||
|
self.descriptors[polldescriptor.fd] = (polldescriptor, callable)
|
||||||
|
self.poll.register(polldescriptor.fd, polldescriptor.mask)
|
||||||
|
|
||||||
|
def unregister(self, polldescriptor):
|
||||||
|
self.poll.unregister(polldescriptor.fd)
|
||||||
|
del self.descriptors[polldescriptor.fd]
|
||||||
|
|
||||||
|
def register_timeout_handler(self, callable):
|
||||||
|
self.timeout_handlers.add(callable)
|
||||||
|
|
||||||
|
def unregister_timeout_handler(self, callable):
|
||||||
|
self.timeout_handlers.remove(callable)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
last_timeout_ev = datetime.now()
|
||||||
|
while True:
|
||||||
|
# poll for a bit, then send a timeout to registered handlers
|
||||||
|
events = self.poll.poll(0.25)
|
||||||
|
for fd, ev in events:
|
||||||
|
polldescriptor, handler = self.descriptors[fd]
|
||||||
|
|
||||||
|
# very chatty - log all events
|
||||||
|
# logging.debug(f'{polldescriptor.name}: {poll_desc(ev)} ({ev})')
|
||||||
|
|
||||||
|
handler(fd, ev, polldescriptor.name)
|
||||||
|
|
||||||
|
if datetime.now() - last_timeout_ev > timedelta(seconds=0.25):
|
||||||
|
for t in self.timeout_handlers:
|
||||||
|
t()
|
||||||
|
last_timeout_ev = datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO)
|
||||||
|
|
||||||
|
parser = ArgumentParser(description='ALSA loopback (with volume forwarding)')
|
||||||
|
|
||||||
|
playback_pcms = pcms(pcmtype=PCM_PLAYBACK)
|
||||||
|
capture_pcms = pcms(pcmtype=PCM_CAPTURE)
|
||||||
|
|
||||||
|
if not playback_pcms:
|
||||||
|
logging.error('no playback PCM found')
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
if not capture_pcms:
|
||||||
|
logging.error('no capture PCM found')
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
parser.add_argument('-d', '--debug', action='store_true')
|
||||||
|
parser.add_argument('-i', '--input', default=capture_pcms[0])
|
||||||
|
parser.add_argument('-o', '--output', default=playback_pcms[0])
|
||||||
|
parser.add_argument('-r', '--rate', type=int, default=44100)
|
||||||
|
parser.add_argument('-c', '--channels', type=int, default=2)
|
||||||
|
parser.add_argument('-p', '--periodsize', type=int, default=444) # must be divisible by 6 for 44k1
|
||||||
|
parser.add_argument('-P', '--periods', type=int, default=2)
|
||||||
|
parser.add_argument('-I', '--input-mixer', help='Control of the input mixer, can contain the card index, e.g. Digital:2')
|
||||||
|
parser.add_argument('-O', '--output-mixer', help='Control of the output mixer, can contain the card index, e.g. PCM:1')
|
||||||
|
parser.add_argument('-A', '--run-after-stop', help='command to run when the capture device is idle/silent')
|
||||||
|
parser.add_argument('-B', '--run-before-start', help='command to run when the capture device becomes active')
|
||||||
|
parser.add_argument('-V', '--volume', help='Initial volume (default is leave unchanged)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
playback_args = {
|
||||||
|
'type': PCM_PLAYBACK,
|
||||||
|
'mode': PCM_NONBLOCK,
|
||||||
|
'device': args.output,
|
||||||
|
'rate': args.rate,
|
||||||
|
'channels': args.channels,
|
||||||
|
'periodsize': args.periodsize,
|
||||||
|
'periods': args.periods
|
||||||
|
}
|
||||||
|
|
||||||
|
reactor = Reactor()
|
||||||
|
|
||||||
|
# If args.input_mixer and args.output_mixer are set, forward the capture volume to the playback volume.
|
||||||
|
# The usecase is a capture device that is implemented using g_audio, i.e. the Linux USB gadget driver.
|
||||||
|
# When a USB device (eg. an iPad) is connected to this machine, its volume events will go to the volume control
|
||||||
|
# of the output device
|
||||||
|
|
||||||
|
capture = None
|
||||||
|
playback = None
|
||||||
|
volume_handler = None
|
||||||
|
if args.input_mixer and args.output_mixer:
|
||||||
|
re_mixer = re.compile(r'([a-zA-Z0-9]+):?([0-9+])?')
|
||||||
|
|
||||||
|
input_mixer_card = None
|
||||||
|
m = re_mixer.match(args.input_mixer)
|
||||||
|
if m:
|
||||||
|
input_mixer = m.group(1)
|
||||||
|
if m.group(2):
|
||||||
|
input_mixer_card = int(m.group(2))
|
||||||
|
else:
|
||||||
|
parser.print_usage()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
output_mixer_card = None
|
||||||
|
m = re_mixer.match(args.output_mixer)
|
||||||
|
if m:
|
||||||
|
output_mixer = m.group(1)
|
||||||
|
if m.group(2):
|
||||||
|
output_mixer_card = int(m.group(2))
|
||||||
|
else:
|
||||||
|
parser.print_usage()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if input_mixer_card is None:
|
||||||
|
capture = PCM(type=PCM_CAPTURE, mode=PCM_NONBLOCK, device=args.input, rate=args.rate,
|
||||||
|
channels=args.channels, periodsize=args.periodsize, periods=args.periods)
|
||||||
|
input_mixer_card = capture.info()['card_no']
|
||||||
|
|
||||||
|
if output_mixer_card is None:
|
||||||
|
playback = PCM(**playback_args)
|
||||||
|
output_mixer_card = playback.info()['card_no']
|
||||||
|
playback.close()
|
||||||
|
|
||||||
|
playback_control = Mixer(control=output_mixer, cardindex=int(output_mixer_card))
|
||||||
|
capture_control = Mixer(control=input_mixer, cardindex=int(input_mixer_card))
|
||||||
|
|
||||||
|
volume_handler = VolumeForwarder(capture_control, playback_control)
|
||||||
|
reactor.register(PollDescriptor.from_alsa_object('capture_control', capture_control, select.POLLIN), volume_handler)
|
||||||
|
|
||||||
|
if args.volume and playback_control:
|
||||||
|
playback_control.setvolume(int(args.volume))
|
||||||
|
|
||||||
|
loopback = Loopback(capture, playback_args, volume_handler, args.run_after_stop, args.run_before_start)
|
||||||
|
loopback.register(reactor)
|
||||||
|
loopback.start()
|
||||||
|
|
||||||
|
reactor.run()
|
||||||
11
mixertest.py
11
mixertest.py
@@ -72,8 +72,13 @@ def show_mixer(name, kwargs):
|
|||||||
volumes = mixer.getvolume()
|
volumes = mixer.getvolume()
|
||||||
volumes_dB = mixer.getvolume(units=alsaaudio.VOLUME_UNITS_DB)
|
volumes_dB = mixer.getvolume(units=alsaaudio.VOLUME_UNITS_DB)
|
||||||
for i in range(len(volumes)):
|
for i in range(len(volumes)):
|
||||||
print("Channel %i volume: %i%% (%.1f dB)" % (i, volumes[i], volumes_dB[i] / 100.0))
|
print("Channel %i playback volume: %i%% (%.1f dB)" % (i, volumes[i], volumes_dB[i] / 100.0))
|
||||||
|
|
||||||
|
volumes = mixer.getvolume(pcmtype=alsaaudio.PCM_CAPTURE)
|
||||||
|
volumes_dB = mixer.getvolume(pcmtype=alsaaudio.PCM_CAPTURE, units=alsaaudio.VOLUME_UNITS_DB)
|
||||||
|
for i in range(len(volumes)):
|
||||||
|
print("Channel %i capture volume: %i%% (%.1f dB)" % (i, volumes[i], volumes_dB[i] / 100.0))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mutes = mixer.getmute()
|
mutes = mixer.getmute()
|
||||||
for i in range(len(mutes)):
|
for i in range(len(mutes)):
|
||||||
@@ -113,7 +118,7 @@ def set_mixer(name, args, kwargs):
|
|||||||
mixer.setmute(1, channel)
|
mixer.setmute(1, channel)
|
||||||
else:
|
else:
|
||||||
mixer.setmute(0, channel)
|
mixer.setmute(0, channel)
|
||||||
|
|
||||||
elif args in ['rec','unrec']:
|
elif args in ['rec','unrec']:
|
||||||
# Enable/disable recording
|
# Enable/disable recording
|
||||||
if args == 'rec':
|
if args == 'rec':
|
||||||
|
|||||||
@@ -49,5 +49,5 @@ if __name__ == '__main__':
|
|||||||
while data:
|
while data:
|
||||||
out.write(data)
|
out.write(data)
|
||||||
data = f.read(320)
|
data = f.read(320)
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
|||||||
6
setup.py
6
setup.py
@@ -8,7 +8,7 @@ from setuptools import setup
|
|||||||
from setuptools.extension import Extension
|
from setuptools.extension import Extension
|
||||||
from sys import version
|
from sys import version
|
||||||
|
|
||||||
pyalsa_version = '0.9.2'
|
pyalsa_version = '0.10.1'
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
setup(
|
setup(
|
||||||
@@ -29,12 +29,12 @@ if __name__ == '__main__':
|
|||||||
'License :: OSI Approved :: Python Software Foundation License',
|
'License :: OSI Approved :: Python Software Foundation License',
|
||||||
'Operating System :: POSIX :: Linux',
|
'Operating System :: POSIX :: Linux',
|
||||||
'Programming Language :: Python :: 2',
|
'Programming Language :: Python :: 2',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Topic :: Multimedia :: Sound/Audio',
|
'Topic :: Multimedia :: Sound/Audio',
|
||||||
'Topic :: Multimedia :: Sound/Audio :: Mixers',
|
'Topic :: Multimedia :: Sound/Audio :: Mixers',
|
||||||
'Topic :: Multimedia :: Sound/Audio :: Players',
|
'Topic :: Multimedia :: Sound/Audio :: Players',
|
||||||
'Topic :: Multimedia :: Sound/Audio :: Capture/Recording',
|
'Topic :: Multimedia :: Sound/Audio :: Capture/Recording',
|
||||||
],
|
],
|
||||||
ext_modules=[Extension('alsaaudio',['alsaaudio.c'],
|
ext_modules=[Extension('alsaaudio',['alsaaudio.c'],
|
||||||
libraries=['asound'])]
|
libraries=['asound'])]
|
||||||
)
|
)
|
||||||
|
|||||||
37
test.py
37
test.py
@@ -11,6 +11,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import alsaaudio
|
import alsaaudio
|
||||||
import warnings
|
import warnings
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
# we can't test read and write well - these are tested otherwise
|
# we can't test read and write well - these are tested otherwise
|
||||||
PCMMethods = [
|
PCMMethods = [
|
||||||
@@ -20,7 +21,7 @@ PCMMethods = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
PCMDeprecatedMethods = [
|
PCMDeprecatedMethods = [
|
||||||
('setchannels', (2,)),
|
('setchannels', (2,)),
|
||||||
('setrate', (44100,)),
|
('setrate', (44100,)),
|
||||||
('setformat', (alsaaudio.PCM_FORMAT_S8,)),
|
('setformat', (alsaaudio.PCM_FORMAT_S8,)),
|
||||||
('setperiodsize', (320,))
|
('setperiodsize', (320,))
|
||||||
@@ -49,10 +50,10 @@ class MixerTest(unittest.TestCase):
|
|||||||
|
|
||||||
def testMixer(self):
|
def testMixer(self):
|
||||||
"""Open the default Mixers and the Mixers on every card"""
|
"""Open the default Mixers and the Mixers on every card"""
|
||||||
|
|
||||||
for c in alsaaudio.card_indexes():
|
for c in alsaaudio.card_indexes():
|
||||||
mixers = alsaaudio.mixers(cardindex=c)
|
mixers = alsaaudio.mixers(cardindex=c)
|
||||||
|
|
||||||
for m in mixers:
|
for m in mixers:
|
||||||
mixer = alsaaudio.Mixer(m, cardindex=c)
|
mixer = alsaaudio.Mixer(m, cardindex=c)
|
||||||
mixer.close()
|
mixer.close()
|
||||||
@@ -73,7 +74,7 @@ class MixerTest(unittest.TestCase):
|
|||||||
mixer.close()
|
mixer.close()
|
||||||
|
|
||||||
def testMixerClose(self):
|
def testMixerClose(self):
|
||||||
"""Run common Mixer methods on a closed object and verify it raises an
|
"""Run common Mixer methods on a closed object and verify it raises an
|
||||||
error"""
|
error"""
|
||||||
|
|
||||||
mixers = alsaaudio.mixers()
|
mixers = alsaaudio.mixers()
|
||||||
@@ -133,7 +134,7 @@ class PCMTest(unittest.TestCase):
|
|||||||
pcm = alsaaudio.PCM(card='default')
|
pcm = alsaaudio.PCM(card='default')
|
||||||
except alsaaudio.ALSAAudioError:
|
except alsaaudio.ALSAAudioError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Verify we got a DepreciationWarning
|
# Verify we got a DepreciationWarning
|
||||||
self.assertEqual(len(w), 1, "PCM(card='default') expected a warning" )
|
self.assertEqual(len(w), 1, "PCM(card='default') expected a warning" )
|
||||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning), "PCM(card='default') expected a DeprecationWarning")
|
self.assertTrue(issubclass(w[-1].category, DeprecationWarning), "PCM(card='default') expected a DeprecationWarning")
|
||||||
@@ -156,5 +157,31 @@ class PCMTest(unittest.TestCase):
|
|||||||
self.assertEqual(len(w), 1, method + " expected a warning")
|
self.assertEqual(len(w), 1, method + " expected a warning")
|
||||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning), method + " expected a DeprecationWarning")
|
self.assertTrue(issubclass(w[-1].category, DeprecationWarning), method + " expected a DeprecationWarning")
|
||||||
|
|
||||||
|
class PollDescriptorArgsTest(unittest.TestCase):
|
||||||
|
'''Test invalid args for polldescriptors_revents (takes a list of tuples of 2 integers)'''
|
||||||
|
def testArgsNoList(self):
|
||||||
|
with closing(alsaaudio.PCM()) as pcm:
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
pcm.polldescriptors_revents('foo')
|
||||||
|
|
||||||
|
def testArgsListButNoTuples(self):
|
||||||
|
with closing(alsaaudio.PCM()) as pcm:
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
pcm.polldescriptors_revents(['foo', 1])
|
||||||
|
|
||||||
|
def testArgsListButInvalidTuples(self):
|
||||||
|
with closing(alsaaudio.PCM()) as pcm:
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
pcm.polldescriptors_revents([('foo', 'bar')])
|
||||||
|
|
||||||
|
def testArgsListTupleWrongLength(self):
|
||||||
|
with closing(alsaaudio.PCM()) as pcm:
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
pcm.polldescriptors_revents([(1, )])
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
pcm.polldescriptors_revents([(1, 2, 3)])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user