Compare commits

...

159 Commits

Author SHA1 Message Date
Roman Zeyde
ee5c543737 Bump version: 1.15.1 → 1.15.2 2021-06-19 19:38:26 +03:00
Roman Zeyde
1311c58005 Ignore 'consider-using-with' pylint warning 2021-06-19 14:31:38 +03:00
Roman Zeyde
9c6ef2884e Drop izip compatibility helper 2021-06-19 14:12:26 +03:00
Roman Zeyde
21fe42e68d Increase leading and trailing silence defaults
Leading silence should help ignore initial A/D artifacts.
Trailing silence fixes buffering issues (e.g. #49).
2021-06-19 14:07:01 +03:00
Roman Zeyde
9352088389 Bump version: 1.15.0 → 1.15.1 2020-07-03 14:56:12 +03:00
Roman Zeyde
db47cda390 Remove travis, coveralls, landscape & waffle badges 2020-07-03 14:55:12 +03:00
Roman Zeyde
4cd3def507 Fix lint errors and adapt to latest Python 2020-07-03 14:50:59 +03:00
Roman Zeyde
9192a8ff67 Switch to GitHub actions 2020-07-03 14:23:12 +03:00
Roman Zeyde
3b88a23dfb Document PortAudio DLL usage on Windows 2020-07-03 13:16:10 +03:00
Roman Zeyde
10c06f7646 Ignore .pytest_cache/ 2019-03-08 15:11:47 +02:00
Roman Zeyde
df9cbfdf13 Allow more silence before transmission 2019-03-08 15:10:02 +02:00
Roman Zeyde
02f45de4f1 Bump version: 1.14.0 → 1.15.0 2018-08-13 22:28:38 +03:00
Roman Zeyde
6d6bd44dd8 Update Python 3.x versions 2018-08-13 22:22:46 +03:00
Roman Zeyde
4598cfc7f6 Fix 'no-else-return' pylint warning 2018-08-13 22:10:26 +03:00
Roman Zeyde
ff6c9968e8 Don't inherit from object on modern Python 3 2018-08-13 16:04:06 +03:00
Roman Zeyde
0bd691320e Drop Python 2 support 2018-08-13 16:03:41 +03:00
Roman Zeyde
4527eaa931 Refactor log configuration 2018-08-13 15:17:54 +03:00
Roman Zeyde
1dd59f9f8f Merge remote-tracking branch 'jwoillez/jwoillez' 2018-07-23 10:39:16 +03:00
Roman Zeyde
c3cefee85f Remove Python 3.3 from Travis 2018-07-22 22:07:35 +03:00
Julien Woillez
2ab3c62d0d Added debug messages to check timing of recv(). 2018-07-22 19:46:53 +02:00
Julien Woillez
601a14297f Created defaultInterpolator to speed up recv(). 2018-07-22 19:46:53 +02:00
Julien Woillez
71dab0e7bc De-duplicate equalizer_length configuration. 2018-07-22 19:46:53 +02:00
Julien Woillez
53df2d5934 Speed up carrier detection with numpy. 2018-07-22 19:46:53 +02:00
Roman Zeyde
ac2e66bddd Add HN link 2018-06-18 22:11:30 +03:00
ellipticcurv3
0534b6891a More packages needed to install 2018-05-23 12:37:21 +02:00
Roman Zeyde
4f0bc6883b amodem: show the configuration during initialization 2018-05-02 09:39:31 +03:00
Roman Zeyde
4846cdaf8f README: explain audio redirection 2018-05-02 09:29:53 +03:00
Roman Zeyde
0e171a58f2 amodem: disable PortAudio when I/O is redirected 2018-05-02 09:25:49 +03:00
Roman Zeyde
20f90edf8d async: rename to async_reader
async is becoming a reserved word in Python 3.7
2018-05-02 09:13:37 +03:00
Roman Zeyde
72397e541c amodem: fix CLI argument help string 2018-05-02 09:09:34 +03:00
Roman Zeyde
6d17cc132f git: ignore deb_dist/ 2018-03-11 09:47:03 +02:00
Roman Zeyde
9dbb3826c7 Bump version: 1.13.1 → 1.14.0 2018-03-11 08:50:09 +02:00
Roman Zeyde
c991b2264e setup: update package status to stable :) 2018-03-11 08:48:21 +02:00
Roman Zeyde
f30d28e39a Bump version: 1.13.0 → 1.13.1 2018-02-18 11:24:05 +02:00
Roman Zeyde
fff10853a9 version: use bumpversion for bumping 2018-02-18 11:23:41 +02:00
Roman Zeyde
147404645c pep8 -> pycodestyle 2018-02-18 11:05:44 +02:00
Roman Zeyde
0e29f9a606 pylint: fix warnings (mostly import-related) 2018-02-18 11:05:43 +02:00
Roman Zeyde
93a174142b Update supported Python version 2018-02-18 10:04:23 +02:00
Roman Zeyde
3210638003 framing: use '%08x' for CRC-32 logging 2018-02-18 10:04:23 +02:00
Roman Zeyde
ceaf893675 config: use '//' for Python 3 (instead of '/') 2018-02-18 10:04:23 +02:00
Roman Zeyde
6629cb6762 Merge pull request #27 from babetoduarte/master
Added basic comment descriptions to the scripts and to some of the so…
2017-10-15 12:05:53 -07:00
Jorge A. Duarte
555186c2d8 Fixed PEP-8 trailing whitespaces on doctrings. 2017-10-15 13:58:15 -05:00
Jorge A. Duarte
66acac3e35 Made PEP8 changes to several scripts and files. 2017-10-15 13:52:22 -05:00
Jorge A. Duarte
e1bdae2069 Made documentation changes as requested, according to PEP-257. 2017-10-15 13:23:57 -05:00
babetoduarte
1ff777d226 Added basic comment descriptions to the scripts and to some of the source files. There's still much to be done, but it's a start. 2017-10-14 17:03:27 -05:00
Roman Zeyde
40460e0291 README: remove unused badges 2017-10-05 17:53:48 +03:00
Roman Zeyde
43b68779a4 travis: add Python 3.6 2017-10-05 17:28:39 +03:00
Roman Zeyde
90aae24600 update LICENSE 2016-05-26 19:20:54 +03:00
Roman Zeyde
4c6315daf2 record: remove print statement 2016-05-26 19:20:08 +03:00
Roman Zeyde
f7a151534f travis: remove Python 3.2 due to broken coverage support 2016-02-06 17:55:27 +02:00
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
Roman Zeyde
aec1648ae7 __main__: fix PEP8 2015-07-01 14:23:16 +03:00
Roman Zeyde
2ad3ffced4 alsa: fix string format for Python 2.6 2015-07-01 14:20:16 +03:00
Roman Zeyde
318081fca4 __main__: fix dummy interface case 2015-07-01 14:17:49 +03:00
Roman Zeyde
f82a4f4a39 audio: add simple ALSA interface 2015-07-01 13:55:08 +03:00
Roman Zeyde
8d72621b9b __main__: fix format string 2015-06-29 12:23:52 +03:00
Roman Zeyde
2e6196416b __main__: add logging about zlib usage 2015-06-23 11:38:09 +03:00
Roman Zeyde
01c78bae8f __main__: fix imports 2015-06-23 11:37:52 +03:00
Roman Zeyde
c56f696e9e version: use separate file for versioning 2015-06-23 11:37:43 +03:00
Roman Zeyde
3fe515ea59 __main__: should not be executable 2015-06-23 11:15:41 +03:00
Roman Zeyde
55e7152da6 README: add downloads badge 2015-06-13 14:23:03 +03:00
Roman Zeyde
66c639b597 README: use shields.io for badges 2015-06-13 08:26:23 +03:00
Roman Zeyde
a4ebf68223 bump package version 2015-04-23 11:56:33 +03:00
Roman Zeyde
ccfe5c00cf equalizer: shorten prefix and training sequence to 500ms 2015-04-23 11:17:12 +03:00
Roman Zeyde
f476055cf2 detect: move TIMEOUT to config module 2015-04-23 09:43:58 +03:00
Roman Zeyde
fce906df0a cli: add gain option for sender 2015-04-13 12:03:51 +03:00
Roman Zeyde
129b9a4ad0 resample: use argparse instead of sys.argv 2015-04-13 12:03:51 +03:00
Roman Zeyde
0a4584f1b8 README: fix after CLI rename 2015-04-11 11:39:32 +03:00
Roman Zeyde
5d6b47574d __main__: implicit exit code handling 2015-04-11 11:15:45 +03:00
Roman Zeyde
ef7467efd7 travis: fix script name 2015-04-10 22:30:56 +03:00
Roman Zeyde
449d4eac0a CI: fix tox and travis 2015-04-10 22:24:47 +03:00
Roman Zeyde
66148650ed setup.py: fix entry_point to use amodem/__main__.py 2015-04-10 22:24:47 +03:00
Roman Zeyde
3a33338425 levinson: reuse previous vectors 2015-04-08 11:07:35 +03:00
Roman Zeyde
18462289f8 README: add waffle.io for issue tracking 2015-03-31 09:57:52 +03:00
Roman Zeyde
c5f8b48554 plot: fix imports 2015-03-27 09:57:13 +03:00
Roman Zeyde
7b4b2dd7ef sampling: fix lint warning 2015-03-27 09:49:04 +03:00
Roman Zeyde
6d317df465 scripts: fix lint warnings 2015-03-27 09:48:51 +03:00
Roman Zeyde
daca119c6f calib: fixup recv logging 2015-03-26 15:01:49 +02:00
Roman Zeyde
13cbd82d5a tox: use specific pylint rcfile 2015-03-26 15:01:49 +02:00
Roman Zeyde
37ee53d8e4 Revert "travis: test on nightly Python"
numpy has weird problems on nightly Python build.
This reverts commit 66cecb9be4.
2015-03-26 11:00:27 +02:00
Roman Zeyde
66cecb9be4 travis: test on nightly Python 2015-03-26 10:54:20 +02:00
Roman Zeyde
de426c6187 config: validate settings 2015-03-26 09:47:40 +02:00
Roman Zeyde
c637b3e914 cli: change command line switch --zip to --zlib 2015-03-16 07:44:40 +02:00
Roman Zeyde
5cf0fa4e27 calib: fix TypeError on config.frequencies 2015-03-08 10:33:51 +02:00
Roman Zeyde
eb6ace3cc3 README: fix carrier and audio sampling rate info. 2015-03-04 17:10:24 +02:00
Roman Zeyde
e94fd0e2ff README: remove whitespace. 2015-03-04 17:09:46 +02:00
Roman Zeyde
3b4b64253d detect: fix detection logging 2015-03-03 18:15:32 +02:00
Roman Zeyde
ddfdf2f7f4 config: add UT for bitrate verification 2015-02-24 19:01:30 +02:00
Roman Zeyde
9b2ebf05df config: use more readable initialization for MODEM settings 2015-02-24 18:57:04 +02:00
Roman Zeyde
c06e842eb7 config: use lower sample rate for slowest bitrates
this will use much less CPU.
2015-02-22 16:45:59 +02:00
Roman Zeyde
23ce7bba08 bump version 2015-02-22 16:25:56 +02:00
Roman Zeyde
03a600ddd2 recv: remove re-buffering from decoded data. 2015-02-19 19:02:25 +02:00
Roman Zeyde
1aa41db6cb recv: fixup main for-loop 2015-02-19 18:17:03 +02:00
Roman Zeyde
078e429340 recv: fixup bytes' issue 2015-02-19 18:06:36 +02:00
Roman Zeyde
da636212e8 recv: output.write() should get bytes (not bytearray) 2015-02-19 17:56:41 +02:00
Roman Zeyde
65c0892367 travis: add sanity test for CLI 2015-02-19 17:00:03 +02:00
Roman Zeyde
34a892e72c equalizer: remove unused dependencies 2015-02-19 15:28:20 +02:00
Roman Zeyde
a73b09c186 dsp: remove linalg.lstsq() dependency 2015-02-19 15:20:25 +02:00
Roman Zeyde
cac280cf3f dsp: remove buffering from MODEM.decode() 2015-02-19 14:57:39 +02:00
Roman Zeyde
52ee71fad1 scripts: add profiling test 2015-02-19 14:57:14 +02:00
Roman Zeyde
8fe7f1d716 config: bits_per_baud should be integer 2015-02-19 09:54:23 +02:00
Roman Zeyde
6b77534bc2 tox: log testcase names 2015-02-18 18:15:32 +02:00
Roman Zeyde
964b5e0df4 travis: log everything 2015-02-18 18:15:30 +02:00
Roman Zeyde
06cc8918f0 test_transfer: add sanity test for all supported rates. 2015-02-18 18:15:30 +02:00
Roman Zeyde
6a2e320808 equalizer: replace Least-Square solver by Levinson-Durbin recursion 2015-02-18 18:15:30 +02:00
Roman Zeyde
97e992ea56 audio: return self from Interface.load() 2015-02-18 18:15:30 +02:00
Roman Zeyde
61dc35c122 detect: refactor find_start() 2015-02-17 18:08:57 +02:00
Roman Zeyde
e06cb37e2b recv: reduce equalization filter size, due to better timing estimation 2015-02-17 17:35:42 +02:00
Roman Zeyde
d34d2fdbea test_transfer: verify it works after "signal flip". 2015-02-17 17:35:41 +02:00
Roman Zeyde
42ad312418 test_transfer: add 1% frequency drift test. 2015-02-17 17:35:41 +02:00
Roman Zeyde
e0718596e2 send: set gain (to prevent saturation) 2015-02-17 17:35:41 +02:00
Roman Zeyde
fdf6e7e882 sampling: use raised cosine window. 2015-02-17 17:35:41 +02:00
Roman Zeyde
90dd3e55f0 detect: find actual starting offset of the carrier 2015-02-17 17:35:41 +02:00
Roman Zeyde
b3619a75ba detect: remove phase logging 2015-02-17 17:35:41 +02:00
Roman Zeyde
1ddc693683 recv: count errors at prefix 2015-02-17 17:35:41 +02:00
Roman Zeyde
b3804a42be cli: support "dummy" audio interface
specify '-' to to skip loading PortAudio shared library.
2015-02-14 10:47:25 +02:00
Roman Zeyde
c0634a34d0 dsp: pre-compute polynome bit_length
since Python 2.6 has no .bit_length() method
2015-02-13 15:08:11 +02:00
Roman Zeyde
807c03a8e8 equalizer: use PRBS for equalization sequence 2015-02-13 14:47:28 +02:00
Roman Zeyde
e5ff6297b1 autocalib: fix amodem invocation 2015-02-11 17:21:28 +02:00
Roman Zeyde
07a3d5cc98 scripts: fix permissions 2015-02-11 17:21:26 +02:00
Roman Zeyde
d81ec630a5 dsp: move lfilter and IIR to tests 2015-02-11 17:21:26 +02:00
Roman Zeyde
4cebb06e11 setup.py: " -> ' 2015-02-11 10:29:45 +02:00
Roman Zeyde
a43e674fbe scripts: add auto-calibration script
should be used when sender and receiver run at the same computer
2015-02-11 10:29:33 +02:00
Roman Zeyde
ffc9ece45c scripts: add ALSA helpers 2015-02-11 10:01:53 +02:00
Roman Zeyde
e374a65920 scripts: rename existing filenames 2015-02-11 09:59:30 +02:00
Roman Zeyde
9709ffc523 travis: output textual report 2015-02-07 09:09:40 +02:00
Roman Zeyde
8295b0865d PEP8 fixes
imports order
2015-02-07 09:06:59 +02:00
Roman Zeyde
cbdf4d1616 PEP8 fixes
lambdas and imports
2015-02-07 09:02:26 +02:00
Roman Zeyde
a2b220c8e4 PEP8 fixes
lambdas and coverage
2015-02-07 08:52:27 +02:00
Roman Zeyde
5b94d7fd49 PEP8 fixes 2015-02-07 08:42:44 +02:00
Roman Zeyde
cbf14a5153 README: add package status 2015-02-07 08:30:58 +02:00
Roman Zeyde
d09391f43f README: move to restructured text format. 2015-02-06 18:31:10 +02:00
Roman Zeyde
6361f8a257 bump version 2015-02-06 11:29:01 +02:00
50 changed files with 1108 additions and 684 deletions

