12 Commits

Author SHA1 Message Date
Lars Immisch
62e5515341 Document the release process. 2020-07-13 22:00:44 +02:00
Lars Immisch
ed027a6141 More output for playwav 2020-07-13 20:42:25 +01:00
Lars Immisch
5302dc524d Cleanup warnings 2020-07-13 20:59:49 +02:00
Lars Immisch
b17b36be50 Better error messages in tests 2020-07-13 20:51:59 +02:00
Lars Immisch
08bdce9ed9 Tests for Depreciations 2020-07-13 20:20:28 +02:00
Lars Immisch
0224c8a308 Inline documentation (and .gitignore) 2020-07-10 00:54:24 +02:00
Lars Immisch
f07627543c Update documentation 2020-07-10 00:45:57 +02:00
Lars Immisch
df889b94ef Don't use setrate etc. in samples. 2020-07-09 21:22:06 +02:00
Lars Immisch
2a21bf6c42 Support all essential parameters in alsapcm_new. 2020-07-08 22:39:46 +02:00
Lars Immisch
8084297926 Merge pull request #83 from stalkerg/master
fix generate switch capabilities
2020-05-25 12:58:03 +02:00
stalkerg
8fbc04e18d fix generate switch capabilities 2020-05-21 17:21:40 +09:00
Lars Immisch
8ed9f924cd Attempt to fix #45 2020-04-23 21:36:29 +01:00
10 changed files with 2151 additions and 2101 deletions

4
.gitignore vendored
View File

@@ -4,6 +4,8 @@ MANIFEST
doc/gh-pages/ doc/gh-pages/
doc/html/ doc/html/
doc/doctrees/ doc/doctrees/
doc/_build/
gh-pages/ gh-pages/
build/ build/
dist/ dist/
.vscode/

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,22 @@
# Make a new release
Update the version in setup.py
pyalsa_version = '0.9.0'
Commit and push the update.
Create and push a tag naming the version (i.e. 0.9.0):
git tag 0.9.0
git push origin 0.9.0
Upload the package:
python3 setup.py sdist
Don't forget to update the documentation.
# Publish the documentation # Publish the documentation
The documentation is published through the `gh-pages` branch. The documentation is published through the `gh-pages` branch.

View File

@@ -63,7 +63,6 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
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.
.. 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:
@@ -108,7 +107,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, device='default', cardindex=-1) .. class:: PCM(type=PCM_PLAYBACK, mode=PCM_NORMAL, rate=44100, channels=2, format=PCM_FORMAT_S16_LE, periodsize=32, 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:
@@ -117,75 +116,14 @@ following arguments:
(default). (default).
* *mode* - can be either :const:`PCM_NONBLOCK`, or :const:`PCM_NORMAL` * *mode* - can be either :const:`PCM_NONBLOCK`, or :const:`PCM_NORMAL`
(default). (default).
* *device* - the name of the PCM device that should be used (for example * *rate* - the sampling rate in Hz. Typical values are ``8000``
a value from the output of :func:`pcms`). The default value is (mainly used for telephony), ``16000``, ``44100`` (default), ``48000`` and ``96000``.
``'default'``. * *channels* - the number of channels. The default value is 2 (stereo).
* *cardindex* - the card index. If this argument is given, the device name * *format* - the data format. This controls how the PCM device interprets data for playback, and how data is encoded in captures.
is constructed as 'hw:*cardindex*' and The default value is :const:`PCM_FORMAT_S16_LE`.
the `device` keyword argument is ignored.
``0`` is the first hardware sound card.
This will construct a PCM object with these default settings:
* Sample format: :const:`PCM_FORMAT_S16_LE`
* Rate: 44100 Hz
* Channels: 2
* Period size: 32 frames
*Changed in 0.8:*
- The `card` keyword argument is still supported,
but deprecated. Please use `device` instead.
- The keyword argument `cardindex` was added.
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.pcmtype()
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.
.. method:: PCM.setchannels(nchannels)
Used to set the number of capture or playback channels. Common
values are: ``1`` = mono, ``2`` = stereo, and ``6`` = full 6 channel audio.
Few sound cards support more than 2 channels
.. method:: PCM.setrate(rate)
Set the sample rate in Hz for the device. Typical values are ``8000``
(mainly used for telephony), ``16000``, ``44100`` (CD quality),
``48000`` and ``96000``.
.. method:: PCM.setformat(format)
The sound *format* of the device. Sound format controls how the PCM device
interpret data for playback, and how data is encoded in captures.
The following formats are provided by ALSA:
========================= =============== ========================= ===============
Format Description Format Description
========================= =============== ========================= ===============
``PCM_FORMAT_S8`` Signed 8 bit samples for each channel ``PCM_FORMAT_S8`` Signed 8 bit samples for each channel
``PCM_FORMAT_U8`` Signed 8 bit samples for each channel ``PCM_FORMAT_U8`` Signed 8 bit samples for each channel
@@ -215,15 +153,66 @@ PCM objects have the following methods:
``PCM_FORMAT_U24_3LE`` Unsigned 24 bit samples for each channel (Little Endian byte order in 3 bytes) ``PCM_FORMAT_U24_3LE`` Unsigned 24 bit samples for each channel (Little Endian byte order in 3 bytes)
``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.
* *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'``.
* *cardindex* - the card index. If this argument is given, the device name
is constructed as 'hw:*cardindex*' and
the `device` keyword argument is ignored.
``0`` is the first hardware sound card.
This will construct a PCM object with the given settings.
*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.
- The keyword argument `cardindex` was added.
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.pcmtype()
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.
.. method:: PCM.setchannels(nchannels)
.. deprecated:: 0.9 Use the `channels` named argument to :func:`PCM`.
.. method:: PCM.setrate(rate)
.. 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) .. method:: PCM.setperiodsize(period)
Sets the actual period size in frames. Each write should consist of .. deprecated:: 0.9 Use the `periodsize` named argument to :func:`PCM`.
exactly this number of frames, and each read will return this
number of frames (unless the device is in :const:`PCM_NONBLOCK` mode, in
which case it may return nothing at all)
.. method:: PCM.read() .. method:: PCM.read()

