forked from auracaster/pyalsaaudio
Compare commits
17 Commits
0.11.0
...
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
|
||||
- Fix alsamixer_getvolume (#112 from @stephensp)
|
||||
|
||||
@@ -15,15 +28,15 @@
|
||||
supported formats - e.g. `{"U8": 1, "S16_LE": 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]`,
|
||||
- `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)`.
|
||||
|
||||
|
||||
(#82 contributed by @jdstmporter)
|
||||
|
||||
- Prevent hang on close after capturing audio (#80 contributed by @daym)
|
||||
|
||||
# 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)
|
||||
|
||||
- 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
|
||||
===================================================
|
||||
|
||||
@@ -18,15 +13,13 @@ Download
|
||||
========
|
||||
|
||||
* `Download from pypi <https://pypi.python.org/pypi/pyalsaaudio>`_
|
||||
|
||||
|
||||
|
||||
Github
|
||||
======
|
||||
|
||||
* `Repository <https://github.com/larsimmisch/pyalsaaudio/>`_
|
||||
* `Bug tracker <https://github.com/larsimmisch/pyalsaaudio/issues>`_
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
@@ -34,5 +27,3 @@ Indices and tables
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,38 +5,15 @@
|
||||
.. module:: alsaaudio
|
||||
:platform: Linux
|
||||
|
||||
|
||||
.. % \declaremodule{builtin}{alsaaudio} % standard library, in C
|
||||
.. % not standard, in C
|
||||
|
||||
.. moduleauthor:: Casper Wilstrup <cwi@aves.dk>
|
||||
.. moduleauthor:: Lars Immisch <lars@ibp.de>
|
||||
|
||||
.. % Author of the module code;
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
List available PCM devices by name.
|
||||
|
||||
|
||||
Arguments are:
|
||||
|
||||
* *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
|
||||
useful. If you want to see a list of available PCM devices, use :func:`pcms`
|
||||
instead.
|
||||
|
||||
|
||||
..
|
||||
Omitted by intention due to being superseded by cards():
|
||||
|
||||
.. function:: card_indexes()
|
||||
.. function:: card_name()
|
||||
|
||||
.. function:: mixers(cardindex=-1, device='default')
|
||||
|
||||
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
|
||||
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
|
||||
is ``'default'``.
|
||||
|
||||
**Note:** For a list of available controls, you can also use ``amixer`` on
|
||||
the commandline::
|
||||
|
||||
|
||||
$ amixer
|
||||
|
||||
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
|
||||
|
||||
*Changed in 0.8*:
|
||||
|
||||
|
||||
- The keyword argument `device` is new and can be used to
|
||||
select virtual devices. As a result, the default behaviour has subtly
|
||||
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.
|
||||
|
||||
|
||||
.. _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
|
||||
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
|
||||
recording). The arguments are:
|
||||
@@ -123,7 +109,7 @@ following arguments:
|
||||
* *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.
|
||||
The default value is :const:`PCM_FORMAT_S16_LE`.
|
||||
|
||||
|
||||
========================= ===============
|
||||
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)
|
||||
========================= ===============
|
||||
|
||||
* *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
|
||||
a value from the output of :func:`pcms`). The default value is
|
||||
``'default'``.
|
||||
@@ -165,14 +155,20 @@ following arguments:
|
||||
the `device` keyword argument is ignored.
|
||||
``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.
|
||||
|
||||
*Changed in 0.10:*
|
||||
|
||||
- Added the optional named parameter `periods`.
|
||||
|
||||
*Changed in 0.9:*
|
||||
|
||||
- Added the optional named parameters `rate`, `channels`, `format` and `periodsize`.
|
||||
|
||||
*Changed in 0.8:*
|
||||
|
||||
|
||||
- The `card` keyword argument is still supported,
|
||||
but deprecated. Please use `device` instead.
|
||||
|
||||
@@ -180,8 +176,7 @@ following arguments:
|
||||
|
||||
The `card` keyword is deprecated because it guesses the real ALSA
|
||||
name of the card. This was always fragile and broke some legitimate usecases.
|
||||
|
||||
|
||||
|
||||
PCM objects have the following methods:
|
||||
|
||||
.. method:: PCM.info()
|
||||
@@ -235,17 +230,43 @@ PCM objects have the following methods:
|
||||
Returns the type of PCM object. Either :const:`PCM_CAPTURE` or
|
||||
:const:`PCM_PLAYBACK`.
|
||||
|
||||
|
||||
.. method:: PCM.pcmmode()
|
||||
|
||||
Return the mode of the PCM object. One of :const:`PCM_NONBLOCK`,
|
||||
:const:`PCM_ASYNC`, or :const:`PCM_NORMAL`
|
||||
|
||||
|
||||
.. method:: PCM.cardname()
|
||||
|
||||
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)
|
||||
|
||||
.. 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`.
|
||||
|
||||
.. method:: PCM.setformat(format)
|
||||
|
||||
|
||||
.. deprecated:: 0.9 Use the `format` named argument to :func:`PCM`.
|
||||
|
||||
.. method:: PCM.setperiodsize(period)
|
||||
|
||||
.. 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()
|
||||
|
||||
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
|
||||
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])
|
||||
|
||||
If *enable* is :const:`True`, playback or capture is paused.
|
||||
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()
|
||||
|
||||
Returns a tuple of *(file descriptor, eventmask)* that can be used to
|
||||
wait for changes on the PCM with *select.poll*.
|
||||
Returns a list of tuples of *(file descriptor, eventmask)* that can be
|
||||
used to wait for changes on the PCM with *select.poll*.
|
||||
|
||||
The *eventmask* value is compatible with `poll.register`__ in the Python
|
||||
:const:`select` module.
|
||||
@@ -347,7 +422,7 @@ PCM objects have the following methods:
|
||||
``PCM_TSTAMP_TYPE_MONOTONIC_RAW`` Monotonic time from an unspecified starting
|
||||
time using only the system clock.
|
||||
================================= ===========================================
|
||||
|
||||
|
||||
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.
|
||||
================================= ===========================================
|
||||
|
||||
|
||||
__ poll_objects_
|
||||
|
||||
**A few hints on using PCM devices for playback**
|
||||
|
||||
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.
|
||||
|
||||
|
||||
.. class:: Mixer(control='Master', id=0, cardindex=-1, device='default')
|
||||
|
||||
Arguments are:
|
||||
|
||||
|
||||
* *control* - specifies which control to manipulate using this mixer
|
||||
object. The list of available controls can be found with the
|
||||
: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
|
||||
value is ``'default'``.
|
||||
|
||||
|
||||
*Changed in 0.8*:
|
||||
|
||||
|
||||
- The keyword argument `device` is new and can be used to select virtual
|
||||
devices.
|
||||
|
||||
|
||||
Mixer objects have the following methods:
|
||||
|
||||
.. method:: Mixer.cardname()
|
||||
|
||||
Return the name of the sound card used by this Mixer object
|
||||
|
||||
|
||||
.. method:: Mixer.mixer()
|
||||
|
||||
Return the name of the specific mixer controlled by this object, For example
|
||||
``'Master'`` or ``'PCM'``
|
||||
|
||||
|
||||
.. method:: Mixer.mixerid()
|
||||
|
||||
Return the ID of the ALSA mixer controlled by this object.
|
||||
|
||||
|
||||
.. method:: Mixer.switchcap()
|
||||
|
||||
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
|
||||
:meth:`setmute` methods
|
||||
|
||||
|
||||
.. method:: Mixer.volumecap()
|
||||
|
||||
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
|
||||
'Joined Capture Volume' Manipulate sound capture volume for all channels at a time
|
||||
======================== ================
|
||||
|
||||
|
||||
.. method:: Mixer.getenum()
|
||||
|
||||
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
|
||||
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
|
||||
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)
|
||||
.. method:: Mixer.getrange(pcmtype=PCM_PLAYBACK, units=VOLUME_UNITS_RAW)
|
||||
|
||||
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
|
||||
: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`.
|
||||
|
||||
The optional *units* argument can be one of :const:`VOLUME_UNITS_PERCENTAGE`,
|
||||
:const:`VOLUME_UNITS_RAW`, or :const:`VOLUME_UNITS_DB`.
|
||||
|
||||
.. 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.getvolume(pcmtype=PCM_PLAYBACK)
|
||||
.. method:: Mixer.getvolume(pcmtype=PCM_PLAYBACK, units=VOLUME_UNITS_PERCENTAGE)
|
||||
|
||||
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
|
||||
: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`.
|
||||
|
||||
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
|
||||
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
|
||||
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`
|
||||
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])
|
||||
|
||||
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
|
||||
|
||||
.. 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])
|
||||
|
||||
@@ -578,20 +656,22 @@ Mixer objects have the following methods:
|
||||
|
||||
.. method:: Mixer.polldescriptors()
|
||||
|
||||
Returns a tuple of *(file descriptor, eventmask)* that can be used to
|
||||
wait for changes on the mixer with *select.poll*.
|
||||
Returns a list of tuples of *(file descriptor, eventmask)* that can be
|
||||
used to wait for changes on the mixer with *select.poll*.
|
||||
|
||||
The *eventmask* value is compatible with `poll.register`__ in the Python
|
||||
:const:`select` module.
|
||||
|
||||
__ poll_objects_
|
||||
|
||||
.. 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.
|
||||
Returns the number of events that were acknowledged.
|
||||
|
||||
.. method:: Mixer.close()
|
||||
|
||||
Closes the Mixer device.
|
||||
|
||||
**A rant on the ALSA Mixer API**
|
||||
|
||||
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
|
||||
painful trial and error process.
|
||||
|
||||
.. % ==== 4. ====
|
||||
|
||||
|
||||
.. _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** will record and play a raw
|
||||
sound file in CD quality.
|
||||
|
||||
@@ -678,7 +757,7 @@ Without arguments, **mixertest.py** will list all available *controls* on the
|
||||
default soundcard.
|
||||
|
||||
The output might look like this::
|
||||
|
||||
|
||||
$ ./mixertest.py
|
||||
Available mixer controls:
|
||||
'Master'
|
||||
@@ -726,9 +805,3 @@ argument::
|
||||
Capabilities: Playback Volume Playback Mute
|
||||
Channel 0 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
|
||||
|
||||
.. % 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:
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
This package contains wrappers for accessing the ALSA API from Python. It is
|
||||
currently fairly complete for PCM devices and Mixer access. MIDI sequencer
|
||||
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
|
||||
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
|
||||
===============
|
||||
|
||||
The older Linux sound API (OSS) which is now deprecated is well supported from
|
||||
the standard Python library, through the ossaudiodev module. No native ALSA
|
||||
The older Linux sound API (OSS) -- which is now deprecated -- is well supported
|
||||
by the standard Python library, through the ossaudiodev module. No native ALSA
|
||||
support exists in the standard library.
|
||||
|
||||
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
|
||||
|
||||
|
||||
*******
|
||||
Testing
|
||||
*******
|
||||
@@ -130,7 +117,7 @@ with ``Ctl-C``.
|
||||
|
||||
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
|
||||
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
|
||||
dynamic range is the difference between the quietest and the
|
||||
loudest signal that can be resproduced.
|
||||
loudest signal that can be reproduced.
|
||||
|
||||
Frame
|
||||
A frame consists of exactly one sample per channel. If there is only one
|
||||
@@ -28,9 +28,9 @@ Frame
|
||||
|
||||
Frame size
|
||||
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.
|
||||
Similarly in 6 channel audio with 64 bit floating point samples, the frame
|
||||
size is 48 bytes
|
||||
is 8 bits, and we're handling mono sound, the frame size is one byte.
|
||||
For six channel audio with 64 bit floating point samples, the frame size
|
||||
is 48 bytes.
|
||||
|
||||
Rate
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
When the hardware processes data this is done in chunks of frames. The time
|
||||
interval between each processing (A/D or D/A conversion) is known
|
||||
as the period.
|
||||
The size of the period has direct implication on the latency of the
|
||||
sound input or output. For low-latency the period size should be
|
||||
very small, while low CPU resource usage would usually demand
|
||||
larger period sizes. With ALSA, the CPU utilization is not impacted
|
||||
much by the period size, since the kernel layer buffers multiple
|
||||
periods internally, so each period generates an interrupt and a
|
||||
memory copy, but userspace can be slower and read or write multiple
|
||||
periods at the same time.
|
||||
The CPU processes sample data in chunks of frames, so-called periods
|
||||
(also called fragments by some systems). The operating system kernel's
|
||||
sample buffer must hold at least two periods (at any given time, one
|
||||
is processed by the sound hardware, and one by the CPU).
|
||||
|
||||
The completion of a *period* triggers a CPU interrupt, which causes
|
||||
processing and context switching overhead. Therefore, a smaller period
|
||||
size causes higher CPU resource usage at a given data rate.
|
||||
|
||||
A bigger size of the *buffer* improves the system's resilience to xruns.
|
||||
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
|
||||
This is the size of each period in Hz. *Not bytes, but Hz!.* In
|
||||
:mod:`alsaaudio` the period size is set directly, and it is
|
||||
This is the size of each period in frames. *Not bytes, but frames!*
|
||||
In :mod:`alsaaudio` the period size is set directly, and it is
|
||||
therefore important to understand the significance of this
|
||||
number. If the period size is configured to for example 32,
|
||||
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_dB = mixer.getvolume(units=alsaaudio.VOLUME_UNITS_DB)
|
||||
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:
|
||||
mutes = mixer.getmute()
|
||||
for i in range(len(mutes)):
|
||||
@@ -113,7 +118,7 @@ def set_mixer(name, args, kwargs):
|
||||
mixer.setmute(1, channel)
|
||||
else:
|
||||
mixer.setmute(0, channel)
|
||||
|
||||
|
||||
elif args in ['rec','unrec']:
|
||||
# Enable/disable recording
|
||||
if args == 'rec':
|
||||
|
||||
@@ -49,5 +49,5 @@ if __name__ == '__main__':
|
||||
while data:
|
||||
out.write(data)
|
||||
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 sys import version
|
||||
|
||||
pyalsa_version = '0.9.2'
|
||||
pyalsa_version = '0.10.1'
|
||||
|
||||
if __name__ == '__main__':
|
||||
setup(
|
||||
@@ -29,12 +29,12 @@ if __name__ == '__main__':
|
||||
'License :: OSI Approved :: Python Software Foundation License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Multimedia :: Sound/Audio',
|
||||
'Topic :: Multimedia :: Sound/Audio :: Mixers',
|
||||
'Topic :: Multimedia :: Sound/Audio :: Players',
|
||||
'Topic :: Multimedia :: Sound/Audio :: Capture/Recording',
|
||||
],
|
||||
ext_modules=[Extension('alsaaudio',['alsaaudio.c'],
|
||||
ext_modules=[Extension('alsaaudio',['alsaaudio.c'],
|
||||
libraries=['asound'])]
|
||||
)
|
||||
|
||||
37
test.py
37
test.py
@@ -11,6 +11,7 @@
|
||||
import unittest
|
||||
import alsaaudio
|
||||
import warnings
|
||||
from contextlib import closing
|
||||
|
||||
# we can't test read and write well - these are tested otherwise
|
||||
PCMMethods = [
|
||||
@@ -20,7 +21,7 @@ PCMMethods = [
|
||||
]
|
||||
|
||||
PCMDeprecatedMethods = [
|
||||
('setchannels', (2,)),
|
||||
('setchannels', (2,)),
|
||||
('setrate', (44100,)),
|
||||
('setformat', (alsaaudio.PCM_FORMAT_S8,)),
|
||||
('setperiodsize', (320,))
|
||||
@@ -49,10 +50,10 @@ class MixerTest(unittest.TestCase):
|
||||
|
||||
def testMixer(self):
|
||||
"""Open the default Mixers and the Mixers on every card"""
|
||||
|
||||
|
||||
for c in alsaaudio.card_indexes():
|
||||
mixers = alsaaudio.mixers(cardindex=c)
|
||||
|
||||
|
||||
for m in mixers:
|
||||
mixer = alsaaudio.Mixer(m, cardindex=c)
|
||||
mixer.close()
|
||||
@@ -73,7 +74,7 @@ class MixerTest(unittest.TestCase):
|
||||
mixer.close()
|
||||
|
||||
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"""
|
||||
|
||||
mixers = alsaaudio.mixers()
|
||||
@@ -133,7 +134,7 @@ class PCMTest(unittest.TestCase):
|
||||
pcm = alsaaudio.PCM(card='default')
|
||||
except alsaaudio.ALSAAudioError:
|
||||
pass
|
||||
|
||||
|
||||
# Verify we got a DepreciationWarning
|
||||
self.assertEqual(len(w), 1, "PCM(card='default') expected a warning" )
|
||||
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.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__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user