switch to PyAudio package for portability

This commit is contained in:
Roman Zeyde
2014-12-29 17:53:23 +02:00
parent fbd34844cf
commit 4d75dba0bc
11 changed files with 93 additions and 125 deletions

View File

@@ -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

View File

@@ -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__':

View File

@@ -0,0 +1 @@
from . import send, recv, config, audio

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -1,3 +1,4 @@
numpy
bitarray
argcomplete
pyaudio

View File

@@ -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",

View File

@@ -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)
]