View File

@@ -56,10 +56,7 @@ class SinePlayer(Thread):
def __init__(self, frequency = 440.0): def __init__(self, frequency = 440.0):
Thread.__init__(self) Thread.__init__(self)
self.setDaemon(True) self.setDaemon(True)
self.device = alsaaudio.PCM() self.device = alsaaudio.PCM(channels=channels, format=format, rate=sampling_rate)
self.device.setchannels(channels)
self.device.setformat(format)
self.device.setrate(sampling_rate)
self.queue = Queue() self.queue = Queue()
self.change(frequency) self.change(frequency)

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
## playbacktest.py ## playbacktest.py
## ##
@@ -38,18 +39,11 @@ if __name__ == '__main__':
f = open(args[0], 'rb') f = open(args[0], 'rb')
# Open the device in playback mode. # Open the device in playback mode in Mono, 44100 Hz, 16 bit little endian frames
out = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, device=device)
# Set attributes: Mono, 44100 Hz, 16 bit little endian frames
out.setchannels(1)
out.setrate(44100)
out.setformat(alsaaudio.PCM_FORMAT_S16_LE)
# The period size controls the internal number of frames per period. # The period size controls the internal number of frames per period.
# The significance of this parameter is documented in the ALSA api. # The significance of this parameter is documented in the ALSA api.
out.setperiodsize(160)
out = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, channels=1, rate=44100, format=alsaaudio.PCM_FORMAT_S16_LE, periodsize=160, device=device)
# Read data from stdin # Read data from stdin
data = f.read(320) data = f.read(320)
while data: while data:

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
# Simple test script that plays (some) wav files # Simple test script that plays (some) wav files
@@ -9,57 +10,54 @@ import wave
import getopt import getopt
import alsaaudio import alsaaudio
def play(device, f): def play(device, f):
print('%d channels, %d sampling rate\n' % (f.getnchannels(), format = None
f.getframerate()))
# Set attributes
device.setchannels(f.getnchannels())
device.setrate(f.getframerate())
# 8bit is unsigned in wav files # 8bit is unsigned in wav files
if f.getsampwidth() == 1: if f.getsampwidth() == 1:
device.setformat(alsaaudio.PCM_FORMAT_U8) format = alsaaudio.PCM_FORMAT_U8
# Otherwise we assume signed data, little endian # Otherwise we assume signed data, little endian
elif f.getsampwidth() == 2: elif f.getsampwidth() == 2:
device.setformat(alsaaudio.PCM_FORMAT_S16_LE) format = alsaaudio.PCM_FORMAT_S16_LE
elif f.getsampwidth() == 3: elif f.getsampwidth() == 3:
device.setformat(alsaaudio.PCM_FORMAT_S24_3LE) format = alsaaudio.PCM_FORMAT_S24_3LE
elif f.getsampwidth() == 4: elif f.getsampwidth() == 4:
device.setformat(alsaaudio.PCM_FORMAT_S32_LE) format = alsaaudio.PCM_FORMAT_S32_LE
else: else:
raise ValueError('Unsupported format') raise ValueError('Unsupported format')
periodsize = f.getframerate() // 8 periodsize = f.getframerate() // 8
device.setperiodsize(periodsize) print('%d channels, %d sampling rate, format %d, periodsize %d\n' % (f.getnchannels(),
f.getframerate(),
data = f.readframes(periodsize) format,
while data: periodsize))
# Read data from stdin
device.write(data) device = alsaaudio.PCM(channels=f.getnchannels(), rate=f.getframerate(), format=format, periodsize=periodsize, device=device)
data = f.readframes(periodsize)
data = f.readframes(periodsize)
while data:
# Read data from stdin
device.write(data)
data = f.readframes(periodsize)
def usage(): def usage():
print('usage: playwav.py [-d <device>] <file>', file=sys.stderr) print('usage: playwav.py [-d <device>] <file>', file=sys.stderr)
sys.exit(2) sys.exit(2)
if __name__ == '__main__': if __name__ == '__main__':
device = 'default' device = 'default'
opts, args = getopt.getopt(sys.argv[1:], 'd:') opts, args = getopt.getopt(sys.argv[1:], 'd:')
for o, a in opts: for o, a in opts:
if o == '-d': if o == '-d':
device = a device = a
if not args: if not args:
usage() usage()
f = wave.open(args[0], 'rb') with wave.open(args[0], 'rb') as f:
device = alsaaudio.PCM(device=device) play(device, f)
play(device, f)
f.close()

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
## recordtest.py ## recordtest.py
## ##
@@ -22,48 +23,42 @@ import getopt
import alsaaudio import alsaaudio
def usage(): def usage():
print('usage: recordtest.py [-d <device>] <file>', file=sys.stderr) print('usage: recordtest.py [-d <device>] <file>', file=sys.stderr)
sys.exit(2) sys.exit(2)
if __name__ == '__main__': if __name__ == '__main__':
device = 'default' device = 'default'
opts, args = getopt.getopt(sys.argv[1:], 'd:') opts, args = getopt.getopt(sys.argv[1:], 'd:')
for o, a in opts: for o, a in opts:
if o == '-d': if o == '-d':
device = a device = a
if not args: if not args:
usage() usage()
f = open(args[0], 'wb') f = open(args[0], 'wb')
# Open the device in nonblocking capture mode. The last argument could # Open the device in nonblocking capture mode in mono, with a sampling rate of 44100 Hz
# just as well have been zero for blocking mode. Then we could have # and 16 bit little endian samples
# left out the sleep call in the bottom of the loop # The period size controls the internal number of frames per period.
inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NONBLOCK, device=device) # The significance of this parameter is documented in the ALSA api.
# For our purposes, it is suficcient to know that reads from the device
# will return this many frames. Each frame being 2 bytes long.
# This means that the reads below will return either 320 bytes of data
# or 0 bytes of data. The latter is possible because we are in nonblocking
# mode.
inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NONBLOCK,
channels=1, rate=44100, format=alsaaudio.PCM_FORMAT_S16_LE,
periodsize=160, device=device)
# Set attributes: Mono, 44100 Hz, 16 bit little endian samples loops = 1000000
inp.setchannels(1) while loops > 0:
inp.setrate(44100) loops -= 1
inp.setformat(alsaaudio.PCM_FORMAT_S16_LE) # Read data from device
l, data = inp.read()
# The period size controls the internal number of frames per period.
# The significance of this parameter is documented in the ALSA api. if l:
# For our purposes, it is suficcient to know that reads from the device f.write(data)
# will return this many frames. Each frame being 2 bytes long. time.sleep(.001)
# This means that the reads below will return either 320 bytes of data
# or 0 bytes of data. The latter is possible because we are in nonblocking
# mode.
inp.setperiodsize(160)
loops = 1000000
while loops > 0:
loops -= 1
# Read data from device
l, data = inp.read()
if l:
f.write(data)
time.sleep(.001)