7
.bumpversion.cfg Normal file
View File

@@ -0,0 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 1.15.2
[bumpversion:file:setup.py]

37
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8]
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 mock pycodestyle coverage pylint six
pip install -e .
- name: Lint
run: |
pycodestyle amodem/ scripts/
pylint --extension-pkg-whitelist=numpy --reports=no amodem --rcfile .pylintrc
- name: Test with pytest
run: |
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v
coverage report

4
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*~
\#*\#
*.out
*.pyc
*.sublime-*
@@ -23,3 +24,6 @@ htmlcov
*_ext.so
/.tox
/dist
/deb_dist
*.html
.pytest_cache/

2
.pylintrc Normal file
View File

@@ -0,0 +1,2 @@
[MESSAGES CONTROL]
disable=invalid-name, missing-docstring, too-many-instance-attributes, too-few-public-methods, logging-format-interpolation, consider-using-with

View File

@@ -1,20 +0,0 @@
sudo: false
language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- "3.4"
install:
- pip install .
- pip install coveralls pep8 mock
script:
- pep8 amodem/ scripts/ tests/ amodem-cli
- cd tests
- coverage run --source=amodem -m py.test
after_success:
- coveralls

View File

@@ -1,6 +1,6 @@
amodem -- Audio Modem Communication Library
Copyright (C) 2014, Roman Zeyde.
Copyright (C) 2014, Roman Zeyde, Google Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

211
README.md
View File

