From 4d75dba0bcbf316b2198ba5f5b7396810b241c27 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 29 Dec 2014 17:53:23 +0200 Subject: [PATCH] switch to PyAudio package for portability --- .travis.yml | 3 +-- amodem-cli | 61 +++++++++++---------------------------------- amodem/__init__.py | 1 + amodem/audio.py | 52 ++++++++++++++++++++------------------ amodem/calib.py | 30 +++++++--------------- amodem/config.py | 5 ++++ amodem/recv.py | 8 +++--- amodem/send.py | 2 +- requirements.txt | 1 + setup.py | 2 +- tests/test_audio.py | 53 ++++++++++++++++++++------------------- 11 files changed, 93 insertions(+), 125 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2bfebb7..50213a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,7 @@ python: install: - pip install . - - pip install coveralls - - pip install pep8 + - pip install coveralls pep8 mock script: - pep8 amodem/ scripts/ tests/ amodem-cli diff --git a/amodem-cli b/amodem-cli index 9751712..e7e39ac 100755 --- a/amodem-cli +++ b/amodem-cli @@ -19,7 +19,7 @@ import logging log = logging.getLogger('__name__') -from amodem import recv, send, audio, calib +from amodem import recv, send, calib, audio from amodem.config import bitrates null = open('/dev/null', 'wb') @@ -29,18 +29,18 @@ bitrate = os.environ.get('BITRATE', 1) config = bitrates.get(int(bitrate)) -def FileType(mode, process=None): +def FileType(mode, audio_interface=None): def opener(fname): assert 'r' in mode or 'w' in mode - if process is None and fname is None: + if audio_interface is None and fname is None: fname = '-' if fname is None: - assert process is not None + assert audio_interface is not None if 'r' in mode: - return process.launch(stdout=audio.sp.PIPE, stderr=null).stdout + return audio_interface.recorder() if 'w' in mode: - return process.launch(stdin=audio.sp.PIPE, stderr=null).stdin + return audio_interface.player() if fname == '-': if 'r' in mode: @@ -58,6 +58,7 @@ def main(): 'Fs={3:.1f} kHz') description = fmt.format(config.modem_bps / 1e3, len(config.symbols), config.Nfreq, config.Fs / 1e3) + interface = audio.Interface(config) p = argparse.ArgumentParser(description=description) subparsers = p.add_subparsers() @@ -71,13 +72,11 @@ def main(): ' if not specified, `aplay` tool will be used.') sender.add_argument( '-c', '--calibrate', default=False, action='store_true') - sender.add_argument( - '-w', '--wave', default=False, action='store_true') sender.set_defaults( - main=run_send, + main=send.main, calibration=calib.send, input_type=FileType('rb'), - output_type=FileType('wb', audio.play(Fs=config.Fs)) + output_type=FileType('wb', interface) ) # Demodulator @@ -90,14 +89,12 @@ def main(): '-o', '--output', help='output file (use "-" for stdout).') receiver.add_argument( '-c', '--calibrate', default=False, action='store_true') - receiver.add_argument( - '-w', '--wave', default=False, action='store_true') receiver.add_argument( '--plot', action='store_true', default=False, help='plot results using pylab module') receiver.set_defaults( - main=run_recv, - input_type=FileType('rb', audio.record(Fs=config.Fs)), + main=recv.main, calibration=calib.recv, + input_type=FileType('rb', interface), output_type=FileType('wb') ) @@ -125,43 +122,15 @@ def main(): log.debug(description) if getattr(args, 'plot', False): import pylab - args.plot = pylab - args.config = config - args.main(args) + else: + pylab = None - -def join_process(process): - exitcode = 0 - try: - exitcode = process.wait() - except KeyboardInterrupt: - process.kill() - exitcode = process.wait() - sys.exit(exitcode) - - -def run_modem(args, main_func, **kwargs): src = args.input_type(args.input) dst = args.output_type(args.output) - main_func(args.config, src, dst, **kwargs) - - -def run_send(args): if args.calibrate: - calib.send(config=config, verbose=args.verbose) - elif args.wave: - join_process(audio.play(Fs=config.Fs).launch(fname=args.input)) + args.calibration(config=config, src=src, dst=dst, verbose=args.verbose) else: - run_modem(args, send.main) - - -def run_recv(args): - if args.calibrate: - calib.recv(config=config, verbose=args.verbose) - elif args.wave: - join_process(audio.record(Fs=config.Fs).launch(fname=args.output)) - else: - run_modem(args, recv.main, plt=args.plot) + args.main(config=config, src=src, dst=dst, pylab=pylab) if __name__ == '__main__': diff --git a/amodem/__init__.py b/amodem/__init__.py index e69de29..e4159d0 100644 --- a/amodem/__init__.py +++ b/amodem/__init__.py @@ -0,0 +1 @@ +from . import send, recv, config, audio diff --git a/amodem/audio.py b/amodem/audio.py index e5ebb71..1f4f460 100644 --- a/amodem/audio.py +++ b/amodem/audio.py @@ -1,31 +1,35 @@ -import subprocess as sp +import pyaudio import logging -import functools log = logging.getLogger(__name__) -class ALSA(object): - def __init__(self, tool, Fs): - self.Fs = int(Fs) # sampling rate - self.bits_per_sample = 16 - self.bytes_per_sample = self.bits_per_sample / 8.0 - self.bytes_per_second = self.bytes_per_sample * self.Fs - # PCM signed little endian - self.audio_format = 'S{0}_LE'.format(self.bits_per_sample) - self.audio_tool = tool +class Interface(object): + def __init__(self, config, library=pyaudio): + self.p = library.PyAudio() + format = getattr(library, 'paInt{0}'.format(config.bits_per_sample)) + self.sample_size = config.sample_size + self.kwargs = dict( + channels=1, rate=int(config.Fs), format=format, + frames_per_buffer=config.samples_per_buffer + ) - def launch(self, fname=None, **kwargs): - if fname is None: - fname = '-' # use stdin/stdout if filename not specified - args = [self.audio_tool, fname, '-q', - '-f', self.audio_format, - '-c', '1', - '-r', str(self.Fs)] - log.debug('Running: %r', ' '.join(args)) - p = sp.Popen(args=args, **kwargs) - return p + def __del__(self): + self.p.terminate() -# Use ALSA tools for audio playing/recording -play = functools.partial(ALSA, tool='aplay') -record = functools.partial(ALSA, tool='arecord') + def player(self): + return self.p.open(output=True, **self.kwargs) + + def recorder(self): + stream = self.p.open(input=True, **self.kwargs) + return _Recorder(stream, sample_size=self.sample_size) + + +class _Recorder(object): + def __init__(self, stream, sample_size): + self.stream = stream + self.sample_size = sample_size + + def read(self, size): + assert size % self.sample_size == 0 + return self.stream.read(size // self.sample_size) diff --git a/amodem/calib.py b/amodem/calib.py index b58e4ab..632ab8e 100644 --- a/amodem/calib.py +++ b/amodem/calib.py @@ -3,49 +3,40 @@ import logging log = logging.getLogger(__name__) -from subprocess import PIPE from . import common -from . import audio ALLOWED_EXCEPTIONS = (IOError, KeyboardInterrupt) -def send(config, audio_play=audio.play, verbose=False): +def send(config, dst, src=None, verbose=False): calibration_symbols = int(1.0 * config.Fs) t = np.arange(0, calibration_symbols) * config.Ts signal = [np.sin(2 * np.pi * f * t) for f in config.frequencies] signal = common.dumps(np.concatenate(signal)) - player = audio_play(Fs=config.Fs) - p = player.launch(stdin=PIPE) - fd = p.stdin try: while True: - fd.write(signal) + dst.write(signal) except ALLOWED_EXCEPTIONS: pass - finally: - p.kill() def run_recorder(config, recorder): - FRAME_LENGTH = 200 * config.Nsym - process = recorder.launch(stdout=PIPE) - frame_size = int(recorder.bytes_per_sample * FRAME_LENGTH) + frame_length = 200 * config.Nsym + frame_size = frame_length * config.sample_size + + t = np.arange(0, frame_length) * config.Ts - t = np.arange(0, FRAME_LENGTH) * config.Ts scaling_factor = 0.5 * len(t) carriers = [np.exp(2j * np.pi * f * t) for f in config.frequencies] carriers = np.array(carriers) / scaling_factor - fd = process.stdout - states = [True] errors = ['weak', 'strong', 'noisy'] try: while True: - data = fd.read(frame_size) + data = recorder.read(frame_size) if len(data) < frame_size: return data = common.loads(data) @@ -76,20 +67,17 @@ def run_recorder(config, recorder): ) except ALLOWED_EXCEPTIONS: pass - finally: - process.kill() fmt = '{freq:6.0f} Hz: {message:s}{extra:s}' fields = ['peak', 'total', 'rms', 'coherency'] -def recv(config, audio_record=audio.record, verbose=False): +def recv(config, src, dst=None, verbose=False): extra = '' if verbose: extra = ''.join(', {0}={{{0}:.4f}}'.format(f) for f in fields) - recorder = audio_record(Fs=config.Fs) - for result in run_recorder(config=config, recorder=recorder): + for result in run_recorder(config=config, recorder=src): msg = fmt.format(extra=extra.format(**result), **result) if not result['error']: log.info(msg) diff --git a/amodem/config.py b/amodem/config.py index 03f246e..9efbd2b 100644 --- a/amodem/config.py +++ b/amodem/config.py @@ -8,6 +8,11 @@ class Configuration(object): Npoints = 64 F0 = 1e3 + # audio config + bits_per_sample = 16 + sample_size = bits_per_sample // 8 + samples_per_buffer = 1024 + # sender config silence_start = 1.0 silence_stop = 1.0 diff --git a/amodem/recv.py b/amodem/recv.py index 857824a..8cec3a2 100644 --- a/amodem/recv.py +++ b/amodem/recv.py @@ -18,9 +18,9 @@ from . import detect class Receiver(object): - def __init__(self, config, plt=None): + def __init__(self, config, pylab=None): self.stats = {} - self.plt = plt or common.Dummy() + self.plt = pylab or common.Dummy() self.modem = dsp.MODEM(config.symbols) self.frequencies = np.array(config.frequencies) self.omegas = 2 * np.pi * self.frequencies / config.Fs @@ -209,7 +209,7 @@ class Receiver(object): self.plt.title(title) -def main(config, src, dst, plt=None): +def main(config, src, dst, pylab=None): reader = stream.Reader(src, data_type=common.loads) signal = itertools.chain.from_iterable(reader) @@ -220,7 +220,7 @@ def main(config, src, dst, plt=None): reader.check = common.check_saturation detector = detect.Detector(config=config) - receiver = Receiver(config=config, plt=plt) + receiver = Receiver(config=config, pylab=pylab) success = False try: log.info('Waiting for carrier tone: %.1f kHz', config.Fc / 1e3) diff --git a/amodem/send.py b/amodem/send.py index 9b935db..69ff555 100644 --- a/amodem/send.py +++ b/amodem/send.py @@ -52,7 +52,7 @@ class Sender(object): log.debug('Sent %8.1f kB', total_bits / 8e3) -def main(config, src, dst): +def main(config, src, dst, pylab=None): sender = Sender(dst, config=config) Fs = config.Fs diff --git a/requirements.txt b/requirements.txt index b9dad37..eead0b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ numpy bitarray argcomplete +pyaudio diff --git a/setup.py b/setup.py index b0d2d11..a1c6b99 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( packages=['amodem'], tests_require=['pytest'], cmdclass={'test': PyTest}, - install_requires=['numpy', 'bitarray', 'argcomplete'], + install_requires=['numpy', 'bitarray', 'argcomplete', 'pyaudio'], platforms=['POSIX'], classifiers=[ "Development Status :: 4 - Beta", diff --git a/tests/test_audio.py b/tests/test_audio.py index 5142dc1..089f5d9 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -1,32 +1,33 @@ from amodem import audio -import subprocess as sp -import signal +from amodem import config + +import mock -def test_launch(): - p = audio.ALSA(tool='true', Fs=32000).launch() - assert p.wait() == 0 +def test_pyaudio_mock(): + m = mock.Mock() + m.paInt16 = 8 + m.PyAudio.return_value = m + m.open.return_value = m + cfg = config.fastest() + interface = audio.Interface(config=cfg, library=m) + recorder = interface.recorder() + n = 1024 + data = recorder.read(n) -def test_exit(): - p = audio.ALSA(tool='python', Fs=32000).launch(fname='-', stdin=sp.PIPE) - s = b'import sys; sys.exit(42)' - p.stdin.write(s) - p.stdin.close() - assert p.wait() == 42 + data = '\x00' * n + player = interface.player() + player.write(data) - -def test_io(): - p = audio.ALSA(tool='python', Fs=32000) - p = p.launch(fname='-', stdin=sp.PIPE, stdout=sp.PIPE) - s = b'Hello World!' - p.stdin.write(b'print("' + s + b'")\n') - p.stdin.close() - assert p.stdout.read(len(s)) == s - - -def test_kill(): - p = audio.ALSA(tool='python', Fs=32000) - p = p.launch(fname='-', stdin=sp.PIPE, stdout=sp.PIPE) - p.kill() - assert p.wait() == -signal.SIGKILL + kwargs = dict( + channels=1, frames_per_buffer=cfg.samples_per_buffer, + rate=cfg.Fs, format=m.paInt16 + ) + assert m.mock_calls == [ + mock.call.PyAudio(), + mock.call.open(input=True, **kwargs), + mock.call.read(n // cfg.sample_size), + mock.call.open(output=True, **kwargs), + mock.call.write(data) + ]