Compare commits

...

27 Commits
v1.11 ... v1.13

Author SHA1 Message Date
Roman Zeyde
e637e701df bump version 2015-09-19 12:13:19 +03:00
Roman Zeyde
5be6684fa6 travis: upgrade pytest version for Python 3.5 2015-09-19 12:05:58 +03:00
Roman Zeyde
0876be18e4 Merge branch 'py35' 2015-09-19 12:02:24 +03:00
Roman Zeyde
3b8f913fcb setup: Python 3.5 is supported 2015-09-19 12:01:52 +03:00
Roman Zeyde
45d4ccae76 use --assert=plain on Travis 2015-09-19 11:58:30 +03:00
Roman Zeyde
7cb05aaaf7 travis: test under Python 3.5 2015-09-19 11:45:50 +03:00
Roman Zeyde
3911f16bd7 tox: fix indent to tabs 2015-09-18 21:07:43 +03:00
Roman Zeyde
b5f8e07ae2 recv: define integration gain as member variable 2015-08-16 18:30:30 +03:00
Roman Zeyde
835841bf2e test_calib: test frequency change case 2015-08-16 18:21:40 +03:00
Roman Zeyde
c70b3c9dc7 calib: remove AttributeHolder 2015-08-16 17:55:40 +03:00
Roman Zeyde
544dd28ddd common: add docstrings 2015-08-16 17:55:40 +03:00
Roman Zeyde
c887dbf4e6 README: use screencasts instead of videos 2015-08-15 08:35:22 +03:00
Roman Zeyde
bf6282127c docs: link to readthedocs.org 2015-08-14 13:07:04 +03:00
Roman Zeyde
65f2559a19 README: fix whitespace 2015-08-13 10:07:07 +03:00
Roman Zeyde
1c6f8894a5 setup.py: fix pytest invocation 2015-08-11 10:43:28 +03:00
Roman Zeyde
c19d11744f remove requirements.txt 2015-08-11 10:39:00 +03:00
Roman Zeyde
4d60cac7ed version: meta-bump due to PyPI problems 2015-08-07 17:44:45 +03:00
Roman Zeyde
a3adda625b gitignore: ignore backup files 2015-07-29 18:03:56 +03:00
Roman Zeyde
a44f55a608 travis: fix tests runner 2015-07-29 17:59:09 +03:00
Roman Zeyde
e0a38bf5d7 README: simplify description 2015-07-29 17:44:47 +03:00
Roman Zeyde
6b67721374 tox: update to run on updated tests 2015-07-29 17:44:46 +03:00
Roman Zeyde
20efa6a688 tests: remove unused code 2015-07-29 17:44:46 +03:00
Roman Zeyde
327a7f9d0f tests: move into amodem package 2015-07-29 17:44:46 +03:00
Roman Zeyde
7b0ba1714f Merge pull request #18 from anduck/patch-1
changed input and output vice versa
2015-07-28 08:58:01 +03:00
anduck
ec76a1394c changed input and output vice versa 2015-07-27 22:45:34 +03:00
Roman Zeyde
bf7b59db11 bump version 2015-07-17 08:27:30 +03:00
Roman Zeyde
f51cf8c4db config: shorten start & stop silence periods 2015-07-17 08:22:49 +03:00
25 changed files with 94 additions and 91 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*~
\#*\#
*.out
*.pyc
*.sublime-*

View File

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

View File

@@ -8,8 +8,11 @@ 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
: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/
@@ -26,7 +29,7 @@ Audio Modem Communication Library
.. 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'
@@ -34,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).
@@ -139,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
-----
@@ -152,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::
@@ -223,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
-------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
'1.11'
'1.13'

View File

@@ -1,3 +0,0 @@
numpy
six
argcomplete

View File

@@ -22,7 +22,7 @@ class PyTest(TestCommand):
def run_tests(self):
import sys
import pytest
sys.exit(pytest.main(['tests']))
sys.exit(pytest.main(['.']))
setup(
name='amodem',
@@ -48,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',

View File

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