@@ -1,211 +0,0 @@
# Audio Modem Communication Library
[![Build Status](https://travis-ci.org/romanz/amodem.svg?branch=master)](https://travis-ci.org/romanz/amodem)
[![Coverage Status](https://coveralls.io/repos/romanz/amodem/badge.png?branch=master)](https://coveralls.io/r/romanz/amodem?branch=master)
[![Code Health](https://landscape.io/github/romanz/amodem/master/landscape.svg)](https://landscape.io/github/romanz/amodem/master)
[![Supported Python Versions](https://pypip.in/py_versions/amodem/badge.svg)](https://pypi.python.org/pypi/amodem/)
[![License](https://pypip.in/license/amodem/badge.svg)](https://pypi.python.org/pypi/amodem/)
[![Version](https://pypip.in/version/amodem/badge.svg)](https://pypi.python.org/pypi/amodem/)
# 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).
The sender modulates an input binary data file into an 32kHz audio,
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 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.
The modem is using OFDM over an audio cable with the following parameters:
- Sampling rate: 32 kHz
- Baud rate: 1 kHz
- Symbol modulation: BPSK, 4-PSK, 16-QAM ,64-QAM
- Carriers: 2-11 kHz
This way, modem may achieve 60kbps bitrate = 7.5 kB/s.
A simple CRC-32 checksum is used for data integrity verification
on each 250 byte data frame.
# Installation
Make sure that `numpy` and `PortAudio v19` packages are installed (on Debian):
$ sudo apt-get install python-numpy portaudio19-dev
Get the latest released version from [PyPI](https://pypi.python.org/pypi/amodem/):
$ pip install --user amodem
Or, try the latest (unstable) development version from GitHub:
$ git clone https://github.com/romanz/amodem.git
$ cd amodem
$ pip install --user -e .
For graphs and visualization (optional), install `matplotlib` Python package.
For validation, run:
$ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
$ amodem-cli -h
usage: amodem-cli [-h] {send,recv} ...
Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
positional arguments:
{send,recv}
send modulate binary data into audio signal.
recv demodulate audio signal into binary data.
optional arguments:
-h, --help show this help message and exit
# Calibration
Connect the audio cable between the sender and the receiver, and run the
following scripts:
- On the sender's side:
```
~/sender $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
~/sender $ amodem-cli send --calibrate
```
- On the receiver's side:
```
~/receiver $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
~/receiver $ amodem-cli recv --calibrate
```
If BITRATE is not set, the MODEM will use 1 kbps settings (single frequency with BPSK modulation).
Change the sender computer's output audio level, until
all frequencies are received well:
```
3000 Hz: good signal
4000 Hz: good signal
5000 Hz: good signal
6000 Hz: good signal
7000 Hz: good signal
8000 Hz: good signal
9000 Hz: good signal
10000 Hz: good signal
```
If the signal is "too weak", increase the sender's output audio level.
If the signal is "too strong", decrease the sender's output audio level.
If the signal is "too noisy", the SNR is probably too low: decrease the
background noise or increase the signal (without causing saturation).
You can see a video of the calibration process [here](http://www.youtube.com/watch?v=jRUj2Ifk-Po).
# Usage
- Prepare the sender (generate a random binary data file to be sent):
```
~/sender $ dd if=/dev/urandom of=data.tx bs=10KB count=1 status=none
~/sender $ sha256sum data.tx
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.tx
```
- Start the receiver (will wait for the sender to start):
```
~/receiver $ amodem-cli recv -vv -i data.rx
```
- Start the sender (will modulate the data and start the transmission):
```
~/sender $ amodem-cli send -vv -o data.tx
```
- A similar log should be emitted by the sender:
```
2015-01-16 11:49:25,181 DEBUG Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz amodem-cli:174
2015-01-16 11:49:27,028 INFO Sending 2.150 seconds of training audio send.py:63
2015-01-16 11:49:27,029 INFO Starting modulation send.py:68
2015-01-16 11:49:28,016 DEBUG Sent 6.000 kB send.py:50
2015-01-16 11:49:28,776 INFO Sent 10.000 kB @ 1.701 seconds send.py:73
```
- A similar log should be emitted by the receiver:
```
2015-01-16 11:49:24,369 DEBUG Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz amodem-cli:174
2015-01-16 11:49:24,382 DEBUG Skipping 0.100 seconds recv.py:214
2015-01-16 11:49:24,535 INFO Waiting for carrier tone: 3.0 kHz recv.py:221
2015-01-16 11:49:26,741 INFO Carrier detected at ~1761.0 ms @ 3.0 kHz: coherence=99.944%, amplitude=0.499 detect.py:64
2015-01-16 11:49:26,741 DEBUG Buffered 1000 ms of audio detect.py:66
2015-01-16 11:49:26,912 DEBUG Carrier starts at 1761.000 ms detect.py:76
2015-01-16 11:49:26,917 DEBUG Carrier symbols amplitude : 0.499 detect.py:101
2015-01-16 11:49:26,917 DEBUG Current phase on carrier: -0.466 detect.py:112
2015-01-16 11:49:26,917 DEBUG Frequency error: -0.01 ppm detect.py:113
2015-01-16 11:49:26,917 DEBUG Frequency correction: 0.005 ppm recv.py:225
2015-01-16 11:49:26,917 DEBUG Gain correction: 2.004 recv.py:228
2015-01-16 11:49:27,099 DEBUG Prefix OK recv.py:48
2015-01-16 11:49:27,925 DEBUG 3.0 kHz: SNR = 40.58 dB recv.py:92
2015-01-16 11:49:27,925 DEBUG 4.0 kHz: SNR = 41.98 dB recv.py:92
2015-01-16 11:49:27,925 DEBUG 5.0 kHz: SNR = 42.81 dB recv.py:92
2015-01-16 11:49:27,925 DEBUG 6.0 kHz: SNR = 43.71 dB recv.py:92
2015-01-16 11:49:27,926 DEBUG 7.0 kHz: SNR = 43.43 dB recv.py:92
2015-01-16 11:49:27,926 DEBUG 8.0 kHz: SNR = 42.96 dB recv.py:92
2015-01-16 11:49:27,926 DEBUG 9.0 kHz: SNR = 42.66 dB recv.py:92
2015-01-16 11:49:27,926 DEBUG 10.0 kHz: SNR = 42.22 dB recv.py:92
2015-01-16 11:49:27,928 INFO Starting demodulation recv.py:119
2015-01-16 11:49:28,008 DEBUG Got 0.600 kB, realtime: 80.73%, drift: -0.00 ppm recv.py:140
2015-01-16 11:49:28,081 DEBUG Got 1.200 kB, realtime: 76.60%, drift: -0.00 ppm recv.py:140
2015-01-16 11:49:28,153 DEBUG Got 1.800 kB, realtime: 75.17%, drift: -0.00 ppm recv.py:140
2015-01-16 11:49:28,224 DEBUG Got 2.400 kB, realtime: 74.02%, drift: -0.00 ppm recv.py:140
2015-01-16 11:49:28,306 DEBUG Got 3.000 kB, realtime: 75.72%, drift: -0.00 ppm recv.py:140
2015-01-16 11:49:28,382 DEBUG Got 3.600 kB, realtime: 75.71%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:28,458 DEBUG Got 4.200 kB, realtime: 75.72%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:28,528 DEBUG Got 4.800 kB, realtime: 75.10%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:28,609 DEBUG Got 5.400 kB, realtime: 75.76%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:28,686 DEBUG Got 6.000 kB, realtime: 75.80%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:28,757 DEBUG Got 6.600 kB, realtime: 75.36%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:28,828 DEBUG Got 7.200 kB, realtime: 75.03%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:28,909 DEBUG Got 7.800 kB, realtime: 75.50%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:28,980 DEBUG Got 8.400 kB, realtime: 75.15%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:29,051 DEBUG Got 9.000 kB, realtime: 74.88%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:29,261 DEBUG Got 9.600 kB, realtime: 83.31%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:29,342 DEBUG Got 10.200 kB, realtime: 83.23%, drift: -0.01 ppm recv.py:140
2015-01-16 11:49:29,343 DEBUG EOF frame detected framing.py:57
2015-01-16 11:49:29,343 DEBUG Demodulated 10.205 kB @ 1.415 seconds (83.2% realtime) recv.py:165
2015-01-16 11:49:29,343 INFO Received 10.000 kB @ 1.415 seconds = 7.066 kB/s recv.py:169
```
- 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 [here](http://www.youtube.com/watch?v=GZQUtHB8so4).
# Visualization
Make sure that `matplotlib` package is installed, and run (at the receiver side):
```
~/receiver $ amodem-cli recv --plot -o data.rx
```
# Donations
Want to donate? Feel free.
Send to [1C1snTrkHAHM5XnnfuAtiTBaA11HBxjJyv](https://blockchain.info/address/1C1snTrkHAHM5XnnfuAtiTBaA11HBxjJyv).
Thanks :)

248
README.rst Normal file
View File

@@ -0,0 +1,248 @@
Audio Modem Communication Library
=================================
.. image:: https://img.shields.io/pypi/pyversions/amodem.svg
:target: https://pypi.python.org/pypi/amodem/
:alt: Python Versions
.. image:: https://img.shields.io/pypi/l/amodem.svg
:target: https://pypi.python.org/pypi/amodem/
:alt: License
.. image:: https://img.shields.io/pypi/v/amodem.svg
:target: https://pypi.python.org/pypi/amodem/
:alt: Package Version
.. image:: https://img.shields.io/pypi/status/amodem.svg
:target: https://pypi.python.org/pypi/amodem/
:alt: Development Status
Description
-----------
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 the input data into an audio signal,
which is played to the sound card.
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 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
-----------------
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
- Carriers: 2-11 kHz (up to ten carriers)
This way, modem may achieve 80kbps bitrate = 10 kB/s (for best SNR).
A simple CRC-32 checksum is used for data integrity verification
on each 250 byte data frame.
Installation
------------
Make sure that all the required packages are installed (on Debian)::
$ sudo apt-get install python-numpy python-pip portaudio19-dev git
Get the latest released version from PyPI::
$ pip install --user amodem
Or, try the latest (unstable) development version from GitHub::
$ git clone https://github.com/romanz/amodem.git
$ cd amodem
$ pip install --user -e .
For graphs and visualization (optional), install `matplotlib` Python package.
For validation, run::
$ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
$ amodem -h
usage: amodem [-h] {send,recv} ...
Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
positional arguments:
{send,recv}
send modulate binary data into audio signal.
recv demodulate audio signal into binary data.
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
-----------
Connect the audio cable between the sender and the receiver, and run the
following scripts:
On the sender's side::
~/sender $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
~/sender $ amodem send --calibrate
On the receiver's side::
~/receiver $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
~/receiver $ amodem recv --calibrate
If BITRATE is not set, the MODEM will use 1 kbps settings (single frequency with BPSK modulation).
Change the sender computer's output audio level, until
all frequencies are received well::
3000 Hz: good signal
4000 Hz: good signal
5000 Hz: good signal
6000 Hz: good signal
7000 Hz: good signal
8000 Hz: good signal
9000 Hz: good signal
10000 Hz: good signal
If the signal is "too weak", increase the sender's output audio level.
If the signal is "too strong", decrease the sender's output audio level.
If the signal is "too noisy", it may be that the noise level is too high
or that the analog signal is being distorted.
Please run the following command during the calibration session,
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 screencast of the `calibration process <https://asciinema.org/a/25065?autoplay=1>`_.
Usage
-----
Prepare the sender (generate a random binary data file to be sent)::
~/sender $ dd if=/dev/urandom of=data.tx bs=60KB count=1 status=none
~/sender $ sha256sum data.tx
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.tx
Start the receiver (will wait for the sender to start)::
~/receiver $ amodem recv -vv -o data.rx
Start the sender (will modulate the data and start the transmission)::
~/sender $ amodem send -vv -i data.tx
A similar log should be emitted by the sender::
2015-02-06 18:12:46,222 DEBUG Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
2015-02-06 18:12:46,222 INFO PortAudio V19-devel (built Feb 25 2014 21:09:53) loaded
2015-02-06 18:12:48,297 INFO Sending 2.150 seconds of training audio
2015-02-06 18:12:48,297 INFO Starting modulation
2015-02-06 18:12:49,303 DEBUG Sent 6.000 kB
2015-02-06 18:12:50,296 DEBUG Sent 12.000 kB
2015-02-06 18:12:51,312 DEBUG Sent 18.000 kB
2015-02-06 18:12:52,290 DEBUG Sent 24.000 kB
2015-02-06 18:12:53,299 DEBUG Sent 30.000 kB
2015-02-06 18:12:54,299 DEBUG Sent 36.000 kB
2015-02-06 18:12:55,306 DEBUG Sent 42.000 kB
2015-02-06 18:12:56,296 DEBUG Sent 48.000 kB
2015-02-06 18:12:57,311 DEBUG Sent 54.000 kB
2015-02-06 18:12:58,293 DEBUG Sent 60.000 kB
2015-02-06 18:12:58,514 INFO Sent 60.000 kB @ 10.201 seconds
2015-02-06 18:12:59,506 DEBUG Closing input and output
A similar log should be emitted by the receiver::
2015-02-06 18:12:44,848 DEBUG Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
2015-02-06 18:12:44,849 INFO PortAudio V19-devel (built Feb 25 2014 21:09:53) loaded
2015-02-06 18:12:44,929 DEBUG AsyncReader thread started
2015-02-06 18:12:44,930 DEBUG Skipping 0.100 seconds
2015-02-06 18:12:45,141 INFO Waiting for carrier tone: 3.0 kHz
2015-02-06 18:12:47,846 INFO Carrier detected at ~2265.0 ms @ 3.0 kHz
2015-02-06 18:12:47,846 DEBUG Buffered 1000 ms of audio
2015-02-06 18:12:48,025 DEBUG Carrier starts at 2264.000 ms
2015-02-06 18:12:48,029 DEBUG Carrier symbols amplitude : 0.573
2015-02-06 18:12:48,030 DEBUG Current phase on carrier: 0.061
2015-02-06 18:12:48,030 DEBUG Frequency error: -0.009 ppm
2015-02-06 18:12:48,030 DEBUG Frequency correction: 0.009 ppm
2015-02-06 18:12:48,030 DEBUG Gain correction: 1.746
2015-02-06 18:12:48,198 DEBUG Prefix OK
2015-02-06 18:12:48,866 DEBUG 3.0 kHz: SNR = 34.82 dB
2015-02-06 18:12:48,866 DEBUG 4.0 kHz: SNR = 36.39 dB
2015-02-06 18:12:48,867 DEBUG 5.0 kHz: SNR = 37.88 dB
2015-02-06 18:12:48,867 DEBUG 6.0 kHz: SNR = 38.58 dB
2015-02-06 18:12:48,867 DEBUG 7.0 kHz: SNR = 38.86 dB
2015-02-06 18:12:48,867 DEBUG 8.0 kHz: SNR = 38.63 dB
2015-02-06 18:12:48,867 DEBUG 9.0 kHz: SNR = 38.07 dB
2015-02-06 18:12:48,868 DEBUG 10.0 kHz: SNR = 37.22 dB
2015-02-06 18:12:48,869 INFO Starting demodulation
2015-02-06 18:12:49,689 DEBUG Got 6.000 kB, SNR: 41.19 dB, drift: -0.01 ppm
2015-02-06 18:12:50,659 DEBUG Got 12.000 kB, SNR: 41.05 dB, drift: -0.00 ppm
2015-02-06 18:12:51,639 DEBUG Got 18.000 kB, SNR: 40.96 dB, drift: -0.00 ppm
2015-02-06 18:12:52,610 DEBUG Got 24.000 kB, SNR: 41.47 dB, drift: -0.01 ppm
2015-02-06 18:12:53,610 DEBUG Got 30.000 kB, SNR: 41.06 dB, drift: -0.00 ppm
2015-02-06 18:12:54,589 DEBUG Got 36.000 kB, SNR: 41.37 dB, drift: -0.00 ppm
2015-02-06 18:12:55,679 DEBUG Got 42.000 kB, SNR: 41.13 dB, drift: -0.00 ppm
2015-02-06 18:12:56,650 DEBUG Got 48.000 kB, SNR: 41.31 dB, drift: -0.00 ppm
2015-02-06 18:12:57,631 DEBUG Got 54.000 kB, SNR: 41.23 dB, drift: +0.00 ppm
2015-02-06 18:12:58,605 DEBUG Got 60.000 kB, SNR: 41.31 dB, drift: +0.00 ppm
2015-02-06 18:12:58,857 DEBUG EOF frame detected
2015-02-06 18:12:58,857 DEBUG Demodulated 61.205 kB @ 9.988 seconds (97.9% realtime)
2015-02-06 18:12:58,858 INFO Received 60.000 kB @ 9.988 seconds = 6.007 kB/s
2015-02-06 18:12:58,876 DEBUG Closing input and output
2015-02-06 18:12:58,951 DEBUG AsyncReader thread stopped (read 896000 bytes)
After the receiver has finished, verify the received file's hash::
~/receiver $ sha256sum data.rx
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.rx
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)::
~/receiver $ amodem recv --plot -o data.rx

131
amodem-cli → amodem/__main__.py Executable file → Normal file
View File

@@ -1,11 +1,19 @@
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
import argparse
import logging
import os
import sys
import zlib
import logging
import argparse
import pkg_resources
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)
@@ -19,16 +27,14 @@ except ImportError:
log = logging.getLogger('__name__')
from amodem import main, calib, audio, async
from amodem.config import bitrates
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')
self.stream = stream
def read(self, size):
@@ -46,9 +52,10 @@ class Compressor(object):
return result
class Decompressor(object):
class Decompressor:
def __init__(self, stream):
self.obj = zlib.decompressobj()
log.info('Using zlib decompressor')
self.stream = stream
def write(self, data):
@@ -58,8 +65,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 = '-'
@@ -68,7 +77,7 @@ def FileType(mode, audio_interface=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()
@@ -93,21 +102,17 @@ def get_volume_cmd(args):
for c in volume_controllers:
if os.system(c['test']) == 0:
return c[args.command]
return None
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),
config.Nfreq, config.Fs / 1e3)
interface = audio.Interface(config=config)
def wrap(cls, stream, enable):
return cls(stream) if enable else stream
def create_parser(description, interface_factory):
p = argparse.ArgumentParser(description=description)
subparsers = p.add_subparsers()
def wrap(cls, stream, enable):
return cls(stream) if enable else stream
# Modulator
sender = subparsers.add_parser(
'send', help='modulate binary data into audio signal.')
@@ -116,16 +121,23 @@ def _main():
sender.add_argument(
'-o', '--output', help='output file (use "-" for stdout).'
' if not specified, `aplay` tool will be used.')
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.zip), dst=args.dst
config, src=wrap(Compressor, args.src, args.zlib), dst=args.dst,
gain=args.gain, extra_silence=args.silence
),
calib=lambda config, args: calib.send(
config=config, dst=args.dst,
volume_cmd=get_volume_cmd(args)
),
input_type=FileType('rb'),
output_type=FileType('wb', interface),
output_type=FileType('wb', interface_factory),
command='send'
)
@@ -133,8 +145,7 @@ def _main():
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(
@@ -145,14 +156,14 @@ def _main():
help='plot results using pylab module')
receiver.set_defaults(
main=lambda config, args: main.recv(
config, src=args.src, dst=wrap(Decompressor, args.dst, args.zip),
config, src=args.src, dst=wrap(Decompressor, args.dst, args.zlib),
pylab=args.pylab, dump_audio=args.dump
),
calib=lambda config, args: calib.recv(
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'
)
@@ -165,8 +176,8 @@ def _main():
metavar='SYSTEM', help=calibration_help)
sub.add_argument('-l', '--audio-library', default='libportaudio.so',
help='File name of PortAudio shared library.')
sub.add_argument('-z', '--zip', default=False, action='store_true',
help='Use ZIP to compress data.')
sub.add_argument('-z', '--zlib', default=False, action='store_true',
help='Use zlib to compress/decompress data.')
g = sub.add_mutually_exclusive_group()
g.add_argument('-v', '--verbose', default=0, action='count')
g.add_argument('-q', '--quiet', default=False, action='store_true')
@@ -174,7 +185,22 @@ def _main():
if argcomplete:
argcomplete.autocomplete(p)
args = p.parse_args()
return p
class _Dummy:
def __enter__(self):
return self
def __exit__(self, *args):
pass
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:
@@ -187,31 +213,58 @@ def _main():
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) '
'Fs={4:.1f} kHz')
description = fmt.format(_version(),
config.modem_bps / 1e3, len(config.symbols),
config.Nfreq, config.Fs / 1e3)
interface = None
def interface_factory():
return interface
p = create_parser(description, interface_factory)
args = p.parse_args()
_config_log(args)
# Parsing and execution
log.debug(description)
log.info(description)
args.pylab = None
if getattr(args, 'plot', False):
import pylab
import pylab # pylint: disable=import-error,import-outside-toplevel
args.pylab = pylab
with interface.load(args.audio_library):
if args.audio_library == 'ALSA':
from . import alsa # pylint: disable=import-outside-toplevel
interface = alsa.Interface(config)
elif args.audio_library == '-':
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)
with interface:
args.src = args.input_type(args.input)
args.dst = args.output_type(args.output)
try:
if args.calibrate is False:
return args.main(config=config, args=args)
args.main(config=config, args=args)
else:
try:
args.calib(config=config, args=args)
except KeyboardInterrupt:
pass
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__':
success = _main()
sys.exit(0 if success else 1)
_main()

70
amodem/alsa.py Normal file
View File

@@ -0,0 +1,70 @@
"""Code which adds Linux ALSA support for interfaces,
recording and playing.
"""
import subprocess
import logging
log = logging.getLogger(__name__)
class Interface:
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:
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:
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()

View File

@@ -1,11 +1,14 @@
import threading
import six # since `Queue` module was renamed to `queue` (in Python 3)
"""Asynchronous Reading capabilities for amodem."""
import logging
import threading
import six # since `Queue` module was renamed to `queue` (in Python 3)
log = logging.getLogger()
class AsyncReader(object):
class AsyncReader:
def __init__(self, stream, bufsize):
self.stream = stream
self.queue = six.moves.queue.Queue()
@@ -26,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)

View File

@@ -1,3 +1,5 @@
"""Audio capabilities for amodem."""
import ctypes
import logging
import time
@@ -5,7 +7,7 @@ import time
log = logging.getLogger(__name__)
class Interface(object):
class Interface:
def __init__(self, config, debug=False):
self.debug = bool(debug)
self.config = config
@@ -51,7 +53,7 @@ class Interface(object):
return Stream(self, config=self.config, write=True)
class Stream(object):
class Stream:
timer = time.time

View File

@@ -1,15 +1,18 @@
import numpy as np
"""Calibration capabilities for amodem."""
import itertools
import logging
import subprocess
log = logging.getLogger(__name__)
import numpy as np
from . import common
from . import dsp
from . import sampling
from . import stream
log = logging.getLogger(__name__)
def volume_controller(cmd):
def controller(level):
@@ -36,7 +39,7 @@ def send(config, dst, volume_cmd=None, gain=1.0, limit=None):
def frame_iter(config, src, frame_length):
frame_size = frame_length * config.Nsym * config.sample_size
omegas = 2 * np.pi * config.frequencies / config.Fs
omegas = 2 * np.pi * np.array(config.frequencies) / config.Fs
while True:
data = src.read(frame_size)
@@ -72,10 +75,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 +93,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
@@ -102,30 +105,37 @@ 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:
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:
src = stream.Dumper(src, dump_audio)
result_iterator = detector(config=config, src=src)
result_iterator = volume_calibration(result_iterator, volume_ctl)
result_iterator = iter_window(result_iterator, size=3)
for r in result_iterator:
for _prev, curr, _next in iter_window(result_iterator, size=3):
# don't log errors during frequency changes
if r[0].success and r[2].success and r[0].freq != r[2].freq:
r[1].msg = r[1].msg if r[1].success else 'frequency change'
log.info(fmt.format(r[1]))
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,28 +1,38 @@
""" Common package functionality.
Commom utilities and procedures for amodem.
"""
import itertools
import logging
import numpy as np
import logging
log = logging.getLogger(__name__)
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 +50,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 +62,22 @@ 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):
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):
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

@@ -1,39 +1,48 @@
"""Configuration class."""
import numpy as np
class Configuration(object):
class Configuration:
Fs = 32000.0 # sampling frequency [Hz]
Tsym = 0.001 # symbol duration [seconds]
Nfreq = 8 # number of frequencies used
Npoints = 64
F0 = 1e3
frequencies = [1e3, 8e3] # use 1..8 kHz carriers
# audio config
bits_per_sample = 16
sample_size = bits_per_sample // 8
latency = 0.1
# sender config
silence_start = 1.0
silence_stop = 1.0
silence_start = 0.5
silence_stop = 0.5
# receiver config
skip_start = 0.1
timeout = 60.0
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
self.sample_size = self.bits_per_sample // 8
assert self.sample_size * 8 == self.bits_per_sample
self.Ts = 1.0 / self.Fs
self.Fsym = 1 / self.Tsym
self.frequencies = self.F0 + np.arange(self.Nfreq) * self.Fsym
self.Nsym = int(self.Tsym / self.Ts)
self.baud = int(1.0 / self.Tsym)
assert self.baud * self.Tsym == 1
if len(self.frequencies) != 1:
first, last = self.frequencies
self.frequencies = np.arange(first, last + self.baud, self.baud)
self.Nfreq = len(self.frequencies)
self.carrier_index = 0
self.Fc = self.frequencies[self.carrier_index]
self.Nsym = int(self.Tsym / self.Ts)
self.baud = int(1.0 / self.Tsym)
bits_per_symbol = np.log2(self.Npoints)
assert int(bits_per_symbol) == bits_per_symbol
bits_per_symbol = int(np.log2(self.Npoints))
assert 2 ** bits_per_symbol == self.Npoints
self.bits_per_baud = bits_per_symbol * self.Nfreq
self.modem_bps = self.baud * self.bits_per_baud
self.carriers = np.array([
@@ -42,34 +51,40 @@ class Configuration(object):
])
# QAM constellation
Nx = 2 ** int(np.ceil(bits_per_symbol / 2))
Nx = 2 ** int(np.ceil(bits_per_symbol // 2))
Ny = self.Npoints // Nx
symbols = [complex(x, y) for x in range(Nx) for y in range(Ny)]
symbols = np.array(symbols)
symbols = symbols - symbols[-1]/2
self.symbols = symbols / np.max(np.abs(symbols))
# MODEM configurations for various bitrates [kbps]
bitrates = {
1: Configuration(F0=8e3, Npoints=2, Nfreq=1),
2: Configuration(F0=8e3, Npoints=4, Nfreq=1),
4: Configuration(F0=8e3, Npoints=16, Nfreq=1),
8: Configuration(F0=8e3, Npoints=16, Nfreq=2),
12: Configuration(F0=6e3, Npoints=16, Nfreq=3),
16: Configuration(F0=6e3, Npoints=16, Nfreq=4),
18: Configuration(F0=5e3, Npoints=16, Nfreq=5),
24: Configuration(F0=5e3, Npoints=16, Nfreq=6),
28: Configuration(F0=4e3, Npoints=16, Nfreq=7),
32: Configuration(F0=3e3, Npoints=16, Nfreq=8),
36: Configuration(F0=4e3, Npoints=64, Nfreq=6),
42: Configuration(F0=4e3, Npoints=64, Nfreq=7),
48: Configuration(F0=3e3, Npoints=64, Nfreq=8),
54: Configuration(F0=3e3, Npoints=64, Nfreq=9),
60: Configuration(F0=2e3, Npoints=64, Nfreq=10),
64: Configuration(F0=3e3, Npoints=256, Nfreq=8),
72: Configuration(F0=2e3, Npoints=256, Nfreq=9),
80: Configuration(F0=2e3, Npoints=256, Nfreq=10),
1: Configuration(Fs=8e3, Npoints=2, frequencies=[2e3]),
2: Configuration(Fs=8e3, Npoints=4, frequencies=[2e3]),
4: Configuration(Fs=8e3, Npoints=16, frequencies=[2e3]),
8: Configuration(Fs=8e3, Npoints=16, frequencies=[1e3, 2e3]),
12: Configuration(Fs=16e3, Npoints=16, frequencies=[3e3, 5e3]),
16: Configuration(Fs=16e3, Npoints=16, frequencies=[2e3, 5e3]),
20: Configuration(Fs=16e3, Npoints=16, frequencies=[2e3, 6e3]),
24: Configuration(Fs=16e3, Npoints=16, frequencies=[1e3, 6e3]),
28: Configuration(Fs=32e3, Npoints=16, frequencies=[3e3, 9e3]),
32: Configuration(Fs=32e3, Npoints=16, frequencies=[2e3, 9e3]),
36: Configuration(Fs=32e3, Npoints=64, frequencies=[4e3, 9e3]),
42: Configuration(Fs=32e3, Npoints=64, frequencies=[4e3, 10e3]),
48: Configuration(Fs=32e3, Npoints=64, frequencies=[3e3, 10e3]),
54: Configuration(Fs=32e3, Npoints=64, frequencies=[2e3, 10e3]),
60: Configuration(Fs=32e3, Npoints=64, frequencies=[2e3, 11e3]),
64: Configuration(Fs=32e3, Npoints=256, frequencies=[3e3, 10e3]),
72: Configuration(Fs=32e3, Npoints=256, frequencies=[2e3, 10e3]),
80: Configuration(Fs=32e3, Npoints=256, frequencies=[2e3, 11e3]),
}
fastest = lambda: bitrates[max(bitrates)]
slowest = lambda: bitrates[min(bitrates)]
def fastest():
return bitrates[max(bitrates)]
def slowest():
return bitrates[min(bitrates)]

View File

@@ -1,24 +1,26 @@
import numpy as np
import logging
import itertools
import collections
"""Signal detection capabilities for amodem."""
log = logging.getLogger(__name__)
import collections
import itertools
import logging
import numpy as np
from . import dsp
from . import equalizer
from . import common
log = logging.getLogger(__name__)
class Detector(object):
class Detector:
COHERENCE_THRESHOLD = 0.9
CARRIER_DURATION = sum(equalizer.prefix)
CARRIER_THRESHOLD = int(0.9 * CARRIER_DURATION)
SEARCH_WINDOW = int(0.1 * CARRIER_DURATION)
TIMEOUT = 10.0 # [seconds]
START_PATTERN_LENGTH = SEARCH_WINDOW // 4
def __init__(self, config, pylab):
self.freq = config.Fc
@@ -26,7 +28,7 @@ class Detector(object):
self.Nsym = config.Nsym
self.Tsym = config.Tsym
self.maxlen = config.baud # 1 second of symbols
self.max_offset = self.TIMEOUT * config.Fs
self.max_offset = config.timeout * config.Fs
self.plt = pylab
def _wait(self, samples):
@@ -66,7 +68,7 @@ class Detector(object):
bufs.append(np.array(trailing))
buf = np.concatenate(bufs)
offset = self.find_start(buf, duration=self.CARRIER_DURATION)
offset = self.find_start(buf)
start_time += (offset / self.Nsym - self.SEARCH_WINDOW) * self.Tsym
log.debug('Carrier starts at %.3f ms', start_time * 1e3)
@@ -76,14 +78,21 @@ class Detector(object):
amplitude, freq_err = self.estimate(buf[:prefix_length])
return itertools.chain(buf, samples), amplitude, freq_err
def find_start(self, buf, duration):
filt = dsp.FIR(dsp.exp_iwt(self.omega, self.Nsym))
p = np.abs(list(filt(buf))) ** 2
p = np.cumsum(p)[self.Nsym-1:]
p = np.concatenate([[0], p])
length = (duration - 1) * self.Nsym
correlations = np.abs(p[length:] - p[:-length])
offset = np.argmax(correlations)
def find_start(self, buf):
carrier = dsp.exp_iwt(self.omega, self.Nsym)
carrier = np.tile(carrier, self.START_PATTERN_LENGTH)
zeroes = carrier * 0.0
signal = np.concatenate([zeroes, carrier])
signal = (2 ** 0.5) * signal / dsp.norm(signal)
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)
offset = index + len(zeroes)
return offset
def estimate(self, buf, skip=5):
@@ -93,7 +102,7 @@ class Detector(object):
symbols = np.array(symbols[skip:-skip])
amplitude = np.mean(np.abs(symbols))
log.debug('Carrier symbols amplitude : %.3f', amplitude)
log.info('Carrier symbols amplitude : %.3f', amplitude)
phase = np.unwrap(np.angle(symbols)) / (2 * np.pi)
indices = np.arange(len(phase))
@@ -103,9 +112,6 @@ class Detector(object):
self.plt.plot(indices, a * indices + b)
freq_err = a / (self.Tsym * self.freq)
last_phase = a * indices[-1] + b
log.debug('Current phase on carrier: %.3f', last_phase)
log.debug('Frequency error: %.3f ppm', freq_err * 1e6)
log.info('Frequency error: %.3f ppm', freq_err * 1e6)
self.plt.title('Frequency drift: {0:.3f} ppm'.format(freq_err * 1e6))
return amplitude, freq_err

View File

@@ -1,10 +1,11 @@
"""Digital Signal Processing capabilities for amodem."""
import numpy as np
from numpy import linalg
from . import common
class FIR(object):
class FIR:
def __init__(self, h):
self.h = np.array(h)
self.x_state = [0] * len(self.h)
@@ -18,33 +19,7 @@ class FIR(object):
self.x_state = x_
class IIR(object):
def __init__(self, b, a):
self.b = np.array(b) / a[0]
self.a = np.array(a[1:]) / a[0]
self.x_state = [0] * len(self.b)
self.y_state = [0] * (len(self.a) + 1)
def __call__(self, x):
x_, y_ = self.x_state, self.y_state
for v in x:
x_ = [v] + x_[:-1]
y_ = y_[:-1]
num = np.dot(x_, self.b)
den = np.dot(y_, self.a)
y = num - den
y_ = [y] + y_
yield y
self.x_state, self.y_state = x_, y_
def lfilter(b, a, x):
f = IIR(b=b, a=a)
y = list(f(x))
return np.array(y)
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]
@@ -58,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
@@ -72,29 +46,33 @@ def norm(x):
return np.sqrt(np.dot(x.conj(), x).real)
def rms(x):
return np.mean(np.abs(x) ** 2, axis=0) ** 0.5
def coherence(x, omega):
n = len(x)
Hc = exp_iwt(-omega, n) / np.sqrt(0.5*n)
norm_x = norm(x)
if norm_x:
return np.dot(Hc, x) / norm_x
else:
if not norm_x:
return 0.0
return np.dot(Hc, x) / norm_x
def linear_regression(x, y):
''' Find (a,b) such that y = a*x + b. '''
""" Find (a,b) such that y = a*x + b. """
x = np.array(x)
y = np.array(y)
ones = np.ones(len(x))
M = np.array([x, ones]).T
a, b = linalg.lstsq(M, y)[0]
mean_x = np.mean(x)
mean_y = np.mean(y)
x_ = x - mean_x
y_ = y - mean_y
a = np.dot(y_, x_) / np.dot(x_, x_)
b = mean_y - a * mean_x
return a, b
class MODEM(object):
buf_size = 16
class MODEM:
def __init__(self, symbols):
self.encode_map = {}
@@ -120,14 +98,28 @@ class MODEM(object):
yield self.encode_map[bits_tuple]
def decode(self, symbols, error_handler=None):
''' Maximum-likelihood decoding, using naive nearest-neighbour. '''
""" Maximum-likelihood decoding, using naive nearest-neighbour. """
symbols_vec = self.symbols
_dec = self.decode_list
for syms in common.iterate(symbols, self.buf_size, truncate=False):
for received in syms:
error = np.abs(symbols_vec - received)
index = np.argmin(error)
decoded, bits = _dec[index]
if error_handler:
error_handler(received=received, decoded=decoded)
yield bits
for received in symbols:
error = np.abs(symbols_vec - received)
index = np.argmin(error)
decoded, bits = _dec[index]
if error_handler:
error_handler(received=received, decoded=decoded)
yield bits
def prbs(reg, poly, bits):
""" Simple pseudo-random number generator. """
mask = (1 << bits) - 1
size = 0 # effective register size (in bits)
while (poly >> size) > 1:
size += 1
while True:
yield reg & mask
reg = reg << 1
if reg >> size:
reg = reg ^ poly

View File

@@ -1,16 +1,15 @@
import numpy as np
from numpy.linalg import lstsq
from amodem import dsp
from amodem import sampling
"""Audio equalizing capabilities for amodem."""
import itertools
import random
import numpy as np
from . import dsp
from . import sampling
from . import levinson
class Equalizer(object):
_constellation = [1, 1j, -1, -1j]
class Equalizer:
def __init__(self, config):
self.carriers = config.carriers
@@ -18,12 +17,15 @@ class Equalizer(object):
self.Nfreq = config.Nfreq
self.Nsym = config.Nsym
def train_symbols(self, length, seed=0, constant_prefix=16):
r = random.Random(seed)
# Use low-level randomness for cross-version compatibility.
random_symbol = lambda: self._constellation[r.getrandbits(2)]
choose = lambda: [random_symbol() for j in range(self.Nfreq)]
symbols = np.array([choose() for _ in range(length)])
def train_symbols(self, length, constant_prefix=16):
r = dsp.prbs(reg=1, poly=0x1100b, bits=2)
constellation = [1, 1j, -1, -1j]
symbols = []
for _ in range(length):
symbols.append([constellation[next(r)] for _ in range(self.Nfreq)])
symbols = np.array(symbols)
# Constant symbols (for analog debugging)
symbols[:constant_prefix, :] = 1
return symbols
@@ -44,28 +46,21 @@ class Equalizer(object):
return np.array(list(itertools.islice(symbols, size)))
equalizer_length = 200
silence_length = 50
prefix = [1]*equalizer_length + [0]*silence_length
def train(signal, expected, order, lookahead=0):
signal = [np.zeros(order-1), signal, np.zeros(lookahead)]
signal = np.concatenate(signal)
length = len(expected)
padding = np.zeros(lookahead)
assert len(signal) == len(expected)
x = np.concatenate([signal, padding])
y = np.concatenate([padding, expected])
A = []
b = []
# construct Ah=b over-constrained equation system,
# used for least-squares estimation of the filter.
for i in range(length - order):
offset = order + i
row = signal[offset-order:offset+lookahead]
A.append(np.array(row, ndmin=2))
b.append(expected[i])
A = np.concatenate(A, axis=0)
b = np.array(b)
h = lstsq(A, b)[0]
h = h[::-1].real
return h
prefix = [1]*400 + [0]*50
equalizer_length = 500
silence_length = 100
N = order + lookahead # filter length
Rxx = np.zeros(N)
Rxy = np.zeros(N)
for i in range(N):
Rxx[i] = np.dot(x[i:], x[:len(x)-i])
Rxy[i] = np.dot(y[i:], x[:len(x)-i])
return levinson.solver(t=Rxx, y=Rxy)

View File

@@ -1,17 +1,20 @@
from . import common
import binascii
import functools
import itertools
import binascii
import struct
import logging
import struct
from . import common
log = logging.getLogger(__name__)
_checksum_func = lambda x: binascii.crc32(bytes(x)) & 0xFFFFFFFF
# (so the result will be unsigned on Python 2/3)
def _checksum_func(x):
''' The result will be unsigned on Python 2/3. '''
return binascii.crc32(bytes(x)) & 0xFFFFFFFF
class Checksum(object):
class Checksum:
fmt = '>L' # unsigned longs (32-bit)
size = struct.calcsize(fmt)
@@ -24,12 +27,13 @@ class Checksum(object):
payload = data[self.size:]
expected = _checksum_func(payload)
if received != expected:
log.warning('Invalid checksum: %04x != %04x', received, expected)
log.warning('Invalid checksum: %08x != %08x', received, expected)
raise ValueError('Invalid checksum')
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)
@@ -83,7 +87,7 @@ def chain_wrapper(func):
return wrapped
class BitPacker(object):
class BitPacker:
byte_size = 8
def __init__(self):
@@ -113,8 +117,7 @@ def _to_bytes(bits):
yield [converter.to_byte[chunk]]
@chain_wrapper
def decode(bits, framer=None):
def decode_frames(bits, framer=None):
framer = framer or Framer()
for frame in framer.decode(_to_bytes(bits)):
yield frame
yield bytes(frame)

30
amodem/levinson.py Normal file
View File

@@ -0,0 +1,30 @@
import numpy as np
def solver(t, y):
""" Solve Mx = y for x, where M[i,j] = t[|i-j|], in O(N^2) steps.
See http://en.wikipedia.org/wiki/Levinson_recursion for details.
"""
N = len(t)
assert len(y) == N
t0 = np.array([1.0 / t[0]])
f = [t0] # forward vectors
b = [t0] # backward vectors
for n in range(1, N):
prev_f = f[-1]
prev_b = b[-1]
ef = sum(t[n-i] * prev_f[i] for i in range(n))
eb = sum(t[i+1] * prev_b[i] for i in range(n))
f_ = np.concatenate([prev_f, [0]])
b_ = np.concatenate([[0], prev_b])
det = 1.0 - ef * eb
f.append((f_ - ef * b_) / det)
b.append((b_ - eb * f_) / det)
x = []
for n in range(N):
x = np.concatenate([x, [0]])
ef = sum(t[n-i] * x[i] for i in range(n))
x = x + (y[n] - ef) * b[n]
return x

View File

@@ -1,6 +1,8 @@
import numpy as np
import logging
import itertools
import logging
import numpy as np
from . import send as _send
from . import recv as _recv
from . import framing, common, stream, detect, sampling
@@ -8,12 +10,12 @@ from . import framing, common, stream, detect, sampling
log = logging.getLogger(__name__)
def send(config, src, dst):
sender = _send.Sender(dst, config=config)
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()
@@ -57,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:

View File

@@ -1,18 +1,19 @@
import numpy as np
import logging
import itertools
import functools
import itertools
import logging
import time
log = logging.getLogger(__name__)
import numpy as np
from . import dsp
from . import common
from . import framing
from . import equalizer
log = logging.getLogger(__name__)
class Receiver(object):
class Receiver:
def __init__(self, config, pylab=None):
self.stats = {}
@@ -28,6 +29,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))
@@ -41,15 +43,17 @@ class Receiver(object):
self.plt.subplot(1, 2, 2)
self.plt.plot(np.abs(S))
self.plt.plot(equalizer.prefix)
if any(bits != equalizer.prefix):
raise ValueError('Incorrect prefix')
errors = (bits != equalizer.prefix)
if any(errors):
msg = 'Incorrect prefix: {0} errors'.format(sum(errors))
raise ValueError(msg)
log.debug('Prefix OK')
def _train(self, sampler, order, lookahead):
Nfreq = len(self.frequencies)
equalizer_length = equalizer.equalizer_length
train_symbols = self.equalizer.train_symbols(equalizer_length)
train_signal = self.equalizer.modulator(train_symbols) * Nfreq
train_signal = (self.equalizer.modulator(train_symbols) *
len(self.frequencies))
prefix = postfix = equalizer.silence_length * self.Nsym
signal_length = equalizer_length * self.Nsym + prefix + postfix
@@ -58,7 +62,7 @@ class Receiver(object):
coeffs = equalizer.train(
signal=signal[prefix:-postfix],
expected=train_signal,
expected=np.concatenate([train_signal, np.zeros(lookahead)]),
order=order, lookahead=lookahead
)
@@ -66,6 +70,8 @@ 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]
self._verify_training(equalized, train_symbols)
@@ -79,10 +85,9 @@ class Receiver(object):
error_rate = errors.sum() / errors.size
errors = np.array(symbols - train_symbols)
rms = lambda x: (np.mean(np.abs(x) ** 2, axis=0) ** 0.5)
noise_rms = rms(errors)
signal_rms = rms(train_symbols)
noise_rms = dsp.rms(errors)
signal_rms = dsp.rms(train_symbols)
SNRs = 20.0 * np.log10(signal_rms / noise_rms)
self.plt.figure()
@@ -91,6 +96,7 @@ class Receiver(object):
self._constellation(symbols[:, i], train_symbols[:, i],
'$F_c = {0} Hz$'.format(freq), index=i)
assert error_rate == 0, error_rate
log.debug('Training verified')
def _bitstream(self, symbols, error_handler):
streams = []
@@ -105,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 = []
@@ -135,10 +141,10 @@ class Receiver(object):
def _update_sampler(self, errors, sampler):
err = np.array([e for v in errors.values() for e in v])
err = np.mean(np.angle(err))/(2*np.pi) if len(err) else 0
err = np.mean(np.angle(err))/(2*np.pi) if err.size 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):
@@ -152,20 +158,19 @@ 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)
filt = self._train(sampler, order=20, lookahead=20)
filt = self._train(sampler, order=10, lookahead=10)
sampler.equalizer = lambda x: list(filt(x))
bitstream = self._demodulate(sampler, symbols)
bitstream = itertools.chain.from_iterable(bitstream)
data = framing.decode(bitstream)
for chunk in common.iterate(data=data, size=256,
truncate=False, func=bytearray):
output.write(chunk)
self.output_size += len(chunk)
for frame in framing.decode_frames(bitstream):
output.write(frame)
self.output_size += len(frame)
def report(self):
if self.stats:

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env python
import numpy as np
import itertools
from amodem import common
import numpy as np
from . import common
class Interpolator(object):
class Interpolator:
def __init__(self, resolution=1024, width=128):
@@ -14,7 +14,7 @@ class Interpolator(object):
N = resolution * width
u = np.arange(-N, N, dtype=float)
window = (1 + np.cos(0.5 * np.pi * u / N)) / 2.0 # (Hann window)
window = np.cos(0.5 * np.pi * u / N) ** 2.0 # raised cosine
h = np.sinc(u / resolution) * window
self.filt = []
@@ -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
@@ -57,26 +60,26 @@ class Sampler(object):
def _take(self, size):
frame = np.zeros(size)
count = 0
try:
for frame_index in range(size):
offset = self.offset
# offset = k + (j / self.resolution)
k = int(offset) # integer part
j = int((offset - k) * self.resolution) # fractional part
coeffs = self.filt[j] # choose correct filter phase
end = k + self.width
# process input until all buffer is full with samples
for frame_index in range(size):
offset = self.offset
# offset = k + (j / self.resolution)
k = int(offset) # integer part
j = int((offset - k) * self.resolution) # fractional part
coeffs = self.filt[j] # choose correct filter phase
end = k + self.width
# process input until all buffer is full with samples
try:
while self.index < end:
self.buff[:-1] = self.buff[1:]
self.buff[-1] = next(self.src) # throws StopIteration
self.index += 1
except StopIteration:
break
self.offset += self.freq
# apply interpolation filter
frame[frame_index] = np.dot(coeffs, self.buff)
count = frame_index + 1
except StopIteration:
pass
self.offset += self.freq
# apply interpolation filter
frame[frame_index] = np.dot(coeffs, self.buff)
count = frame_index + 1
return self.equalizer(frame[:count])

