mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e637e701df | ||
|
|
5be6684fa6 | ||
|
|
0876be18e4 | ||
|
|
3b8f913fcb | ||
|
|
45d4ccae76 | ||
|
|
7cb05aaaf7 | ||
|
|
3911f16bd7 | ||
|
|
b5f8e07ae2 | ||
|
|
835841bf2e | ||
|
|
c70b3c9dc7 | ||
|
|
544dd28ddd | ||
|
|
c887dbf4e6 | ||
|
|
bf6282127c | ||
|
|
65f2559a19 | ||
|
|
1c6f8894a5 | ||
|
|
c19d11744f |
@@ -6,9 +6,11 @@ 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:
|
||||
|
||||
13
README.rst
13
README.rst
@@ -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/
|
||||
@@ -55,7 +58,7 @@ 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).
|
||||
@@ -142,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
|
||||
-----
|
||||
@@ -226,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
|
||||
-------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -39,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():
|
||||
@@ -48,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():
|
||||
@@ -57,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():
|
||||
@@ -94,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
|
||||
@@ -146,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()
|
||||
|
||||
@@ -1 +1 @@
|
||||
'1.12.0'
|
||||
'1.13'
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
numpy
|
||||
six
|
||||
argcomplete
|
||||
3
setup.py
3
setup.py
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user