View File

@@ -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.8.6' pyalsa_version = '0.9.0'
if __name__ == '__main__': if __name__ == '__main__':
setup( setup(

212
test.py
View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
# These are internal tests. They shouldn't fail, but they don't cover all # These are internal tests. They shouldn't fail, but they don't cover all
# of the ALSA API. Most importantly PCM.read and PCM.write are missing. # of the ALSA API. Most importantly PCM.read and PCM.write are missing.
@@ -12,125 +13,148 @@ import alsaaudio
import warnings import warnings
# 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 = [('pcmtype', None), PCMMethods = [
('pcmmode', None), ('pcmtype', None),
('cardname', None), ('pcmmode', None),
('setchannels', (2,)), ('cardname', None)
('setrate', (44100,)), ]
('setformat', (alsaaudio.PCM_FORMAT_S8,)),
('setperiodsize', (320,))] PCMDeprecatedMethods = [
('setchannels', (2,)),
('setrate', (44100,)),
('setformat', (alsaaudio.PCM_FORMAT_S8,)),
('setperiodsize', (320,))
]
# A clever test would look at the Mixer capabilities and selectively run the # A clever test would look at the Mixer capabilities and selectively run the
# omitted tests, but I am too tired for that. # omitted tests, but I am too tired for that.
MixerMethods = [('cardname', None), MixerMethods = [('cardname', None),
('mixer', None), ('mixer', None),
('mixerid', None), ('mixerid', None),
('switchcap', None), ('switchcap', None),
('volumecap', None), ('volumecap', None),
('getvolume', None), ('getvolume', None),
('getrange', None), ('getrange', None),
('getenum', None), ('getenum', None),
# ('getmute', None), # ('getmute', None),
# ('getrec', None), # ('getrec', None),
# ('setvolume', (60,)), # ('setvolume', (60,)),
# ('setmute', (0,)) # ('setmute', (0,))
# ('setrec', (0')), # ('setrec', (0')),
] ]
class MixerTest(unittest.TestCase): class MixerTest(unittest.TestCase):
"""Test Mixer objects""" """Test Mixer objects"""
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()
def testMixerAll(self): def testMixerAll(self):
"Run common Mixer methods on an open object" "Run common Mixer methods on an open object"
mixers = alsaaudio.mixers() mixers = alsaaudio.mixers()
mixer = alsaaudio.Mixer(mixers[0]) mixer = alsaaudio.Mixer(mixers[0])
for m, a in MixerMethods: for m, a in MixerMethods:
f = alsaaudio.Mixer.__dict__[m] f = alsaaudio.Mixer.__dict__[m]
if a is None: if a is None:
f(mixer) f(mixer)
else: else:
f(mixer, *a) f(mixer, *a)
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()
mixer = alsaaudio.Mixer(mixers[0]) mixer = alsaaudio.Mixer(mixers[0])
mixer.close() mixer.close()
for m, a in MixerMethods: for m, a in MixerMethods:
f = alsaaudio.Mixer.__dict__[m] f = alsaaudio.Mixer.__dict__[m]
if a is None: if a is None:
self.assertRaises(alsaaudio.ALSAAudioError, f, mixer) self.assertRaises(alsaaudio.ALSAAudioError, f, mixer)
else: else:
self.assertRaises(alsaaudio.ALSAAudioError, f, mixer, *a) self.assertRaises(alsaaudio.ALSAAudioError, f, mixer, *a)
class PCMTest(unittest.TestCase): class PCMTest(unittest.TestCase):
"""Test PCM objects""" """Test PCM objects"""
def testPCM(self): def testPCM(self):
"Open a PCM object on every card" "Open a PCM object on every card"
for c in alsaaudio.card_indexes(): for c in alsaaudio.card_indexes():
pcm = alsaaudio.PCM(cardindex=c) pcm = alsaaudio.PCM(cardindex=c)
pcm.close() pcm.close()
def testPCMAll(self): def testPCMAll(self):
"Run all PCM methods on an open object" "Run all PCM methods on an open object"
pcm = alsaaudio.PCM() pcm = alsaaudio.PCM()
for m, a in PCMMethods: for m, a in PCMMethods:
f = alsaaudio.PCM.__dict__[m] f = alsaaudio.PCM.__dict__[m]
if a is None: if a is None:
f(pcm) f(pcm)
else: else:
f(pcm, *a) f(pcm, *a)
pcm.close() pcm.close()
def testPCMClose(self): def testPCMClose(self):
"Run all PCM methods on a closed object and verify it raises an error" "Run all PCM methods on a closed object and verify it raises an error"
pcm = alsaaudio.PCM() pcm = alsaaudio.PCM()
pcm.close() pcm.close()
for m, a in PCMMethods: for m, a in PCMMethods:
f = alsaaudio.PCM.__dict__[m] f = alsaaudio.PCM.__dict__[m]
if a is None: if a is None:
self.assertRaises(alsaaudio.ALSAAudioError, f, pcm) self.assertRaises(alsaaudio.ALSAAudioError, f, pcm)
else: else:
self.assertRaises(alsaaudio.ALSAAudioError, f, pcm, *a) self.assertRaises(alsaaudio.ALSAAudioError, f, pcm, *a)
def testPCMDeprecated(self): def testPCMDeprecated(self):
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
try:
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")
for m, a in PCMDeprecatedMethods:
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
pcm = alsaaudio.PCM()
f = alsaaudio.PCM.__dict__[m]
if a is None:
f(pcm)
else:
f(pcm, *a)
# Verify we got a DepreciationWarning
method = "%s%s" % (m, str(a))
self.assertEqual(len(w), 1, method + " expected a warning")
self.assertTrue(issubclass(w[-1].category, DeprecationWarning), method + " expected a DeprecationWarning")
try:
pcm = alsaaudio.PCM(card='default')
except alsaaudio.ALSAAudioError:
pass
# Verify we got a DepreciationWarning
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()