View File

@@ -1,16 +1,18 @@
import numpy as np
import logging
import itertools
import logging
log = logging.getLogger(__name__)
import numpy as np
from . import common
from . import equalizer
from . import dsp
log = logging.getLogger(__name__)
class Sender(object):
def __init__(self, fd, config):
class Sender:
def __init__(self, fd, config, gain=1.0):
self.gain = gain
self.offset = 0
self.fd = fd
self.modem = dsp.MODEM(config.symbols)
@@ -22,7 +24,7 @@ class Sender(object):
self.equalizer = equalizer.Equalizer(config)
def write(self, sym):
sym = np.array(sym)
sym = np.array(sym) * self.gain
data = common.dumps(sym)
self.fd.write(data)
self.offset += len(sym)

View File

@@ -1,7 +1,7 @@
import time
class Reader(object):
class Reader:
wait = 0.2
timeout = 2.0
@@ -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

40
amodem/tests/test_alsa.py Normal file
View 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'])]

View File

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

View File

@@ -15,7 +15,8 @@ def test():
lib.Pa_OpenStream.return_value = 0
cdll.return_value = lib
interface = audio.Interface(config=config.fastest(), debug=True)
with interface.load(name='portaudio'):
assert interface.load(name='portaudio') is interface
with interface:
s = interface.player()
assert s.params.device == 1
s.stream = 1 # simulate non-zero output stream handle

