mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e637e701df | ||
|
|
5be6684fa6 | ||
|
|
0876be18e4 | ||
|
|
3b8f913fcb | ||
|
|
45d4ccae76 | ||
|
|
7cb05aaaf7 | ||
|
|
3911f16bd7 | ||
|
|
b5f8e07ae2 | ||
|
|
835841bf2e | ||
|
|
c70b3c9dc7 | ||
|
|
544dd28ddd | ||
|
|
c887dbf4e6 | ||
|
|
bf6282127c | ||
|
|
65f2559a19 | ||
|
|
1c6f8894a5 | ||
|
|
c19d11744f | ||
|
|
4d60cac7ed | ||
|
|
a3adda625b | ||
|
|
a44f55a608 | ||
|
|
e0a38bf5d7 | ||
|
|
6b67721374 | ||
|
|
20efa6a688 | ||
|
|
327a7f9d0f | ||
|
|
7b0ba1714f | ||
|
|
ec76a1394c | ||
|
|
bf7b59db11 | ||
|
|
f51cf8c4db | ||
|
|
aec1648ae7 | ||
|
|
2ad3ffced4 | ||
|
|
318081fca4 | ||
|
|
f82a4f4a39 | ||
|
|
8d72621b9b | ||
|
|
2e6196416b | ||
|
|
01c78bae8f | ||
|
|
c56f696e9e | ||
|
|
3fe515ea59 | ||
|
|
55e7152da6 | ||
|
|
66c639b597 | ||
|
|
a4ebf68223 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
*~
|
||||
\#*\#
|
||||
*.out
|
||||
*.pyc
|
||||
*.sublime-*
|
||||
|
||||
@@ -6,15 +6,17 @@ python:
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
|
||||
install:
|
||||
- pip install .
|
||||
- pip install pytest>=2.7.3 --upgrade
|
||||
- pip install coveralls pep8 mock
|
||||
|
||||
script:
|
||||
- pep8 amodem/ scripts/ tests/
|
||||
- pep8 amodem/ scripts/
|
||||
- echo "Hello World!" | amodem send -vv -l- -o- | amodem recv -vv -l- -i-
|
||||
- coverage run --source=amodem --omit="*/__main__.py" -m py.test -vvs tests/
|
||||
- coverage run --source=amodem --omit="*/__main__.py" -m py.test -vvs
|
||||
|
||||
after_success:
|
||||
- coverage report
|
||||
|
||||
49
README.rst
49
README.rst
@@ -8,20 +8,28 @@ Audio Modem Communication Library
|
||||
:target: https://coveralls.io/r/romanz/amodem?branch=master
|
||||
:alt: Code Coverage
|
||||
.. image:: https://landscape.io/github/romanz/amodem/master/landscape.svg?style=flat
|
||||
:target: https://landscape.io/github/romanz/amodem/master
|
||||
:alt: Code Health
|
||||
.. image:: https://pypip.in/py_versions/amodem/badge.svg?style=flat
|
||||
:target: https://landscape.io/github/romanz/amodem/master
|
||||
:alt: Code Health
|
||||
.. image:: https://readthedocs.org/projects/amodem/badge/?version=latest
|
||||
:target: http://amodem.readthedocs.org/en/latest/
|
||||
:alt: Documentation
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Python Versions
|
||||
.. image:: https://pypip.in/license/amodem/badge.svg?style=flat
|
||||
.. image:: https://img.shields.io/pypi/l/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: License
|
||||
.. image:: https://pypip.in/version/amodem/badge.svg?style=flat
|
||||
.. image:: https://img.shields.io/pypi/v/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Package Version
|
||||
.. image:: https://pypip.in/status/amodem/badge.svg?style=flat
|
||||
.. image:: https://img.shields.io/pypi/status/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Development Status
|
||||
.. image:: https://img.shields.io/pypi/dm/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://badge.waffle.io/romanz/amodem.svg?label=ready&title=ready
|
||||
:target: https://waffle.io/romanz/amodem
|
||||
:alt: 'Ready'
|
||||
@@ -29,25 +37,28 @@ Audio Modem Communication Library
|
||||
Description
|
||||
-----------
|
||||
|
||||
This program can be used to transmit a specified file between 2 computers, using
|
||||
a simple audio cable (for better SNR and higher speeds) or a simple headset,
|
||||
allowing true air-gapped communication (via a speaker and a microphone).
|
||||
This program can transmit a file between 2 computers, using a simple headset,
|
||||
allowing true air-gapped communication (via a speaker and a microphone),
|
||||
or an audio cable (for higher transmission speed).
|
||||
|
||||
The sender modulates an input binary data file into an audio signal,
|
||||
The sender modulates the input data into an audio signal,
|
||||
which is played to the sound card.
|
||||
|
||||
The receiver side records the transmitted audio,
|
||||
which is demodulated concurrently into an output binary data file.
|
||||
The receiver records the audio, and demodulates it back to the original data.
|
||||
|
||||
The process requires a single manual calibration step: the transmitter has to
|
||||
find maximal output volume for its sound card, which will not saturate the
|
||||
receiving microphone.
|
||||
find the optimal output volume for its sound card, which will not saturate the
|
||||
receiving microphone and provide good enough Signal-to-Noise ratio
|
||||
for the demodulation to succeed.
|
||||
|
||||
Technical Details
|
||||
-----------------
|
||||
|
||||
The modem is using OFDM over an audio cable with the following parameters:
|
||||
|
||||
- Sampling rate: 8/16/32 kHz
|
||||
- Baud rate: 1 kHz
|
||||
- Symbol modulation: BPSK, 4-PSK, 16-QAM ,64-QAM, 256-QAM
|
||||
- Symbol modulation: BPSK, 4-PSK, 16-QAM, 64-QAM, 256-QAM
|
||||
- Carriers: 2-11 kHz (up to ten carriers)
|
||||
|
||||
This way, modem may achieve 80kbps bitrate = 10 kB/s (for best SNR).
|
||||
@@ -134,7 +145,7 @@ and send me the resulting ``audio.raw`` file for debugging::
|
||||
|
||||
~/receiver $ arecord --format=S16_LE --channels=1 --rate=32000 audio.raw
|
||||
|
||||
You can see a video of the `calibration process <http://www.youtube.com/watch?v=jRUj2Ifk-Po>`_.
|
||||
You can see a screencast of the `calibration process <https://asciinema.org/a/25065?autoplay=1>`_.
|
||||
|
||||
Usage
|
||||
-----
|
||||
@@ -147,11 +158,11 @@ Prepare the sender (generate a random binary data file to be sent)::
|
||||
|
||||
Start the receiver (will wait for the sender to start)::
|
||||
|
||||
~/receiver $ amodem recv -vv -i data.rx
|
||||
~/receiver $ amodem recv -vv -o data.rx
|
||||
|
||||
Start the sender (will modulate the data and start the transmission)::
|
||||
|
||||
~/sender $ amodem send -vv -o data.tx
|
||||
~/sender $ amodem send -vv -i data.tx
|
||||
|
||||
A similar log should be emitted by the sender::
|
||||
|
||||
@@ -218,7 +229,7 @@ After the receiver has finished, verify the received file's hash::
|
||||
~/receiver $ sha256sum data.rx
|
||||
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.rx
|
||||
|
||||
You can see a video of the `data transfer process <http://www.youtube.com/watch?v=GZQUtHB8so4>`_.
|
||||
You can see a screencast of the `data transfer process <https://asciinema.org/a/25066?autoplay=1>`_.
|
||||
|
||||
Visualization
|
||||
-------------
|
||||
|
||||
60
amodem/__main__.py
Executable file → Normal file
60
amodem/__main__.py
Executable file → Normal file
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
from amodem import main, calib, audio, async
|
||||
from amodem.config import bitrates
|
||||
from . import main, calib, audio, async
|
||||
from .config import bitrates
|
||||
from . import version
|
||||
|
||||
import os
|
||||
import sys
|
||||
@@ -28,6 +29,7 @@ config = bitrates.get(int(bitrate))
|
||||
class Compressor(object):
|
||||
def __init__(self, stream):
|
||||
self.obj = zlib.compressobj()
|
||||
log.info('Using zlib compressor')
|
||||
self.stream = stream
|
||||
|
||||
def read(self, size):
|
||||
@@ -48,6 +50,7 @@ class Compressor(object):
|
||||
class Decompressor(object):
|
||||
def __init__(self, stream):
|
||||
self.obj = zlib.decompressobj()
|
||||
log.info('Using zlib decompressor')
|
||||
self.stream = stream
|
||||
|
||||
def write(self, data):
|
||||
@@ -57,8 +60,10 @@ class Decompressor(object):
|
||||
self.stream.write(self.obj.flush())
|
||||
|
||||
|
||||
def FileType(mode, audio_interface=None):
|
||||
def FileType(mode, interface_factory=None):
|
||||
def opener(fname):
|
||||
audio_interface = interface_factory() if interface_factory else None
|
||||
|
||||
assert 'r' in mode or 'w' in mode
|
||||
if audio_interface is None and fname is None:
|
||||
fname = '-'
|
||||
@@ -94,20 +99,11 @@ def get_volume_cmd(args):
|
||||
return c[args.command]
|
||||
|
||||
|
||||
class _DummyContextManager(object):
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
def wrap(cls, stream, enable):
|
||||
return cls(stream) if enable else stream
|
||||
|
||||
|
||||
def create_parser(description, interface):
|
||||
def create_parser(description, interface_factory):
|
||||
p = argparse.ArgumentParser(description=description)
|
||||
subparsers = p.add_subparsers()
|
||||
|
||||
@@ -132,7 +128,7 @@ def create_parser(description, interface):
|
||||
volume_cmd=get_volume_cmd(args)
|
||||
),
|
||||
input_type=FileType('rb'),
|
||||
output_type=FileType('wb', interface),
|
||||
output_type=FileType('wb', interface_factory),
|
||||
command='send'
|
||||
)
|
||||
|
||||
@@ -159,7 +155,7 @@ def create_parser(description, interface):
|
||||
config=config, src=args.src, verbose=args.verbose,
|
||||
volume_cmd=get_volume_cmd(args)
|
||||
),
|
||||
input_type=FileType('rb', interface),
|
||||
input_type=FileType('rb', interface_factory),
|
||||
output_type=FileType('wb'),
|
||||
command='recv'
|
||||
)
|
||||
@@ -184,13 +180,27 @@ def create_parser(description, interface):
|
||||
return p
|
||||
|
||||
|
||||
class _Dummy(object):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
def _main():
|
||||
fmt = ('Audio OFDM MODEM: {0:.1f} kb/s ({1:d}-QAM x {2:d} carriers) '
|
||||
'Fs={3:.1f} kHz')
|
||||
description = fmt.format(config.modem_bps / 1e3, len(config.symbols),
|
||||
fmt = ('Audio OFDM MODEM v{0:s}: '
|
||||
'{1:.1f} kb/s ({2:d}-QAM x {3:d} carriers) '
|
||||
'Fs={4:.1f} kHz')
|
||||
description = fmt.format(version.__doc__,
|
||||
config.modem_bps / 1e3, len(config.symbols),
|
||||
config.Nfreq, config.Fs / 1e3)
|
||||
interface = audio.Interface(config=config)
|
||||
p = create_parser(description, interface)
|
||||
interface = None
|
||||
|
||||
def interface_factory():
|
||||
return interface
|
||||
|
||||
p = create_parser(description, interface_factory)
|
||||
|
||||
args = p.parse_args()
|
||||
if args.verbose == 0:
|
||||
@@ -213,9 +223,13 @@ def _main():
|
||||
import pylab # pylint: disable=import-error
|
||||
args.pylab = pylab
|
||||
|
||||
if args.audio_library == '-':
|
||||
interface = _DummyContextManager()
|
||||
if args.audio_library == 'ALSA':
|
||||
from . import alsa
|
||||
interface = alsa.Interface(config)
|
||||
elif args.audio_library == '-':
|
||||
interface = _Dummy()
|
||||
else:
|
||||
interface = audio.Interface(config)
|
||||
interface.load(args.audio_library)
|
||||
|
||||
with interface:
|
||||
@@ -227,9 +241,9 @@ def _main():
|
||||
else:
|
||||
args.calib(config=config, args=args)
|
||||
finally:
|
||||
log.debug('Closing input and output')
|
||||
args.src.close()
|
||||
args.dst.close()
|
||||
log.debug('Finished I/O')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
65
amodem/alsa.py
Normal file
65
amodem/alsa.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Interface(object):
|
||||
|
||||
RECORDER = 'arecord'
|
||||
PLAYER = 'aplay'
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
rate = int(config.Fs)
|
||||
bits_per_sample = config.bits_per_sample
|
||||
assert bits_per_sample == 16
|
||||
|
||||
args = '-f S{0:d}_LE -c 1 -r {1:d} -T 100 -q -'
|
||||
args = args.format(bits_per_sample, rate).split()
|
||||
|
||||
self.record_cmd = [self.RECORDER] + args
|
||||
self.play_cmd = [self.PLAYER] + args
|
||||
self.processes = []
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
for p in self.processes:
|
||||
try:
|
||||
p.wait()
|
||||
except OSError:
|
||||
log.warning('%s failed', p)
|
||||
|
||||
def launch(self, **kwargs):
|
||||
log.debug('Launching subprocess: %s', kwargs)
|
||||
p = subprocess.Popen(**kwargs)
|
||||
self.processes.append(p)
|
||||
return p
|
||||
|
||||
def recorder(self):
|
||||
return Recorder(self)
|
||||
|
||||
def player(self):
|
||||
return Player(self)
|
||||
|
||||
|
||||
class Recorder(object):
|
||||
def __init__(self, lib):
|
||||
self.p = lib.launch(args=lib.record_cmd, stdout=subprocess.PIPE)
|
||||
self.read = self.p.stdout.read
|
||||
self.bufsize = 4096
|
||||
|
||||
def close(self):
|
||||
self.p.kill()
|
||||
|
||||
|
||||
class Player(object):
|
||||
def __init__(self, lib):
|
||||
self.p = lib.launch(args=lib.play_cmd, stdin=subprocess.PIPE)
|
||||
self.write = self.p.stdin.write
|
||||
|
||||
def close(self):
|
||||
self.p.stdin.close()
|
||||
self.p.wait()
|
||||
@@ -72,10 +72,10 @@ def detector(config, src, frame_length=200):
|
||||
else:
|
||||
msg = 'too {0} signal'.format(errors[flags.index(False)])
|
||||
|
||||
yield common.AttributeHolder(dict(
|
||||
yield dict(
|
||||
freq=freq, rms=rms, peak=peak, coherency=coherency,
|
||||
total=total, success=success, msg=msg
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
def volume_calibration(result_iterator, volume_ctl):
|
||||
@@ -90,7 +90,7 @@ def volume_calibration(result_iterator, volume_ctl):
|
||||
for index, result in enumerate(itertools.chain([None], result_iterator)):
|
||||
if index % iters_per_update == 0:
|
||||
if index > 0: # skip dummy (first result)
|
||||
sign = 1 if (result.total < target_level) else -1
|
||||
sign = 1 if (result['total'] < target_level) else -1
|
||||
level = level + step * sign
|
||||
level = min(max(level, min_level), max_level)
|
||||
step = step * 0.5
|
||||
@@ -111,12 +111,7 @@ def iter_window(iterable, size):
|
||||
yield block
|
||||
|
||||
|
||||
def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
|
||||
fmt = '{0.freq:6.0f} Hz: {0.msg:20s}'
|
||||
if verbose:
|
||||
fields = ['total', 'rms', 'coherency', 'peak']
|
||||
fmt += ', '.join('{0}={{0.{0}:.4f}}'.format(f) for f in fields)
|
||||
|
||||
def recv_iter(config, src, volume_cmd=None, dump_audio=None):
|
||||
volume_ctl = volume_controller(volume_cmd)
|
||||
|
||||
if dump_audio:
|
||||
@@ -125,6 +120,19 @@ def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
|
||||
result_iterator = volume_calibration(result_iterator, volume_ctl)
|
||||
for _prev, curr, _next in iter_window(result_iterator, size=3):
|
||||
# don't log errors during frequency changes
|
||||
if _prev.success and _next.success and _prev.freq != _next.freq:
|
||||
curr.msg = curr.msg if curr.success else 'frequency change'
|
||||
log.info(fmt.format(curr))
|
||||
if _prev['success'] and _next['success']:
|
||||
if _prev['freq'] != _next['freq']:
|
||||
if not curr['success']:
|
||||
curr['msg'] = 'frequency change'
|
||||
yield curr
|
||||
|
||||
|
||||
def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
|
||||
fmt = '{freq:6.0f} Hz: {msg:20s}'
|
||||
log.info('verbose: %s', verbose)
|
||||
if verbose:
|
||||
fields = ['total', 'rms', 'coherency', 'peak']
|
||||
fmt += ', '.join('{0}={{{0}:.4f}}'.format(f) for f in fields)
|
||||
|
||||
for state in recv_iter(config, src, volume_cmd, dump_audio):
|
||||
log.info(fmt.format(**state))
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
''' Common package functionality.
|
||||
'''
|
||||
|
||||
import itertools
|
||||
import numpy as np
|
||||
|
||||
@@ -8,21 +11,25 @@ scaling = 32000.0 # out of 2**15
|
||||
|
||||
|
||||
def load(fileobj):
|
||||
''' Load signal from file object. '''
|
||||
return loads(fileobj.read())
|
||||
|
||||
|
||||
def loads(data):
|
||||
''' Load signal from memory buffer. '''
|
||||
x = np.frombuffer(data, dtype='int16')
|
||||
x = x / scaling
|
||||
return x
|
||||
|
||||
|
||||
def dumps(sym):
|
||||
''' Dump signal to memory buffer. '''
|
||||
sym = sym.real * scaling
|
||||
return sym.astype('int16').tostring()
|
||||
|
||||
|
||||
def iterate(data, size, func=None, truncate=True, index=False):
|
||||
''' Iterate over a signal, taking each time *size* elements. '''
|
||||
offset = 0
|
||||
data = iter(data)
|
||||
|
||||
@@ -40,6 +47,9 @@ def iterate(data, size, func=None, truncate=True, index=False):
|
||||
|
||||
|
||||
def split(iterable, n):
|
||||
''' Split an iterable of n-tuples into n iterables of scalars.
|
||||
The k-th iterable will be equivalent to (i[k] for i in iter).
|
||||
'''
|
||||
def _gen(it, index):
|
||||
for item in it:
|
||||
yield item[index]
|
||||
@@ -49,37 +59,29 @@ def split(iterable, n):
|
||||
|
||||
|
||||
def icapture(iterable, result):
|
||||
''' Appends each yielded item to result. '''
|
||||
for i in iter(iterable):
|
||||
result.append(i)
|
||||
yield i
|
||||
|
||||
|
||||
def take(iterable, n):
|
||||
''' Take n elements from iterable, and return them as a numpy array. '''
|
||||
return np.array(list(itertools.islice(iterable, n)))
|
||||
|
||||
|
||||
# "Python 3" zip re-implementation for Python 2
|
||||
def izip(iterables):
|
||||
''' "Python 3" zip re-implementation for Python 2. '''
|
||||
iterables = [iter(iterable) for iterable in iterables]
|
||||
while True:
|
||||
yield tuple([next(iterable) for iterable in iterables])
|
||||
|
||||
|
||||
class Dummy(object):
|
||||
''' Dummy placeholder object for testing and mocking. '''
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
|
||||
class AttributeHolder(object):
|
||||
|
||||
def __init__(self, d):
|
||||
self.__dict__.update(d)
|
||||
|
||||
def __repr__(self):
|
||||
items = sorted(self.__dict__.items())
|
||||
args = ', '.join('{0}={1}'.format(k, v) for k, v in items)
|
||||
return '{0}({1})'.format(self.__class__.__name__, args)
|
||||
|
||||
@@ -12,8 +12,8 @@ class Configuration(object):
|
||||
latency = 0.1
|
||||
|
||||
# sender config
|
||||
silence_start = 1.0
|
||||
silence_stop = 1.0
|
||||
silence_start = 0.25
|
||||
silence_stop = 0.25
|
||||
|
||||
# receiver config
|
||||
skip_start = 0.1
|
||||
|
||||
@@ -28,6 +28,7 @@ class Receiver(object):
|
||||
self.equalizer = equalizer.Equalizer(config)
|
||||
self.carrier_index = config.carrier_index
|
||||
self.output_size = 0 # number of bytes written to output stream
|
||||
self.freq_err_gain = 0.01 * self.Tsym # integration feedback gain
|
||||
|
||||
def _prefix(self, symbols, gain=1.0):
|
||||
S = common.take(symbols, len(equalizer.prefix))
|
||||
@@ -140,7 +141,7 @@ class Receiver(object):
|
||||
err = np.mean(np.angle(err))/(2*np.pi) if len(err) else 0
|
||||
errors.clear()
|
||||
|
||||
sampler.freq -= 0.01 * err * self.Tsym
|
||||
sampler.freq -= self.freq_err_gain * err
|
||||
sampler.offset -= err
|
||||
|
||||
def _report_progress(self, noise, sampler):
|
||||
|
||||
40
amodem/tests/test_alsa.py
Normal file
40
amodem/tests/test_alsa.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from amodem import alsa, config
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
def test_alsa():
|
||||
interface = alsa.Interface(config=config.fastest())
|
||||
interface.launch = mock.Mock()
|
||||
with interface:
|
||||
r = interface.recorder()
|
||||
r.read(2)
|
||||
r.close()
|
||||
|
||||
p = mock.call(
|
||||
args='arecord -f S16_LE -c 1 -r 32000 -T 100 -q -'.split(),
|
||||
stdout=-1)
|
||||
assert interface.launch.mock_calls == [p, p.stdout.read(2), p.kill()]
|
||||
|
||||
interface.launch = mock.Mock()
|
||||
with interface:
|
||||
p = interface.player()
|
||||
p.write('\x00\x00')
|
||||
p.close()
|
||||
|
||||
p = mock.call(
|
||||
args='aplay -f S16_LE -c 1 -r 32000 -T 100 -q -'.split(),
|
||||
stdin=-1)
|
||||
assert interface.launch.mock_calls == [
|
||||
p, p.stdin.write('\x00\x00'), p.stdin.close(), p.wait()
|
||||
]
|
||||
|
||||
|
||||
def test_alsa_subprocess():
|
||||
interface = alsa.Interface(config=config.fastest())
|
||||
with mock.patch('subprocess.Popen') as popen:
|
||||
with interface:
|
||||
p = interface.launch(args=['foobar'])
|
||||
p.wait.side_effect = OSError('invalid command')
|
||||
assert interface.processes == [p]
|
||||
assert popen.mock_calls == [mock.call(args=['foobar'])]
|
||||
@@ -19,14 +19,6 @@ class ProcessMock(object):
|
||||
self.stdout = self
|
||||
self.bytes_per_sample = 2
|
||||
|
||||
def launch(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
__call__ = launch
|
||||
|
||||
def kill(self):
|
||||
pass
|
||||
|
||||
def write(self, data):
|
||||
assert self.buf.tell() < 10e6
|
||||
self.buf.write(data)
|
||||
@@ -47,8 +39,8 @@ def test_too_strong():
|
||||
calib.send(config, p, gain=1.001, limit=32)
|
||||
p.buf.seek(0)
|
||||
for r in calib.detector(config, src=p):
|
||||
assert not r.success
|
||||
assert r.msg == 'too strong signal'
|
||||
assert not r['success']
|
||||
assert r['msg'] == 'too strong signal'
|
||||
|
||||
|
||||
def test_too_weak():
|
||||
@@ -56,8 +48,8 @@ def test_too_weak():
|
||||
calib.send(config, p, gain=0.01, limit=32)
|
||||
p.buf.seek(0)
|
||||
for r in calib.detector(config, src=p):
|
||||
assert not r.success
|
||||
assert r.msg == 'too weak signal'
|
||||
assert not r['success']
|
||||
assert r['msg'] == 'too weak signal'
|
||||
|
||||
|
||||
def test_too_noisy():
|
||||
@@ -65,8 +57,8 @@ def test_too_noisy():
|
||||
signal = np.array([r.choice([-1, 1]) for i in range(int(config.Fs))])
|
||||
src = BytesIO(common.dumps(signal * 0.5))
|
||||
for r in calib.detector(config, src=src):
|
||||
assert not r.success
|
||||
assert r.msg == 'too noisy signal'
|
||||
assert not r['success']
|
||||
assert r['msg'] == 'too noisy signal'
|
||||
|
||||
|
||||
def test_errors():
|
||||
@@ -102,9 +94,9 @@ def test_drift(freq_err):
|
||||
src = BytesIO(common.dumps(signal))
|
||||
iters = 0
|
||||
for r in calib.detector(config, src, frame_length=frame_length):
|
||||
assert r.success is True
|
||||
assert abs(r.rms - rms) < 1e-3
|
||||
assert abs(r.total - rms) < 1e-3
|
||||
assert r['success'] is True
|
||||
assert abs(r['rms'] - rms) < 1e-3
|
||||
assert abs(r['total'] - rms) < 1e-3
|
||||
iters += 1
|
||||
|
||||
assert iters > 0
|
||||
@@ -154,3 +146,15 @@ def test_recv_binary_search():
|
||||
fmt = 'ctl {0:.0f}%'
|
||||
expected = [mock.call(shell=True, args=fmt.format(100 * g)) for g in gains]
|
||||
assert check_call.mock_calls == expected
|
||||
|
||||
|
||||
def test_recv_freq_change():
|
||||
p = ProcessMock()
|
||||
calib.send(config, p, gain=0.5, limit=2)
|
||||
offset = p.buf.tell() // 16
|
||||
p.buf.seek(offset)
|
||||
messages = [state['msg'] for state in calib.recv_iter(config, p)]
|
||||
assert messages == [
|
||||
'good signal', 'good signal', 'good signal',
|
||||
'frequency change',
|
||||
'good signal', 'good signal', 'good signal']
|
||||
@@ -54,14 +54,6 @@ def test_izip():
|
||||
assert list(common.izip([x, y])) == list(zip(x, y))
|
||||
|
||||
|
||||
def test_holder():
|
||||
d = {'x': 1, 'y': 2.3}
|
||||
a = common.AttributeHolder(d)
|
||||
assert a.x == d['x']
|
||||
assert a.y == d['y']
|
||||
assert repr(a) == 'AttributeHolder(x=1, y=2.3)'
|
||||
|
||||
|
||||
def test_configs():
|
||||
default = config.Configuration()
|
||||
fastest = config.fastest()
|
||||
@@ -29,7 +29,6 @@ def test_read():
|
||||
j += 1
|
||||
|
||||
try:
|
||||
for buf in f:
|
||||
pass
|
||||
next(f)
|
||||
except IOError as e:
|
||||
assert e.args == ('timeout',)
|
||||
@@ -14,15 +14,7 @@ logging.basicConfig(level=logging.DEBUG, # useful for debugging
|
||||
format='%(asctime)s %(levelname)-12s %(message)s')
|
||||
|
||||
|
||||
class Args(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return None
|
||||
|
||||
|
||||
def run(size, chan=None, df=0, success=True, reader=None, cfg=None):
|
||||
def run(size, chan=None, df=0, success=True, cfg=None):
|
||||
if cfg is None:
|
||||
cfg = config.fastest()
|
||||
tx_data = os.urandom(size)
|
||||
@@ -43,8 +35,6 @@ def run(size, chan=None, df=0, success=True, reader=None, cfg=None):
|
||||
rx_data = BytesIO()
|
||||
dump = BytesIO()
|
||||
|
||||
if reader:
|
||||
rx_audio = reader(rx_audio)
|
||||
try:
|
||||
result = main.recv(config=cfg, src=rx_audio, dst=rx_data,
|
||||
dump_audio=dump, pylab=None)
|
||||
1
amodem/version.py
Normal file
1
amodem/version.py
Normal file
@@ -0,0 +1 @@
|
||||
'1.13'
|
||||
@@ -1,3 +0,0 @@
|
||||
numpy
|
||||
six
|
||||
argcomplete
|
||||
15
setup.py
15
setup.py
@@ -2,6 +2,16 @@
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
import os
|
||||
import ast
|
||||
|
||||
def parse_vesrion():
|
||||
cwd = os.path.dirname(__name__)
|
||||
version_file = os.path.join(cwd, 'amodem', 'version.py')
|
||||
|
||||
tree = ast.parse(open(version_file).read())
|
||||
expr, = tree.body
|
||||
return expr.value.s
|
||||
|
||||
class PyTest(TestCommand):
|
||||
|
||||
@@ -12,11 +22,11 @@ class PyTest(TestCommand):
|
||||
def run_tests(self):
|
||||
import sys
|
||||
import pytest
|
||||
sys.exit(pytest.main(['tests']))
|
||||
sys.exit(pytest.main(['.']))
|
||||
|
||||
setup(
|
||||
name='amodem',
|
||||
version='1.10',
|
||||
version=parse_vesrion(),
|
||||
description='Audio Modem Communication Library',
|
||||
author='Roman Zeyde',
|
||||
author_email='roman.zeyde@gmail.com',
|
||||
@@ -38,6 +48,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.2',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: Communications',
|
||||
|
||||
6
tox.ini
6
tox.ini
@@ -7,9 +7,9 @@ deps=
|
||||
pep8
|
||||
coverage
|
||||
pylint
|
||||
six
|
||||
six
|
||||
commands=
|
||||
pep8 amodem/ scripts/ tests/
|
||||
pep8 amodem/ scripts/
|
||||
pylint --extension-pkg-whitelist=numpy --report=no amodem --rcfile .pylintrc
|
||||
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v tests/
|
||||
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v
|
||||
coverage report
|
||||
|
||||
Reference in New Issue
Block a user