mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
50 Commits
v1.13.1
...
fix-divisi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3284447d1c | ||
|
|
8557faba6e | ||
|
|
d8059b5ff0 | ||
|
|
250713a853 | ||
|
|
f4473145c3 | ||
|
|
bb68779d75 | ||
|
|
2452ed56e8 | ||
|
|
55b446e362 | ||
|
|
a99fb6d083 | ||
|
|
0c5ba9480b | ||
|
|
034a2c9ebe | ||
|
|
8553f74f88 | ||
|
|
ffeda35d49 | ||
|
|
883ecb4334 | ||
|
|
96b87ec5be | ||
|
|
ceffe7fac0 | ||
|
|
e8cf356248 | ||
|
|
ee5c543737 | ||
|
|
1311c58005 | ||
|
|
9c6ef2884e | ||
|
|
21fe42e68d | ||
|
|
9352088389 | ||
|
|
db47cda390 | ||
|
|
4cd3def507 | ||
|
|
9192a8ff67 | ||
|
|
3b88a23dfb | ||
|
|
10c06f7646 | ||
|
|
df9cbfdf13 | ||
|
|
02f45de4f1 | ||
|
|
6d6bd44dd8 | ||
|
|
4598cfc7f6 | ||
|
|
ff6c9968e8 | ||
|
|
0bd691320e | ||
|
|
4527eaa931 | ||
|
|
1dd59f9f8f | ||
|
|
c3cefee85f | ||
|
|
2ab3c62d0d | ||
|
|
601a14297f | ||
|
|
71dab0e7bc | ||
|
|
53df2d5934 | ||
|
|
ac2e66bddd | ||
|
|
0534b6891a | ||
|
|
4f0bc6883b | ||
|
|
4846cdaf8f | ||
|
|
0e171a58f2 | ||
|
|
20f90edf8d | ||
|
|
72397e541c | ||
|
|
6d17cc132f | ||
|
|
9dbb3826c7 | ||
|
|
c991b2264e |
@@ -1,7 +1,6 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 1.13.1
|
||||
current_version = 1.15.4
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
|
||||
36
.github/workflows/build.yml
vendored
Normal file
36
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-cov mock pycodestyle coverage pylint
|
||||
pip install -e .
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
pycodestyle amodem/ scripts/
|
||||
pylint --extension-pkg-whitelist=numpy --reports=no amodem --rcfile .pylintrc
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest -v --cov=amodem
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,4 +24,6 @@ htmlcov
|
||||
*_ext.so
|
||||
/.tox
|
||||
/dist
|
||||
/deb_dist
|
||||
*.html
|
||||
.pytest_cache/
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[MESSAGES CONTROL]
|
||||
disable=invalid-name, missing-docstring, too-many-instance-attributes, too-few-public-methods, logging-format-interpolation
|
||||
disable=invalid-name, missing-docstring, too-many-instance-attributes, too-few-public-methods, logging-format-interpolation, consider-using-with
|
||||
|
||||
22
.travis.yml
22
.travis.yml
@@ -1,22 +0,0 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
|
||||
install:
|
||||
- pip install .
|
||||
- pip install pytest>=2.7.3 --upgrade
|
||||
- pip install coveralls pycodestyle mock
|
||||
|
||||
script:
|
||||
- pycodestyle 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
|
||||
|
||||
after_success:
|
||||
- coverage report
|
||||
- coveralls
|
||||
45
README.rst
45
README.rst
@@ -1,16 +1,6 @@
|
||||
Audio Modem Communication Library
|
||||
=================================
|
||||
|
||||
.. image:: https://travis-ci.org/romanz/amodem.svg?branch=master
|
||||
:target: https://travis-ci.org/romanz/amodem
|
||||
:alt: Build Status
|
||||
.. image:: https://coveralls.io/repos/romanz/amodem/badge.svg?branch=master
|
||||
: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://img.shields.io/pypi/pyversions/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Python Versions
|
||||
@@ -24,9 +14,6 @@ Audio Modem Communication Library
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Development Status
|
||||
|
||||
.. image:: https://badge.waffle.io/romanz/amodem.svg?label=ready&title=ready
|
||||
:target: https://waffle.io/romanz/amodem
|
||||
:alt: 'Ready'
|
||||
|
||||
Description
|
||||
-----------
|
||||
@@ -45,6 +32,8 @@ 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.
|
||||
|
||||
HackerNews discussion: https://news.ycombinator.com/item?id=17333257
|
||||
|
||||
Technical Details
|
||||
-----------------
|
||||
|
||||
@@ -64,9 +53,9 @@ on each 250 byte data frame.
|
||||
Installation
|
||||
------------
|
||||
|
||||
Make sure that ``numpy`` and ``PortAudio v19`` packages are installed (on Debian)::
|
||||
Make sure that all the required packages are installed (on Debian)::
|
||||
|
||||
$ sudo apt-get install python-numpy portaudio19-dev
|
||||
$ sudo apt-get install python-numpy python-pip portaudio19-dev git
|
||||
|
||||
Get the latest released version from PyPI::
|
||||
|
||||
@@ -96,6 +85,12 @@ For validation, run::
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
On, Windows you may download the `portaudio` library from `MinGW <https://packages.msys2.org/base/mingw-w64-portaudio>`_.
|
||||
Then, you should specify the DLL using the following command-line flag::
|
||||
|
||||
-l AUDIO_LIBRARY, --audio-library AUDIO_LIBRARY
|
||||
File name of PortAudio shared library.
|
||||
|
||||
|
||||
Calibration
|
||||
-----------
|
||||
@@ -225,6 +220,26 @@ After the receiver has finished, verify the received file's hash::
|
||||
|
||||
You can see a screencast of the `data transfer process <https://asciinema.org/a/25066?autoplay=1>`_.
|
||||
|
||||
I/O redirection
|
||||
---------------
|
||||
The audio can be written/read to an intermediate PCM file (instead of the speaker/microphone) using::
|
||||
|
||||
$ echo 123 | amodem send -o /tmp/file.pcm
|
||||
Sending 0.800 seconds of training audio
|
||||
Starting modulation
|
||||
Sent 0.004 kB @ 0.113 seconds
|
||||
|
||||
$ amodem recv -i /tmp/file.pcm
|
||||
Waiting for carrier tone: 2.0 kHz
|
||||
Carrier detected at ~150.0 ms @ 2.0 kHz
|
||||
Carrier coherence: 100.000%
|
||||
Carrier symbols amplitude : 1.000
|
||||
Frequency error: 0.000 ppm
|
||||
Starting demodulation
|
||||
123
|
||||
Received 0.004 kB @ 0.011 seconds = 0.376 kB/s
|
||||
|
||||
|
||||
Visualization
|
||||
-------------
|
||||
Make sure that ``matplotlib`` package is installed, and run (at the receiver side)::
|
||||
|
||||
@@ -8,18 +8,13 @@ import zlib
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from . import async
|
||||
from . import async_reader
|
||||
from . import audio
|
||||
from . import calib
|
||||
from . import main
|
||||
from .config import bitrates
|
||||
|
||||
|
||||
# Python 3 has `buffer` attribute for byte-based I/O
|
||||
_stdin = getattr(sys.stdin, 'buffer', sys.stdin)
|
||||
_stdout = getattr(sys.stdout, 'buffer', sys.stdout)
|
||||
|
||||
|
||||
try:
|
||||
import argcomplete
|
||||
except ImportError:
|
||||
@@ -31,7 +26,7 @@ bitrate = os.environ.get('BITRATE', 1)
|
||||
config = bitrates.get(int(bitrate))
|
||||
|
||||
|
||||
class Compressor(object):
|
||||
class Compressor:
|
||||
def __init__(self, stream):
|
||||
self.obj = zlib.compressobj()
|
||||
log.info('Using zlib compressor')
|
||||
@@ -52,7 +47,7 @@ class Compressor(object):
|
||||
return result
|
||||
|
||||
|
||||
class Decompressor(object):
|
||||
class Decompressor:
|
||||
def __init__(self, stream):
|
||||
self.obj = zlib.decompressobj()
|
||||
log.info('Using zlib decompressor')
|
||||
@@ -77,27 +72,27 @@ def FileType(mode, interface_factory=None):
|
||||
assert audio_interface is not None
|
||||
if 'r' in mode:
|
||||
s = audio_interface.recorder()
|
||||
return async.AsyncReader(stream=s, bufsize=s.bufsize)
|
||||
return async_reader.AsyncReader(stream=s, bufsize=s.bufsize)
|
||||
if 'w' in mode:
|
||||
return audio_interface.player()
|
||||
|
||||
if fname == '-':
|
||||
if 'r' in mode:
|
||||
return _stdin
|
||||
return sys.stdin.buffer
|
||||
if 'w' in mode:
|
||||
return _stdout
|
||||
return sys.stdout.buffer
|
||||
|
||||
return open(fname, mode)
|
||||
return open(fname, mode) # pylint: disable=unspecified-encoding
|
||||
|
||||
return opener
|
||||
|
||||
|
||||
def get_volume_cmd(args):
|
||||
volume_controllers = [
|
||||
dict(test='pactl --version',
|
||||
send='pactl set-sink-volume @DEFAULT_SINK@',
|
||||
recv='pactl set-source-volume @DEFAULT_SOURCE@')
|
||||
]
|
||||
volume_controllers = [{
|
||||
'test': 'pactl --version',
|
||||
'send': 'pactl set-sink-volume @DEFAULT_SINK@',
|
||||
'recv': 'pactl set-source-volume @DEFAULT_SOURCE@'
|
||||
}]
|
||||
if args.calibrate == 'auto':
|
||||
for c in volume_controllers:
|
||||
if os.system(c['test']) == 0:
|
||||
@@ -111,7 +106,7 @@ def wrap(cls, stream, enable):
|
||||
|
||||
def create_parser(description, interface_factory):
|
||||
p = argparse.ArgumentParser(description=description)
|
||||
subparsers = p.add_subparsers()
|
||||
subparsers = p.add_subparsers(required=True)
|
||||
|
||||
# Modulator
|
||||
sender = subparsers.add_parser(
|
||||
@@ -124,10 +119,13 @@ def create_parser(description, interface_factory):
|
||||
sender.add_argument(
|
||||
'-g', '--gain', type=float, default=1.0,
|
||||
help='Modulator gain (defaults to 1)')
|
||||
sender.add_argument(
|
||||
'--silence', type=float, default=0.0,
|
||||
help='Extra silence before sending the data (in seconds)')
|
||||
sender.set_defaults(
|
||||
main=lambda config, args: main.send(
|
||||
config, src=wrap(Compressor, args.src, args.zlib), dst=args.dst,
|
||||
gain=args.gain
|
||||
gain=args.gain, extra_silence=args.silence
|
||||
),
|
||||
calib=lambda config, args: calib.send(
|
||||
config=config, dst=args.dst,
|
||||
@@ -142,8 +140,7 @@ def create_parser(description, interface_factory):
|
||||
receiver = subparsers.add_parser(
|
||||
'recv', help='demodulate audio signal into binary data.')
|
||||
receiver.add_argument(
|
||||
'-i', '--input', help='input file (use "-" for stdin).'
|
||||
' if not specified, `arecord` tool will be used.')
|
||||
'-i', '--input', help='input file (use "-" for stdin).')
|
||||
receiver.add_argument(
|
||||
'-o', '--output', help='output file (use "-" for stdout).')
|
||||
receiver.add_argument(
|
||||
@@ -186,7 +183,7 @@ def create_parser(description, interface_factory):
|
||||
return p
|
||||
|
||||
|
||||
class _Dummy(object):
|
||||
class _Dummy:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@@ -198,6 +195,20 @@ def _version():
|
||||
return pkg_resources.require('amodem')[0].version
|
||||
|
||||
|
||||
def _config_log(args):
|
||||
if args.verbose == 0:
|
||||
level, fmt = 'INFO', '%(message)s'
|
||||
elif args.verbose == 1:
|
||||
level, fmt = 'DEBUG', '%(message)s'
|
||||
elif args.verbose >= 2:
|
||||
level, fmt = ('DEBUG', '%(asctime)s %(levelname)-10s '
|
||||
'%(message)-100s '
|
||||
'%(filename)s:%(lineno)d')
|
||||
if args.quiet:
|
||||
level, fmt = 'WARNING', '%(message)s'
|
||||
logging.basicConfig(level=level, format=fmt)
|
||||
|
||||
|
||||
def _main():
|
||||
fmt = ('Audio OFDM MODEM v{0:s}: '
|
||||
'{1:.1f} kb/s ({2:d}-QAM x {3:d} carriers) '
|
||||
@@ -213,31 +224,25 @@ def _main():
|
||||
p = create_parser(description, interface_factory)
|
||||
|
||||
args = p.parse_args()
|
||||
if args.verbose == 0:
|
||||
level, fmt = 'INFO', '%(message)s'
|
||||
elif args.verbose == 1:
|
||||
level, fmt = 'DEBUG', '%(message)s'
|
||||
elif args.verbose >= 2:
|
||||
level, fmt = ('DEBUG', '%(asctime)s %(levelname)-10s '
|
||||
'%(message)-100s '
|
||||
'%(filename)s:%(lineno)d')
|
||||
if args.quiet:
|
||||
level, fmt = 'WARNING', '%(message)s'
|
||||
logging.basicConfig(level=level, format=fmt)
|
||||
_config_log(args)
|
||||
|
||||
# Parsing and execution
|
||||
log.debug(description)
|
||||
log.info(description)
|
||||
|
||||
args.pylab = None
|
||||
if getattr(args, 'plot', False):
|
||||
import pylab # pylint: disable=import-error
|
||||
import pylab # pylint: disable=import-error,import-outside-toplevel
|
||||
args.pylab = pylab
|
||||
|
||||
if args.audio_library == 'ALSA':
|
||||
from . import alsa
|
||||
from . import alsa # pylint: disable=import-outside-toplevel
|
||||
interface = alsa.Interface(config)
|
||||
elif args.audio_library == '-':
|
||||
interface = _Dummy()
|
||||
interface = _Dummy() # manually disable PortAudio
|
||||
elif args.command == 'send' and args.output is not None:
|
||||
interface = _Dummy() # redirected output
|
||||
elif args.command == 'recv' and args.input is not None:
|
||||
interface = _Dummy() # redirected input
|
||||
else:
|
||||
interface = audio.Interface(config)
|
||||
interface.load(args.audio_library)
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Interface(object):
|
||||
class Interface:
|
||||
|
||||
RECORDER = 'arecord'
|
||||
PLAYER = 'aplay'
|
||||
@@ -50,7 +50,7 @@ class Interface(object):
|
||||
return Player(self)
|
||||
|
||||
|
||||
class Recorder(object):
|
||||
class Recorder:
|
||||
def __init__(self, lib):
|
||||
self.p = lib.launch(args=lib.record_cmd, stdout=subprocess.PIPE)
|
||||
self.read = self.p.stdout.read
|
||||
@@ -60,7 +60,7 @@ class Recorder(object):
|
||||
self.p.kill()
|
||||
|
||||
|
||||
class Player(object):
|
||||
class Player:
|
||||
def __init__(self, lib):
|
||||
self.p = lib.launch(args=lib.play_cmd, stdin=subprocess.PIPE)
|
||||
self.write = self.p.stdin.write
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import six # since `Queue` module was renamed to `queue` (in Python 3)
|
||||
from queue import Queue
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
class AsyncReader(object):
|
||||
class AsyncReader:
|
||||
def __init__(self, stream, bufsize):
|
||||
self.stream = stream
|
||||
self.queue = six.moves.queue.Queue()
|
||||
self.queue = Queue()
|
||||
self.stop = threading.Event()
|
||||
args = (stream, bufsize, self.queue, self.stop)
|
||||
self.thread = threading.Thread(target=AsyncReader._thread,
|
||||
@@ -29,7 +29,7 @@ class AsyncReader(object):
|
||||
queue.put(buf)
|
||||
total += len(buf)
|
||||
log.debug('AsyncReader thread stopped (read %d bytes)', total)
|
||||
except BaseException:
|
||||
except BaseException: # pylint: disable=broad-except
|
||||
log.exception('AsyncReader thread failed')
|
||||
queue.put(None)
|
||||
|
||||
@@ -7,7 +7,11 @@ import time
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Interface(object):
|
||||
class AudioError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Interface:
|
||||
def __init__(self, config, debug=False):
|
||||
self.debug = bool(debug)
|
||||
self.config = config
|
||||
@@ -26,7 +30,7 @@ class Interface(object):
|
||||
|
||||
def call(self, name, *args, **kwargs):
|
||||
assert self.lib is not None
|
||||
func_name = 'Pa_{0}'.format(name)
|
||||
func_name = f'Pa_{name}'
|
||||
if self.debug:
|
||||
log.debug('API: %s%s', name, args)
|
||||
func = getattr(self.lib, func_name)
|
||||
@@ -35,7 +39,7 @@ class Interface(object):
|
||||
|
||||
def _error_check(self, res):
|
||||
if res != 0:
|
||||
raise Exception(res, self._error_string(res))
|
||||
raise AudioError(res, self._error_string(res))
|
||||
|
||||
def __enter__(self):
|
||||
self.call('Initialize')
|
||||
@@ -53,7 +57,7 @@ class Interface(object):
|
||||
return Stream(self, config=self.config, write=True)
|
||||
|
||||
|
||||
class Stream(object):
|
||||
class Stream:
|
||||
|
||||
timer = time.time
|
||||
|
||||
@@ -81,7 +85,7 @@ class Stream(object):
|
||||
assert read != write # don't support full duplex
|
||||
|
||||
direction = 'Input' if read else 'Output'
|
||||
api_name = 'GetDefault{0}Device'.format(direction)
|
||||
api_name = f'GetDefault{direction}Device'
|
||||
index = interface.call(api_name, restype=ctypes.c_int)
|
||||
self.params = Stream.Parameters(
|
||||
device=index, # choose default device
|
||||
|
||||
@@ -18,7 +18,7 @@ def volume_controller(cmd):
|
||||
def controller(level):
|
||||
assert 0 < level <= 1
|
||||
percent = 100 * level
|
||||
args = '{0} {1:.0f}%'.format(cmd, percent)
|
||||
args = f'{cmd} {percent:.0f}%'
|
||||
log.debug('Setting volume to %7.3f%% -> "%s"', percent, args)
|
||||
subprocess.check_call(args=args, shell=True)
|
||||
return controller if cmd else (lambda level: None)
|
||||
@@ -66,19 +66,19 @@ def detector(config, src, frame_length=200):
|
||||
max_index = np.argmax(coeffs)
|
||||
freq = config.frequencies[max_index]
|
||||
rms = abs(coeffs[max_index])
|
||||
coherency = rms / total
|
||||
coherency = rms / total if total > 0 else 0.0
|
||||
flags = [total > 0.1, peak < 1.0, coherency > 0.99]
|
||||
|
||||
success = all(flags)
|
||||
if success:
|
||||
msg = 'good signal'
|
||||
else:
|
||||
msg = 'too {0} signal'.format(errors[flags.index(False)])
|
||||
msg = f'too {errors[flags.index(False)]} signal'
|
||||
|
||||
yield dict(
|
||||
freq=freq, rms=rms, peak=peak, coherency=coherency,
|
||||
total=total, success=success, msg=msg
|
||||
)
|
||||
yield {
|
||||
'freq': freq, 'rms': rms, 'peak': peak, 'coherency': coherency,
|
||||
'total': total, 'success': success, 'msg': msg
|
||||
}
|
||||
|
||||
|
||||
def volume_calibration(result_iterator, volume_ctl):
|
||||
@@ -107,8 +107,7 @@ def volume_calibration(result_iterator, volume_ctl):
|
||||
def iter_window(iterable, size):
|
||||
# pylint: disable=stop-iteration-return
|
||||
block = []
|
||||
while True:
|
||||
item = next(iterable)
|
||||
for item in iterable:
|
||||
block.append(item)
|
||||
block = block[-size:]
|
||||
if len(block) == size:
|
||||
@@ -136,7 +135,7 @@ def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
|
||||
log.info('verbose: %s', verbose)
|
||||
if verbose:
|
||||
fields = ['total', 'rms', 'coherency', 'peak']
|
||||
fmt += ', '.join('{0}={{{0}:.4f}}'.format(f) for f in fields)
|
||||
fmt += ', '.join(f'{f}={{{f}:.4f}}' for f in fields)
|
||||
|
||||
for state in recv_iter(config, src, volume_cmd, dump_audio):
|
||||
log.info(fmt.format(**state))
|
||||
|
||||
@@ -73,15 +73,7 @@ def take(iterable, n):
|
||||
return np.array(list(itertools.islice(iterable, n)))
|
||||
|
||||
|
||||
def izip(iterables):
|
||||
""" "Python 3" zip re-implementation for Python 2. """
|
||||
# pylint: disable=stop-iteration-return
|
||||
iterables = [iter(iterable) for iterable in iterables]
|
||||
while True:
|
||||
yield tuple([next(iterable) for iterable in iterables])
|
||||
|
||||
|
||||
class Dummy(object):
|
||||
class Dummy:
|
||||
""" Dummy placeholder object for testing and mocking. """
|
||||
|
||||
def __getattr__(self, name):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Configuration(object):
|
||||
class Configuration:
|
||||
Fs = 32000.0 # sampling frequency [Hz]
|
||||
Tsym = 0.001 # symbol duration [seconds]
|
||||
Npoints = 64
|
||||
@@ -14,8 +14,8 @@ class Configuration(object):
|
||||
latency = 0.1
|
||||
|
||||
# sender config
|
||||
silence_start = 0.25
|
||||
silence_stop = 0.25
|
||||
silence_start = 0.5
|
||||
silence_stop = 0.5
|
||||
|
||||
# receiver config
|
||||
skip_start = 0.1
|
||||
|
||||
@@ -13,7 +13,7 @@ from . import common
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Detector(object):
|
||||
class Detector:
|
||||
|
||||
COHERENCE_THRESHOLD = 0.9
|
||||
|
||||
@@ -85,12 +85,10 @@ class Detector(object):
|
||||
signal = np.concatenate([zeroes, carrier])
|
||||
signal = (2 ** 0.5) * signal / dsp.norm(signal)
|
||||
|
||||
coeffs = []
|
||||
for i in range(len(buf) - len(signal)):
|
||||
b = buf[i:i+len(signal)]
|
||||
norm_b = dsp.norm(b)
|
||||
c = (np.abs(np.dot(b, signal)) / norm_b) if norm_b else 0.0
|
||||
coeffs.append(c)
|
||||
corr = np.abs(np.correlate(buf, signal))
|
||||
norm_b = np.sqrt(np.correlate(np.abs(buf)**2, np.ones(len(signal))))
|
||||
coeffs = np.zeros_like(corr)
|
||||
coeffs[norm_b > 0.0] = corr[norm_b > 0.0] / norm_b[norm_b > 0.0]
|
||||
|
||||
index = np.argmax(coeffs)
|
||||
log.info('Carrier coherence: %.3f%%', coeffs[index] * 100)
|
||||
@@ -115,5 +113,5 @@ class Detector(object):
|
||||
|
||||
freq_err = a / (self.Tsym * self.freq)
|
||||
log.info('Frequency error: %.3f ppm', freq_err * 1e6)
|
||||
self.plt.title('Frequency drift: {0:.3f} ppm'.format(freq_err * 1e6))
|
||||
self.plt.title(f'Frequency drift: {freq_err * 1e6:.3f} ppm')
|
||||
return amplitude, freq_err
|
||||
|
||||
@@ -5,7 +5,7 @@ import numpy as np
|
||||
from . import common
|
||||
|
||||
|
||||
class FIR(object):
|
||||
class FIR:
|
||||
def __init__(self, h):
|
||||
self.h = np.array(h)
|
||||
self.x_state = [0] * len(self.h)
|
||||
@@ -19,7 +19,7 @@ class FIR(object):
|
||||
self.x_state = x_
|
||||
|
||||
|
||||
class Demux(object):
|
||||
class Demux:
|
||||
def __init__(self, sampler, omegas, Nsym):
|
||||
self.Nsym = Nsym
|
||||
self.filters = [exp_iwt(-w, Nsym) / (0.5*self.Nsym) for w in omegas]
|
||||
@@ -33,8 +33,7 @@ class Demux(object):
|
||||
frame = self.sampler.take(size=self.Nsym)
|
||||
if len(frame) == self.Nsym:
|
||||
return np.dot(self.filters, frame)
|
||||
else:
|
||||
raise StopIteration
|
||||
raise StopIteration
|
||||
|
||||
__next__ = next
|
||||
|
||||
@@ -73,14 +72,14 @@ def linear_regression(x, y):
|
||||
return a, b
|
||||
|
||||
|
||||
class MODEM(object):
|
||||
class MODEM:
|
||||
|
||||
def __init__(self, symbols):
|
||||
self.encode_map = {}
|
||||
symbols = np.array(list(symbols))
|
||||
bits_per_symbol = np.log2(len(symbols))
|
||||
bits_per_symbol = np.round(bits_per_symbol)
|
||||
N = (2 ** bits_per_symbol)
|
||||
N = 2 ** bits_per_symbol
|
||||
assert N == len(symbols)
|
||||
bits_per_symbol = int(bits_per_symbol)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from . import sampling
|
||||
from . import levinson
|
||||
|
||||
|
||||
class Equalizer(object):
|
||||
class Equalizer:
|
||||
|
||||
def __init__(self, config):
|
||||
self.carriers = config.carriers
|
||||
@@ -46,9 +46,9 @@ class Equalizer(object):
|
||||
return np.array(list(itertools.islice(symbols, size)))
|
||||
|
||||
|
||||
prefix = [1]*200 + [0]*50
|
||||
equalizer_length = 200
|
||||
silence_length = 50
|
||||
prefix = [1]*equalizer_length + [0]*silence_length
|
||||
|
||||
|
||||
def train(signal, expected, order, lookahead=0):
|
||||
|
||||
@@ -10,11 +10,10 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _checksum_func(x):
|
||||
''' The result will be unsigned on Python 2/3. '''
|
||||
return binascii.crc32(bytes(x)) & 0xFFFFFFFF
|
||||
return binascii.crc32(bytes(x))
|
||||
|
||||
|
||||
class Checksum(object):
|
||||
class Checksum:
|
||||
fmt = '>L' # unsigned longs (32-bit)
|
||||
size = struct.calcsize(fmt)
|
||||
|
||||
@@ -29,12 +28,11 @@ class Checksum(object):
|
||||
if received != expected:
|
||||
log.warning('Invalid checksum: %08x != %08x', received, expected)
|
||||
raise ValueError('Invalid checksum')
|
||||
else:
|
||||
log.debug('Good checksum: %08x', received)
|
||||
log.debug('Good checksum: %08x', received)
|
||||
return payload
|
||||
|
||||
|
||||
class Framer(object):
|
||||
class Framer:
|
||||
block_size = 250
|
||||
prefix_fmt = '>B'
|
||||
prefix_len = struct.calcsize(prefix_fmt)
|
||||
@@ -88,7 +86,7 @@ def chain_wrapper(func):
|
||||
return wrapped
|
||||
|
||||
|
||||
class BitPacker(object):
|
||||
class BitPacker:
|
||||
byte_size = 8
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -10,12 +10,12 @@ from . import framing, common, stream, detect, sampling
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send(config, src, dst, gain=1.0):
|
||||
def send(config, src, dst, gain=1.0, extra_silence=0.0):
|
||||
sender = _send.Sender(dst, config=config, gain=gain)
|
||||
Fs = config.Fs
|
||||
|
||||
# pre-padding audio with silence (priming the audio sending queue)
|
||||
sender.write(np.zeros(int(Fs * config.silence_start)))
|
||||
sender.write(np.zeros(int(Fs * (config.silence_start + extra_silence))))
|
||||
|
||||
sender.start()
|
||||
|
||||
@@ -59,10 +59,11 @@ def recv(config, src, dst, dump_audio=None, pylab=None):
|
||||
gain = 1.0 / amplitude
|
||||
log.debug('Gain correction: %.3f', gain)
|
||||
|
||||
sampler = sampling.Sampler(signal, sampling.Interpolator(), freq=freq)
|
||||
sampler = sampling.Sampler(signal, sampling.defaultInterpolator,
|
||||
freq=freq)
|
||||
receiver.run(sampler, gain=1.0/amplitude, output=dst)
|
||||
return True
|
||||
except BaseException:
|
||||
except BaseException: # pylint: disable=broad-except
|
||||
log.exception('Decoding failed')
|
||||
return False
|
||||
finally:
|
||||
|
||||
@@ -13,7 +13,7 @@ from . import equalizer
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Receiver(object):
|
||||
class Receiver:
|
||||
|
||||
def __init__(self, config, pylab=None):
|
||||
self.stats = {}
|
||||
@@ -43,9 +43,9 @@ class Receiver(object):
|
||||
self.plt.subplot(1, 2, 2)
|
||||
self.plt.plot(np.abs(S))
|
||||
self.plt.plot(equalizer.prefix)
|
||||
errors = (bits != equalizer.prefix)
|
||||
errors = bits != equalizer.prefix
|
||||
if any(errors):
|
||||
msg = 'Incorrect prefix: {0} errors'.format(sum(errors))
|
||||
msg = f'Incorrect prefix: {sum(errors)} errors'
|
||||
raise ValueError(msg)
|
||||
log.debug('Prefix OK')
|
||||
|
||||
@@ -70,6 +70,7 @@ class Receiver(object):
|
||||
self.plt.plot(np.arange(order+lookahead), coeffs)
|
||||
|
||||
equalization_filter = dsp.FIR(h=coeffs)
|
||||
log.debug('Training completed')
|
||||
# Pre-load equalization filter with the signal (+lookahead)
|
||||
equalized = list(equalization_filter(signal))
|
||||
equalized = equalized[prefix+lookahead:-postfix+lookahead]
|
||||
@@ -80,7 +81,7 @@ class Receiver(object):
|
||||
equalizer_length = equalizer.equalizer_length
|
||||
symbols = self.equalizer.demodulator(equalized, equalizer_length)
|
||||
sliced = np.array(symbols).round()
|
||||
errors = np.array(sliced - train_symbols, dtype=np.bool)
|
||||
errors = np.array(sliced - train_symbols, dtype=bool)
|
||||
error_rate = errors.sum() / errors.size
|
||||
|
||||
errors = np.array(symbols - train_symbols)
|
||||
@@ -93,8 +94,9 @@ class Receiver(object):
|
||||
for (i, freq), snr in zip(enumerate(self.frequencies), SNRs):
|
||||
log.debug('%5.1f kHz: SNR = %5.2f dB', freq / 1e3, snr)
|
||||
self._constellation(symbols[:, i], train_symbols[:, i],
|
||||
'$F_c = {0} Hz$'.format(freq), index=i)
|
||||
f'$F_c = {freq} Hz$', index=i)
|
||||
assert error_rate == 0, error_rate
|
||||
log.debug('Training verified')
|
||||
|
||||
def _bitstream(self, symbols, error_handler):
|
||||
streams = []
|
||||
@@ -109,7 +111,7 @@ class Receiver(object):
|
||||
bits = self.modem.decode(S, freq_handler) # list of bit tuples
|
||||
streams.append(bits) # bit stream per frequency
|
||||
|
||||
return common.izip(streams), symbol_list
|
||||
return zip(*streams), symbol_list
|
||||
|
||||
def _demodulate(self, sampler, symbols):
|
||||
symbol_list = []
|
||||
@@ -156,6 +158,7 @@ class Receiver(object):
|
||||
)
|
||||
|
||||
def run(self, sampler, gain, output):
|
||||
log.debug('Receiving')
|
||||
symbols = dsp.Demux(sampler, omegas=self.omegas, Nsym=self.Nsym)
|
||||
self._prefix(symbols, gain=gain)
|
||||
|
||||
@@ -185,7 +188,7 @@ class Receiver(object):
|
||||
symbol_list = np.array(self.stats['symbol_list'])
|
||||
for i, freq in enumerate(self.frequencies):
|
||||
self._constellation(symbol_list[i], self.modem.symbols,
|
||||
'$F_c = {0} Hz$'.format(freq), index=i)
|
||||
f'$F_c = {freq} Hz$', index=i)
|
||||
self.plt.show()
|
||||
|
||||
def _constellation(self, y, symbols, title, index=None):
|
||||
|
||||
@@ -5,7 +5,7 @@ import numpy as np
|
||||
from . import common
|
||||
|
||||
|
||||
class Interpolator(object):
|
||||
class Interpolator:
|
||||
|
||||
def __init__(self, resolution=1024, width=128):
|
||||
|
||||
@@ -30,7 +30,10 @@ class Interpolator(object):
|
||||
assert len(self.filt) == resolution
|
||||
|
||||
|
||||
class Sampler(object):
|
||||
defaultInterpolator = Interpolator()
|
||||
|
||||
|
||||
class Sampler:
|
||||
def __init__(self, src, interp=None, freq=1.0):
|
||||
self.freq = freq
|
||||
self.equalizer = lambda x: x # LTI equalization filter
|
||||
|
||||
@@ -10,7 +10,7 @@ from . import dsp
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Sender(object):
|
||||
class Sender:
|
||||
def __init__(self, fd, config, gain=1.0):
|
||||
self.gain = gain
|
||||
self.offset = 0
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import time
|
||||
|
||||
|
||||
class Reader(object):
|
||||
class Reader:
|
||||
|
||||
wait = 0.2
|
||||
timeout = 2.0
|
||||
bufsize = (8 << 10)
|
||||
bufsize = 8 << 10
|
||||
|
||||
def __init__(self, fd, data_type=None, eof=False):
|
||||
self.fd = fd
|
||||
@@ -24,8 +24,7 @@ class Reader(object):
|
||||
self.total += len(data)
|
||||
block.extend(data)
|
||||
return block
|
||||
else:
|
||||
raise StopIteration()
|
||||
raise StopIteration()
|
||||
|
||||
finish_time = time.time() + self.timeout
|
||||
while time.time() <= finish_time:
|
||||
@@ -45,7 +44,7 @@ class Reader(object):
|
||||
__next__ = next
|
||||
|
||||
|
||||
class Dumper(object):
|
||||
class Dumper:
|
||||
def __init__(self, src, dst):
|
||||
self.src = src
|
||||
self.dst = dst
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import mock
|
||||
import time
|
||||
import pytest
|
||||
from amodem import async
|
||||
from amodem import async_reader
|
||||
import logging
|
||||
|
||||
logging.basicConfig(format='%(message)s')
|
||||
@@ -13,7 +13,7 @@ def test_async_reader():
|
||||
return b'\x00' * n
|
||||
s = mock.Mock()
|
||||
s.read = _read
|
||||
r = async.AsyncReader(s, 1)
|
||||
r = async_reader.AsyncReader(s, 1)
|
||||
|
||||
n = 5
|
||||
assert r.read(n) == b'\x00' * n
|
||||
@@ -25,6 +25,6 @@ def test_async_reader():
|
||||
def test_async_reader_error():
|
||||
s = mock.Mock()
|
||||
s.read.side_effect = IOError()
|
||||
r = async.AsyncReader(s, 1)
|
||||
r = async_reader.AsyncReader(s, 1)
|
||||
with pytest.raises(IOError):
|
||||
r.read(3)
|
||||
@@ -12,7 +12,7 @@ import mock
|
||||
config = config.fastest()
|
||||
|
||||
|
||||
class ProcessMock(object):
|
||||
class ProcessMock:
|
||||
def __init__(self):
|
||||
self.buf = BytesIO()
|
||||
self.stdin = self
|
||||
|
||||
@@ -48,12 +48,6 @@ def test_dumps_loads():
|
||||
assert all(x == y)
|
||||
|
||||
|
||||
def test_izip():
|
||||
x = range(10)
|
||||
y = range(-10, 0)
|
||||
assert list(common.izip([x, y])) == list(zip(x, y))
|
||||
|
||||
|
||||
def test_configs():
|
||||
default = config.Configuration()
|
||||
fastest = config.fastest()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from amodem import stream
|
||||
import subprocess as sp
|
||||
import sys
|
||||
|
||||
script = br"""
|
||||
import sys
|
||||
@@ -14,7 +15,7 @@ while True:
|
||||
|
||||
|
||||
def test_read():
|
||||
p = sp.Popen(args=['python', '-'], stdin=sp.PIPE, stdout=sp.PIPE)
|
||||
p = sp.Popen(args=[sys.executable, '-'], stdin=sp.PIPE, stdout=sp.PIPE)
|
||||
p.stdin.write(script)
|
||||
p.stdin.close()
|
||||
f = stream.Reader(p.stdout)
|
||||
|
||||
@@ -49,13 +49,18 @@ def run(size, chan=None, df=0, success=True, cfg=None):
|
||||
assert rx_data == tx_data
|
||||
|
||||
|
||||
@pytest.fixture(params=[0, 1, 3, 10, 42, 123])
|
||||
@pytest.fixture(params=[0, 1, 3, 10, 16, 17, 42, 123])
|
||||
def small_size(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def test_small(small_size):
|
||||
run(small_size, chan=lambda x: x)
|
||||
@pytest.fixture(params=list(config.bitrates.values()))
|
||||
def all_configs(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def test_small(small_size, all_configs):
|
||||
run(small_size, chan=lambda x: x, cfg=all_configs)
|
||||
|
||||
|
||||
def test_flip():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
class IIR(object):
|
||||
class IIR:
|
||||
def __init__(self, b, a):
|
||||
self.b = np.array(b) / a[0]
|
||||
self.a = np.array(a[1:]) / a[0]
|
||||
|
||||
19
setup.py
19
setup.py
@@ -15,29 +15,28 @@ class PyTest(TestCommand):
|
||||
|
||||
setup(
|
||||
name='amodem',
|
||||
version='1.13.1',
|
||||
version='1.15.4',
|
||||
description='Audio Modem Communication Library',
|
||||
author='Roman Zeyde',
|
||||
author_email='roman.zeyde@gmail.com',
|
||||
author_email='dev@romanzey.de',
|
||||
license='MIT',
|
||||
url='http://github.com/romanz/amodem',
|
||||
packages=['amodem'],
|
||||
tests_require=['pytest'],
|
||||
cmdclass={'test': PyTest},
|
||||
install_requires=['numpy', 'six'],
|
||||
install_requires=['numpy'],
|
||||
platforms=['POSIX'],
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Information Technology',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.2',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: Communications',
|
||||
|
||||
5
tox.ini
5
tox.ini
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = py27,py3
|
||||
envlist = py3
|
||||
[testenv]
|
||||
deps=
|
||||
pytest
|
||||
@@ -7,9 +7,8 @@ deps=
|
||||
pycodestyle
|
||||
coverage
|
||||
pylint
|
||||
six
|
||||
commands=
|
||||
pycodestyle amodem/ scripts/
|
||||
pylint --extension-pkg-whitelist=numpy --reports=no amodem --rcfile .pylintrc
|
||||
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v
|
||||
coverage run --source amodem/ --omit="*/__main__.py" -m pytest -v
|
||||
coverage report
|
||||
|
||||
Reference in New Issue
Block a user