View File

@@ -1,7 +1,6 @@
from amodem import calib
from amodem import common
from amodem import config
config = config.fastest()
from io import BytesIO
@@ -10,22 +9,16 @@ import random
import pytest
import mock
config = config.fastest()
class ProcessMock(object):
class ProcessMock:
def __init__(self):
self.buf = BytesIO()
self.stdin = self
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)
@@ -46,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():
@@ -55,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():
@@ -64,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():
@@ -101,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
@@ -153,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

@@ -1,4 +1,5 @@
from amodem import common
from amodem import config
import numpy as np
@@ -47,15 +48,9 @@ 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_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()
slowest = config.slowest()
assert slowest.modem_bps <= default.modem_bps
assert fastest.modem_bps >= default.modem_bps

View File

@@ -0,0 +1,12 @@
from amodem import config
def test_bitrates():
for rate, cfg in sorted(config.bitrates.items()):
assert rate * 1000 == cfg.modem_bps
def test_slowest():
c = config.slowest()
assert c.Npoints == 2
assert list(c.symbols) == [-1j, 1j]

View File

@@ -19,7 +19,7 @@ def test_detect():
detector = detect.Detector(config, pylab=common.Dummy())
samples, amp, freq_err = detector.run(x)
assert abs(1 - amp) < 1e-12
assert abs(freq_err) < 1e-16
assert abs(freq_err) < 1e-12
x = np.cos(2 * np.pi * (2*config.Fc) * t)
with pytest.raises(ValueError):
@@ -56,6 +56,6 @@ def test_find_start():
for offset in range(32):
bufs = [prefix, [0] * offset, carrier, postfix]
buf = np.concatenate(bufs)
start = detector.find_start(buf, length)
start = detector.find_start(buf)
expected = offset + len(prefix)
assert expected == start

