From feb9924f546e8cee3539b06b1b602a2796cd5ed4 Mon Sep 17 00:00:00 2001 From: Pedro Date: Wed, 10 Jan 2024 19:16:18 +0000 Subject: [PATCH] Add sounddevice support --- README.rst | 6 +++- amodem/__main__.py | 10 +++++-- amodem/sd.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 4 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 amodem/sd.py diff --git a/README.rst b/README.rst index 6207d27..b4db676 100644 --- a/README.rst +++ b/README.rst @@ -85,11 +85,15 @@ For validation, run:: optional arguments: -h, --help show this help message and exit +`python-sounddevice ` is used by default as the audio library. +This should work out-of-the box on Linux, Windows and macOS, however, other choices are available. + On, Windows you may download the `portaudio` library from `MinGW `_. Then, you should specify the DLL using the following command-line flag:: -l AUDIO_LIBRARY, --audio-library AUDIO_LIBRARY - File name of PortAudio shared library. + +The AUDIO_LIBRARY can be 'alsa', 'sd' (the default) or the file name for the PortAudio shared library. Calibration diff --git a/amodem/__main__.py b/amodem/__main__.py index 2243c4b..3ac77bf 100644 --- a/amodem/__main__.py +++ b/amodem/__main__.py @@ -169,8 +169,9 @@ def create_parser(description, interface_factory): for sub in subparsers.choices.values(): sub.add_argument('-c', '--calibrate', nargs='?', default=False, metavar='SYSTEM', help=calibration_help) - sub.add_argument('-l', '--audio-library', default='libportaudio.so', - help='File name of PortAudio shared library.') + sub.add_argument('-l', '--audio-library', default='sd', + help="'alsa', 'sd' or file name of " + "PortAudio shared library") sub.add_argument('-z', '--zlib', default=False, action='store_true', help='Use zlib to compress/decompress data.') g = sub.add_mutually_exclusive_group() @@ -234,9 +235,12 @@ def interface_factory(): import pylab # pylint: disable=import-error,import-outside-toplevel args.pylab = pylab - if args.audio_library == 'ALSA': + if args.audio_library == 'alsa': from . import alsa # pylint: disable=import-outside-toplevel interface = alsa.Interface(config) + elif args.audio_library == 'sd': + from . import sd # pylint: disable=import-outside-toplevel + interface = sd.Interface(config) elif args.audio_library == '-': interface = _Dummy() # manually disable PortAudio elif args.command == 'send' and args.output is not None: diff --git a/amodem/sd.py b/amodem/sd.py new file mode 100644 index 0000000..490039f --- /dev/null +++ b/amodem/sd.py @@ -0,0 +1,69 @@ +""" +Code which adds sounddevice lib support for interfaces, recording and playing. +""" + +import logging +import sounddevice as sd + +log = logging.getLogger(__name__) + + +class Interface: + + def __init__(self, config): + self.config = config + bits_per_sample = config.bits_per_sample + assert bits_per_sample == 16 + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def recorder(self): + return Recorder(self) + + def player(self): + return Player(self) + + +class Recorder: + def __init__(self, lib): + self.bufsize = 4096 + + self.audio_stream = sd.RawInputStream( + samplerate=lib.config.Fs, + blocksize=self.bufsize, + channels=1, dtype='int16') + self.audio_stream.start() + + def read(self, size): + data, overflowed = self.audio_stream.read(size) + if overflowed: + raise OverflowError("Overflow reading from audio device") + return data + + def close(self): + self.audio_stream.stop() + self.audio_stream.close() + + +class Player: + def __init__(self, lib): + self.buffer_length_ms = 10 + self.buffer_size = int(lib.config.Fs * (self.buffer_length_ms / 1000)) + + self.audio_stream = sd.RawOutputStream( + samplerate=lib.config.Fs, + blocksize=self.buffer_size, + channels=1, dtype='int16') + + self.audio_stream.start() + + def write(self, data): + self.audio_stream.write(data) + + def close(self): + self.audio_stream.stop() + self.audio_stream.close() diff --git a/setup.py b/setup.py index 4b9cf05..0b7400d 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def run_tests(self): packages=['amodem'], tests_require=['pytest'], cmdclass={'test': PyTest}, - install_requires=['numpy'], + install_requires=['numpy','sounddevice'], platforms=['POSIX'], classifiers=[ 'Development Status :: 5 - Production/Stable',