View File

@@ -1,14 +1,14 @@
import numpy as np
from numpy.linalg import norm
from amodem import dsp
from amodem import sampling
from amodem import config
config = config.fastest()
import utils
import numpy as np
import random
import itertools
config = config.fastest()
def test_linreg():
x = np.array([1, 3, 2, 8, 4, 6, 9, 7, 0, 5])
@@ -21,11 +21,11 @@ def test_linreg():
def test_filter():
x = range(10)
y = dsp.lfilter(b=[1], a=[1], x=x)
y = utils.lfilter(b=[1], a=[1], x=x)
assert (np.array(x) == y).all()
x = [1] + [0] * 10
y = dsp.lfilter(b=[0.5], a=[1, -0.5], x=x)
y = utils.lfilter(b=[0.5], a=[1, -0.5], x=x)
assert list(y) == [0.5 ** (i+1) for i in range(len(x))]
@@ -50,7 +50,9 @@ def test_qam():
decoded = list(q.decode(S))
assert decoded == bits
noise = lambda A: A*(r.uniform(-1, 1) + 1j*r.uniform(-1, 1))
def noise(A):
return A*(r.uniform(-1, 1) + 1j*r.uniform(-1, 1))
noised_symbols = [(s + noise(1e-3)) for s in S]
decoded = list(q.decode(noised_symbols))
assert decoded == bits
@@ -70,3 +72,22 @@ def test_overflow():
for i in range(10000):
s = 10*(r.normal() + 1j * r.normal())
quantize(q, s)
def test_prbs():
r = list(itertools.islice(dsp.prbs(reg=1, poly=0x7, bits=2), 4))
assert r == [1, 2, 3, 1]
r = list(itertools.islice(dsp.prbs(reg=1, poly=0x7, bits=1), 4))
assert r == [1, 0, 1, 1]
r = list(itertools.islice(dsp.prbs(reg=1, poly=0xd, bits=3), 8))
assert r == [1, 2, 4, 5, 7, 3, 6, 1]
r = list(itertools.islice(dsp.prbs(reg=1, poly=0xd, bits=2), 8))
assert r == [1, 2, 0, 1, 3, 3, 2, 1]
period = 2 ** 16 - 1
r = list(itertools.islice(dsp.prbs(reg=1, poly=0x1100b, bits=16), period))
r.sort()
assert r == list(range(1, 2 ** 16))

View File

@@ -1,15 +1,17 @@
from numpy.linalg import norm
from numpy.random import RandomState
import numpy as np
from amodem import dsp
import utils
from amodem import equalizer
from amodem import dsp
from amodem import config
config = config.fastest()
def assert_approx(x, y, e=1e-12):
assert norm(x - y) < e * norm(x)
x = x.flatten()
y = y.flatten()
assert dsp.norm(x - y) < e * dsp.norm(x)
def test_training():
@@ -24,14 +26,14 @@ def test_commutation():
x = np.random.RandomState(seed=0).normal(size=1000)
b = [1, 1j, -1, -1j]
a = [1, 0.1]
y = dsp.lfilter(x=x, b=b, a=a)
y1 = dsp.lfilter(x=dsp.lfilter(x=x, b=b, a=[1]), b=[1], a=a)
y2 = dsp.lfilter(x=dsp.lfilter(x=x, b=[1], a=a), b=b, a=[1])
y = utils.lfilter(x=x, b=b, a=a)
y1 = utils.lfilter(x=utils.lfilter(x=x, b=b, a=[1]), b=[1], a=a)
y2 = utils.lfilter(x=utils.lfilter(x=x, b=[1], a=a), b=b, a=[1])
assert_approx(y, y1)
assert_approx(y, y2)
z = dsp.lfilter(x=y, b=a, a=[1])
z_ = dsp.lfilter(x=x, b=b, a=[1])
z = utils.lfilter(x=y, b=a, a=[1])
z_ = utils.lfilter(x=x, b=b, a=[1])
assert_approx(z, z_)
@@ -46,19 +48,20 @@ def test_modem():
def test_signal():
length = 100
length = 120
x = np.sign(RandomState(0).normal(size=length))
x[-20:] = 0 # make sure the signal has bounded support
den = np.array([1, -0.6, 0.1])
num = np.array([0.5])
y = dsp.lfilter(x=x, b=num, a=den)
y = utils.lfilter(x=x, b=num, a=den)
lookahead = 2
h = equalizer.train(
signal=y, expected=x, order=len(den), lookahead=lookahead)
assert norm(h[:lookahead]) < 1e-12
assert dsp.norm(h[:lookahead]) < 1e-12
h = h[lookahead:]
assert_approx(h, den / num)
x_ = dsp.lfilter(x=y, b=h, a=[1])
x_ = utils.lfilter(x=y, b=h, a=[1])
assert_approx(x_, x)

View File

@@ -8,6 +8,7 @@ import pytest
def concat(iterable):
return bytearray(itertools.chain.from_iterable(iterable))
r = random.Random(0)
blob = bytearray(r.randrange(0, 256) for i in range(64 * 1024))
@@ -31,22 +32,22 @@ def test_framer(data):
def test_main(data):
encoded = framing.encode(data)
decoded = framing.decode(encoded)
assert bytearray(decoded) == data
decoded = framing.decode_frames(encoded)
assert concat(decoded) == data
def test_fail():
encoded = list(framing.encode(''))
encoded[-1] = not encoded[-1]
with pytest.raises(ValueError):
list(framing.decode(encoded))
concat(framing.decode_frames(encoded))
def test_missing():
f = framing.Framer()
with pytest.raises(ValueError):
list(f.decode(b''))
concat(f.decode(b''))
with pytest.raises(ValueError):
list(f.decode(b'\x01'))
concat(f.decode(b'\x01'))
with pytest.raises(ValueError):
list(f.decode(b'\xff'))
concat(f.decode(b'\xff'))

View File

@@ -20,6 +20,6 @@ def test_resample():
def test_coeffs():
I = sampling.Interpolator(width=4, resolution=16)
err = I.filt[0] - [0, 0, 0, 1, 0, 0, 0, 0]
interp = sampling.Interpolator(width=4, resolution=16)
err = interp.filt[0] - [0, 0, 0, 1, 0, 0, 0, 0]
assert np.max(np.abs(err)) < 1e-10

View File

@@ -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)
@@ -29,7 +30,6 @@ def test_read():
j += 1
try:
for buf in f:
pass
next(f)
except IOError as e:
assert e.args == ('timeout',)

View File

@@ -1,35 +1,25 @@
from amodem import main
from amodem import common
from amodem import sampling
from amodem import config
import utils
import numpy as np
import os
from io import BytesIO
import numpy as np
from amodem import main
from amodem import common
from amodem import dsp
from amodem import sampling
from amodem import config
from amodem import async
config = config.fastest()
import pytest
import logging
logging.basicConfig(level=logging.DEBUG,
logging.basicConfig(level=logging.DEBUG, # useful for debugging
format='%(asctime)s %(levelname)-12s %(message)s')
import pytest
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):
def run(size, chan=None, df=0, success=True, cfg=None):
if cfg is None:
cfg = config.fastest()
tx_data = os.urandom(size)
tx_audio = BytesIO()
main.send(config=config, src=BytesIO(tx_data), dst=tx_audio)
main.send(config=cfg, src=BytesIO(tx_data), dst=tx_audio, gain=0.5)
data = tx_audio.getvalue()
data = common.loads(data)
@@ -45,11 +35,9 @@ def run(size, chan=None, df=0, success=True, reader=None):
rx_data = BytesIO()
dump = BytesIO()
if reader:
rx_audio = reader(rx_audio)
try:
result = main.recv(config=config, src=rx_audio, dst=rx_data,
dump_audio=dump)
result = main.recv(config=cfg, src=rx_audio, dst=rx_data,
dump_audio=dump, pylab=None)
finally:
rx_audio.close()
@@ -61,13 +49,27 @@ def run(size, chan=None, df=0, success=True, reader=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():
run(16, chan=lambda x: -x)
def test_large_drift():
run(1, df=+0.01)
run(1, df=-0.01)
def test_error():
@@ -76,7 +78,7 @@ def test_error():
@pytest.fixture(params=[sign * mag for sign in (+1, -1)
for mag in (0.1, 1, 10, 100, 1e3, 2e3)])
for mag in (0.1, 1, 10, 100, 1e3, 10e3)])
def freq_err(request):
return request.param * 1e-6
@@ -86,11 +88,11 @@ def test_timing(freq_err):
def test_lowpass():
run(1024, chan=lambda x: dsp.lfilter(b=[0.9], a=[1.0, -0.1], x=x))
run(1024, chan=lambda x: utils.lfilter(b=[0.9], a=[1.0, -0.1], x=x))
def test_highpass():
run(1024, chan=lambda x: dsp.lfilter(b=[0.9], a=[1.0, 0.1], x=x))
run(1024, chan=lambda x: utils.lfilter(b=[0.9], a=[1.0, 0.1], x=x))
def test_attenuation():
@@ -109,3 +111,12 @@ def test_medium_noise():
def test_large():
run(54321, chan=lambda x: x)
@pytest.fixture(params=sorted(config.bitrates.keys()))
def rate(request):
return request.param
def test_rate(rate):
run(1, cfg=config.bitrates[rate])

27
amodem/tests/utils.py Normal file
View File

@@ -0,0 +1,27 @@
import numpy as np
class IIR:
def __init__(self, b, a):
self.b = np.array(b) / a[0]
self.a = np.array(a[1:]) / a[0]
self.x_state = [0] * len(self.b)
self.y_state = [0] * (len(self.a) + 1)
def __call__(self, x):
x_, y_ = self.x_state, self.y_state
for v in x:
x_ = [v] + x_[:-1]
y_ = y_[:-1]
num = np.dot(x_, self.b)
den = np.dot(y_, self.a)
y = num - den
y_ = [y] + y_
yield y
self.x_state, self.y_state = x_, y_
def lfilter(b, a, x):
f = IIR(b=b, a=a)
y = list(f(x))
return np.array(y)

View File

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

2
scripts/autocalib.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
amodem-cli send -vv -c auto | amodem-cli recv -vv -c auto

2
scripts/play.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
aplay -f S16_LE -c 1 -r 32000 $*

View File

@@ -1,6 +1,16 @@
#!/usr/bin/env python
"""Script that exposes pylab's spectogram plotting
capabilities to the command line. It implements this
for amodem.config Configurations.
"""
import pylab
import numpy as np
from amodem import common
from amodem.config import Configuration
import sys
def spectrogram(t, x, Fs, NFFT=256):
@@ -11,10 +21,8 @@ def spectrogram(t, x, Fs, NFFT=256):
pylab.specgram(x, NFFT=NFFT, Fs=Fs, noverlap=NFFT/2,
cmap=pylab.cm.gist_heat)
if __name__ == '__main__':
import sys
from amodem import common
from amodem.config import Configuration
def main():
config = Configuration()
for fname in sys.argv[1:]:
@@ -25,3 +33,7 @@ if __name__ == '__main__':
spectrogram(t, x, config.Fs)
pylab.show()
if __name__ == '__main__':
main()

15
scripts/profile.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -x -u
SRC=`tempfile`
DST=`tempfile`
AUDIO=`tempfile`
dd if=/dev/urandom of=$SRC bs=1kB count=1000
export BITRATE=80
time python -m cProfile -o send.prof amodem-cli send -l- -vv -i $SRC -o $AUDIO 2> send.log
echo -e "sort cumtime\nstats" | python -m pstats send.prof > send.prof.txt
time python -m cProfile -o recv.prof amodem-cli recv -l- -vv -i $AUDIO -o $DST 2> recv.log
echo -e "sort cumtime\nstats" | python -m pstats recv.prof > recv.prof.txt
diff $SRC $DST || echo "ERROR!"
rm $SRC $DST $AUDIO

View File

@@ -1,18 +1,17 @@
#!/usr/bin/env python
"""Script that records audio through an interface
and stores it into an amodem.config Configuration.
"""
import argparse
from amodem import audio
from amodem.config import Configuration
def main():
p = argparse.ArgumentParser()
p.add_argument('-l', '--audio-library', default='libportaudio.so')
p.add_argument('filename')
args = p.parse_args()
def run(args):
config = Configuration()
with open(args.filename, 'wb') as dst:
print dst
interface = audio.Interface(config=config)
with interface.load(args.audio_library):
src = interface.recorder()
@@ -21,8 +20,16 @@ def main():
dst.write(src.read(size))
if __name__ == '__main__':
def main():
p = argparse.ArgumentParser()
p.add_argument('-l', '--audio-library', default='libportaudio.so')
p.add_argument('filename')
try:
main()
run(args=p.parse_args())
except KeyboardInterrupt:
pass
return
if __name__ == '__main__':
main()

2
scripts/record.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
arecord -f S16_LE -c 1 -r 32000 $*

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env python
from amodem.sampling import resample
import sys
resample(src=sys.stdin, dst=sys.stdout, df=float(sys.argv[1]))

22
scripts/resample.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Script that exposes the amodem.resample() function
to the command line, taking parameters via standard
inputs and returning results via standard outputs.
"""
from amodem.sampling import resample
import argparse
import sys
def main():
p = argparse.ArgumentParser()
p.add_argument('df', type=float)
args = p.parse_args()
resample(src=sys.stdin, dst=sys.stdout, df=args.df)
if __name__ == '__main__':
main()

View File

@@ -2,7 +2,6 @@
from setuptools import setup
from setuptools.command.test import test as TestCommand
class PyTest(TestCommand):
def finalize_options(self):
@@ -12,35 +11,34 @@ 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.8",
description="Audio Modem Communication Library",
author="Roman Zeyde",
author_email="roman.zeyde@gmail.com",
license="MIT",
url="http://github.com/romanz/amodem",
name='amodem',
version='1.15.2',
description='Audio Modem Communication Library',
author='Roman Zeyde',
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'],
platforms=['POSIX'],
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Networking",
"Topic :: Communications",
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: MIT License',
'Operating System :: POSIX',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
],
scripts=['amodem-cli'],
entry_points={'console_scripts': ['amodem = amodem.__main__:_main']},
)

12
tox.ini
View File

@@ -1,15 +1,15 @@
[tox]
envlist = py27,py34
envlist = py3
[testenv]
deps=
pytest
mock
pep8
pycodestyle
coverage
pylint
six
six
commands=
pep8 amodem/ scripts/ tests/ amodem-cli
pylint --extension-pkg-whitelist=numpy --report=no amodem
coverage run --source amodem/ -m py.test tests/
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 report