Compare commits

...

279 Commits
v1.2 ... v1.13

Author SHA1 Message Date
Roman Zeyde
e637e701df bump version 2015-09-19 12:13:19 +03:00
Roman Zeyde
5be6684fa6 travis: upgrade pytest version for Python 3.5 2015-09-19 12:05:58 +03:00
Roman Zeyde
0876be18e4 Merge branch 'py35' 2015-09-19 12:02:24 +03:00
Roman Zeyde
3b8f913fcb setup: Python 3.5 is supported 2015-09-19 12:01:52 +03:00
Roman Zeyde
45d4ccae76 use --assert=plain on Travis 2015-09-19 11:58:30 +03:00
Roman Zeyde
7cb05aaaf7 travis: test under Python 3.5 2015-09-19 11:45:50 +03:00
Roman Zeyde
3911f16bd7 tox: fix indent to tabs 2015-09-18 21:07:43 +03:00
Roman Zeyde
b5f8e07ae2 recv: define integration gain as member variable 2015-08-16 18:30:30 +03:00
Roman Zeyde
835841bf2e test_calib: test frequency change case 2015-08-16 18:21:40 +03:00
Roman Zeyde
c70b3c9dc7 calib: remove AttributeHolder 2015-08-16 17:55:40 +03:00
Roman Zeyde
544dd28ddd common: add docstrings 2015-08-16 17:55:40 +03:00
Roman Zeyde
c887dbf4e6 README: use screencasts instead of videos 2015-08-15 08:35:22 +03:00
Roman Zeyde
bf6282127c docs: link to readthedocs.org 2015-08-14 13:07:04 +03:00
Roman Zeyde
65f2559a19 README: fix whitespace 2015-08-13 10:07:07 +03:00
Roman Zeyde
1c6f8894a5 setup.py: fix pytest invocation 2015-08-11 10:43:28 +03:00
Roman Zeyde
c19d11744f remove requirements.txt 2015-08-11 10:39:00 +03:00
Roman Zeyde
4d60cac7ed version: meta-bump due to PyPI problems 2015-08-07 17:44:45 +03:00
Roman Zeyde
a3adda625b gitignore: ignore backup files 2015-07-29 18:03:56 +03:00
Roman Zeyde
a44f55a608 travis: fix tests runner 2015-07-29 17:59:09 +03:00
Roman Zeyde
e0a38bf5d7 README: simplify description 2015-07-29 17:44:47 +03:00
Roman Zeyde
6b67721374 tox: update to run on updated tests 2015-07-29 17:44:46 +03:00
Roman Zeyde
20efa6a688 tests: remove unused code 2015-07-29 17:44:46 +03:00
Roman Zeyde
327a7f9d0f tests: move into amodem package 2015-07-29 17:44:46 +03:00
Roman Zeyde
7b0ba1714f Merge pull request #18 from anduck/patch-1
changed input and output vice versa
2015-07-28 08:58:01 +03:00
anduck
ec76a1394c changed input and output vice versa 2015-07-27 22:45:34 +03:00
Roman Zeyde
bf7b59db11 bump version 2015-07-17 08:27:30 +03:00
Roman Zeyde
f51cf8c4db config: shorten start & stop silence periods 2015-07-17 08:22:49 +03:00
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
Roman Zeyde
b39334bfe8 recv: log SNR during demodulation 2015-02-04 18:08:58 +02:00
Roman Zeyde
1c13671a4c README: add version from PyPI 2015-02-04 18:08:58 +02:00
Roman Zeyde
6a35820155 async: remove AsyncWriter
we are not expecting real-time problems on the sender's side.
2015-02-04 18:08:57 +02:00
Roman Zeyde
1b64e2874a config: add more bitrates 2015-02-04 18:08:57 +02:00
Roman Zeyde
537db23b34 README: separate PyPI and GitHub versions. 2015-02-04 09:09:16 +02:00
Roman Zeyde
ef8a75f10e main: refactor send and recv 2015-02-04 08:16:12 +02:00
Roman Zeyde
53559ff8df recv: fix pylint warning 2015-02-03 17:44:06 +02:00
Roman Zeyde
1988144752 async: fix pylint warnings 2015-02-03 17:42:53 +02:00
Roman Zeyde
1d5d564f4d stream: use async I/O to avoid real-time problems. 2015-02-03 16:52:48 +02:00
Roman Zeyde
5b6d1881ab audio: don't specify framesPerBuffer.
from "Pa_OpenStream" documentation:

 @param framesPerBuffer The number of frames passed to the stream callback
 function, or the preferred block granularity for a blocking read/write stream.
 The special value paFramesPerBufferUnspecified (0) may be used to request that
 the stream callback will receive an optimal (and possibly varying) number of
 frames based on host requirements and the requested latency settings.
 Note: With some host APIs, the use of non-zero framesPerBuffer for a callback
 stream may introduce an additional layer of buffering which could introduce
 additional latency. PortAudio guarantees that the additional latency
 will be kept to the theoretical minimum however, it is strongly recommended
 that a non-zero framesPerBuffer value only be used when your algorithm
 requires a fixed number of frames per stream callback.
2015-02-03 09:54:04 +02:00
Roman Zeyde
221c7c03b9 recv: fix reporting 2015-02-03 08:35:02 +02:00
Roman Zeyde
0e9d8c04da audio: refactor naming and I/O wait report 2015-02-03 08:35:02 +02:00
Roman Zeyde
66b8e377ac stream: move Dumper class from recv and use it in calibration too 2015-02-03 08:35:02 +02:00
Roman Zeyde
bfbb7f3588 Revert "rename README (remove .md extension)"
This reverts commit 32e01dd4e0.
2015-01-25 20:13:50 +02:00
Roman Zeyde
32e01dd4e0 rename README (remove .md extension) 2015-01-25 20:09:20 +02:00
Roman Zeyde
200ec3b0c2 scripts: add recording script for audio debugging. 2015-01-25 20:09:10 +02:00
Roman Zeyde
6a37dd74ec bump version. 2015-01-24 07:59:29 +02:00
Roman Zeyde
dc8e876f13 README: fix typos 2015-01-23 15:32:30 +02:00
Roman Zeyde
a208151fbb detect: more precision on ppm 2015-01-23 14:17:35 +02:00
Roman Zeyde
ef40b498bb Revert "travis: add pylint" - pylint is broken on travis.org's Python 2.6
This reverts commit 33385b294e.
2015-01-21 20:47:33 +02:00
Roman Zeyde
db3ae11bc0 equalizer: use low-level random API, for Python 2<->3 interoperability
Random.randrange() behaviour was changed at v3.2, see:
https://bugs.python.org/issue9025
2015-01-21 20:38:44 +02:00
Roman Zeyde
4c4027b84a config: increate sample buffer to 256ms. 2015-01-20 10:21:02 +02:00
Roman Zeyde
4193fa88d6 cli: format -> fmt 2015-01-20 10:19:58 +02:00
Roman Zeyde
5275f25d14 cli: simplify stdin/stdout logic 2015-01-20 10:19:45 +02:00
Roman Zeyde
33385b294e travis: add pylint 2015-01-20 10:16:47 +02:00
Roman Zeyde
128675955b tox: whitelist numpy 2015-01-20 10:04:57 +02:00
Roman Zeyde
c4d583612b calib: add tests for automation 2015-01-20 10:00:30 +02:00
Roman Zeyde
779ba09c46 equalizer: add remark. 2015-01-19 20:54:16 +02:00
Roman Zeyde
7e8f1e8994 recv: fix subplot on 1x2 figure 2015-01-19 20:50:46 +02:00
Roman Zeyde
3450c2c570 equalizer: use constant symbols' prefix (for analog debugging) 2015-01-19 20:45:40 +02:00
Roman Zeyde
95ed9e0eda cli: use "-c auto" for auto-calibration
currently, support only PulseAudio
2015-01-19 16:15:08 +02:00
Roman Zeyde
95bbfbfe98 cli: handle Ctrl+C during calibration. 2015-01-19 10:59:08 +02:00
Roman Zeyde
066c27843e calib: print special message for frequency changes. 2015-01-19 10:56:15 +02:00
Roman Zeyde
323145c44b audio: fix UTs 2015-01-17 21:07:27 +02:00
Roman Zeyde
f523b7579c calib: allow only KeyboardInterrupt exception. 2015-01-17 19:11:15 +02:00
Roman Zeyde
3b6a54150f audio: split library load from c-tor 2015-01-17 19:01:49 +02:00
Roman Zeyde
2d202cf587 cli: use automaitic calibration with PulseAudio 2015-01-17 18:30:43 +02:00
Roman Zeyde
ee7db32418 audio: fix for Python3 2015-01-17 18:27:10 +02:00
Roman Zeyde
81165799fd calib: fix logging messages 2015-01-17 18:27:01 +02:00
Roman Zeyde
c7251b641e calib: fix UTs 2015-01-17 16:03:41 +02:00
Roman Zeyde
9036069323 cli: add aliases for PulseAudio volume setting 2015-01-17 12:51:10 +02:00
Roman Zeyde
ff8427f5f9 calib: automatic microphone gain calibration
$ amodem-cli send -vv -c 'pactl set-sink-volume @DEFAULT_SINK@'
will set speaker level to 100%.

$ amodem-cli recv -vv -c 'pactl set-source-volume @DEFAULT_SOURCE@'
will use "binary search", to find the best microphone gain.
2015-01-17 12:45:25 +02:00
Roman Zeyde
35487966d8 audio: add PortAudio version string for debugging 2015-01-17 09:04:34 +02:00
Roman Zeyde
5c4849be1c detect: remove unused code
since we now compensate for frequency drift, these estimates may be too
pessimistic.
2015-01-16 12:46:29 +02:00
Roman Zeyde
cbcf2860b9 README: add a video tutorial 2015-01-16 12:39:14 +02:00
Roman Zeyde
d03205fe02 update README for newer version. 2015-01-16 11:53:47 +02:00
Roman Zeyde
603fc75db4 amodem: bump version 2015-01-16 11:36:55 +02:00
Roman Zeyde
2727ff014a amodem-cli: fix compression logic 2015-01-16 11:32:22 +02:00
Roman Zeyde
fe1714a0bc common: fix v2.6 formatting issue 2015-01-16 11:05:04 +02:00
Roman Zeyde
cb8ce9e8ec common: fix __repr__ on AttributeHolder 2015-01-16 10:51:11 +02:00
Roman Zeyde
b4dc0922eb equalizer: back to long training sequence 2015-01-16 10:31:42 +02:00
Roman Zeyde
36f52f9346 calib: make work with large frequency errors
(tested up to 0.2%)
2015-01-16 10:28:11 +02:00
Roman Zeyde
babd4a5438 common: move AttributeHolder 2015-01-16 09:50:29 +02:00
Roman Zeyde
904966365f recv: use finally for exit code 2015-01-15 18:22:47 +02:00
Roman Zeyde
aa0dd2c2c8 fix pep8 2015-01-15 18:22:47 +02:00
Roman Zeyde
1da258ebf8 detect: refactor receiver for large frequency drifts (~0.1%) 2015-01-15 18:22:47 +02:00
Roman Zeyde
5401206178 higher precision progress logging 2015-01-15 08:24:01 +02:00
Roman Zeyde
9b6160ec43 bump version due to calibration change
(it could be shorter)
2015-01-14 12:41:01 +02:00
Roman Zeyde
04b0142955 recv: handle higher frequency drifts. 2015-01-13 18:37:31 +02:00
Roman Zeyde
9dbfcaa870 equalizer: use shorter pilot tone. 2015-01-13 18:32:15 +02:00
Roman Zeyde
af59eb5bdf transfer: test larger frequency errors 2015-01-13 13:02:22 +02:00
Roman Zeyde
e4267f236b dsp: remove unused code 2015-01-11 18:14:37 +02:00
Roman Zeyde
cfc6de9eb0 calib: return attribute holders from receiver's calibration. 2015-01-11 18:09:53 +02:00
Roman Zeyde
2d890339e2 recv: add audio dumping option (for debugging) 2015-01-11 18:01:39 +02:00
Roman Zeyde
2ee79870c5 sampling: add documentation 2015-01-10 11:55:24 +02:00
Roman Zeyde
2bb4956439 audio: fix GetDefault???Device API call 2015-01-10 11:55:12 +02:00
Roman Zeyde
fd8dc1d8b7 train: remove unneeded file 2015-01-09 21:57:22 +02:00
Roman Zeyde
5bf2d0f566 README: a few fixes. 2015-01-09 20:01:34 +02:00
Roman Zeyde
b91f51df12 README: remove downloads badge 2015-01-09 19:58:28 +02:00
Roman Zeyde
fcd58d404d README: update a bit. 2015-01-09 19:57:14 +02:00
Roman Zeyde
4ca1cdf23a tox: fix indentation 2015-01-09 19:51:31 +02:00
Roman Zeyde
8e8d43b041 cli: add zip compression option 2015-01-09 19:50:04 +02:00
Roman Zeyde
c2a4bfbd5e README: update modem info. 2015-01-08 18:24:26 +02:00
Roman Zeyde
ae7d742ee4 config: add faster configurations 2015-01-08 18:07:01 +02:00
Roman Zeyde
c2c1b89a0e audio: add debug flag for PortAudio API 2015-01-08 18:06:50 +02:00
Roman Zeyde
750eb5428f tox: add pylint
ignore numpy "no-member" errors
2015-01-08 14:35:01 +02:00
Roman Zeyde
15f330330c calib: refactor receiver. 2015-01-08 14:25:53 +02:00
Roman Zeyde
96a1abb714 recv: initialize variables at c-tor 2015-01-08 10:35:25 +02:00
Roman Zeyde
a83888ff02 remove unused arguments 2015-01-08 10:35:10 +02:00
Roman Zeyde
a866301774 equalizer: move training to module scope 2015-01-08 09:43:22 +02:00
Roman Zeyde
3dcd9f4ccc equalizer: remove unused code 2015-01-08 09:40:15 +02:00
Roman Zeyde
3b1d193b0b framing: refactor a bit 2015-01-08 09:39:56 +02:00
Roman Zeyde
318a0644de use list comprehensions instead of map() 2015-01-08 09:24:09 +02:00
Roman Zeyde
807cbc31a2 recv: split sampler update 2015-01-08 09:22:19 +02:00
Roman Zeyde
004ad2403f recv: split training verification 2015-01-07 16:22:19 +02:00
Roman Zeyde
a1ad9ff32c __init__: remove unused imports 2015-01-07 13:53:36 +02:00
Roman Zeyde
f086bbfdeb audio: use specified config 2015-01-06 18:01:48 +02:00
Roman Zeyde
bd329c19d0 audio: add mocking UT 2015-01-06 17:34:26 +02:00
Roman Zeyde
7f9e84dd02 setup.py: remove pyaudio from requirements 2015-01-06 14:47:31 +02:00
Roman Zeyde
93f0396bc5 fixup! calib: use shorter signals. 2015-01-06 14:46:47 +02:00
Roman Zeyde
75dd7d28c9 audio: use ctypes to access PortAudio API directly 2015-01-06 14:46:37 +02:00
Roman Zeyde
b3510c18b3 calib: use shorter signals. 2015-01-06 12:39:13 +02:00
Roman Zeyde
a30ee7f92c setup.py: add support for Python 2.6 2015-01-05 14:23:28 +02:00
Roman Zeyde
3901b32cc5 framing: fix bytes handling for Python 2.6 2015-01-05 14:12:49 +02:00
Roman Zeyde
1728bba109 detect: remove for's else statement 2014-12-31 14:37:03 +02:00
Roman Zeyde
28bac11e9a detect: split waiting logic 2014-12-31 14:24:18 +02:00
Roman Zeyde
3ae72091cb tox: update to test with coverage 2014-12-31 14:18:51 +02:00
Roman Zeyde
72ee5147f4 stream: move defaults to class variables 2014-12-31 14:04:38 +02:00
Roman Zeyde
bb6e262b57 sampling: remove unused import 2014-12-31 14:00:17 +02:00
Roman Zeyde
dfd19df01f equalizer: remove unused variables 2014-12-31 13:59:41 +02:00
Roman Zeyde
43ed34ede5 framing: fix PEP8 2014-12-31 13:32:11 +02:00
Roman Zeyde
ffdfce78fc travis debugging 2014-12-31 13:27:09 +02:00
Roman Zeyde
5f664e5944 common: enumerate -> index 2014-12-31 12:46:56 +02:00
Roman Zeyde
3e7b61205c audio: format -> fmt 2014-12-31 12:46:44 +02:00
Roman Zeyde
cfca2d6cb5 equalizer: remove unused variables 2014-12-31 12:38:55 +02:00
Roman Zeyde
4cfe1711b9 framing: remove bitarray dependancy 2014-12-31 12:36:58 +02:00
Roman Zeyde
4b6a7fcf1c README: add landscape badge 2014-12-31 11:38:08 +02:00
Roman Zeyde
477013fcdd recv: remove saturation detection 2014-12-31 11:03:49 +02:00
Roman Zeyde
c38208e10b calib: fix UT 2014-12-31 10:58:54 +02:00
Roman Zeyde
c37cf741bc remove unused code. 2014-12-30 18:24:57 +02:00
Roman Zeyde
2c408907c4 framing: fix UT 2014-12-30 17:00:18 +02:00
Roman Zeyde
ad5e688547 requirements: use git repo for pyaudio 2014-12-30 16:37:50 +02:00
Roman Zeyde
11279e26a6 README: update installation info and other issues 2014-12-30 16:37:36 +02:00
Roman Zeyde
1e09e4961e add main logger to package 2014-12-30 16:29:04 +02:00
Roman Zeyde
2430588077 cli: exit code should reflect success status 2014-12-30 09:45:31 +02:00
Roman Zeyde
d5b18f922c send: return True on success 2014-12-30 09:41:58 +02:00
Roman Zeyde
e7bcf5cbe0 framing: use smaller frames, for faster failure 2014-12-30 09:41:47 +02:00
Roman Zeyde
1d326304e1 bump version 2014-12-29 22:05:46 +02:00
Roman Zeyde
1f0363941d tox: add mock for tests 2014-12-29 21:40:26 +02:00
Roman Zeyde
5988586c08 tox: install pyaudio from git repo 2014-12-29 21:35:38 +02:00
Roman Zeyde
e2ed9915ee travis: install pyaudio from git repo 2014-12-29 21:29:33 +02:00
Roman Zeyde
bc33bc1428 travis: use portaudio-dev package to build pyaudio 2014-12-29 21:26:54 +02:00
Roman Zeyde
f3409b8638 add pyaudio package to travis 2014-12-29 18:37:36 +02:00
Roman Zeyde
4d75dba0bc switch to PyAudio package for portability 2014-12-29 17:54:42 +02:00
Roman Zeyde
fbd34844cf main: use amodem package for recv.main() and send.main() API 2014-12-28 16:46:47 +02:00
Roman Zeyde
0ae80e6d8b setup.py: add support for Python 3.2 2014-12-27 16:25:56 +02:00
Roman Zeyde
8557271da7 fix requirements.txt 2014-12-27 16:25:34 +02:00
Roman Zeyde
29ec1c4864 README: fix -vv flags position 2014-12-27 12:39:31 +02:00
Roman Zeyde
13bedf50a4 README: add a few more badges. 2014-12-27 12:37:08 +02:00
Roman Zeyde
a73fddf988 README: add a few more badges. 2014-12-27 12:36:21 +02:00
Roman Zeyde
3c6ec642eb travis: add pep8 package 2014-12-27 12:17:55 +02:00
Roman Zeyde
9696e8796a tox: remove Python 3.3 2014-12-27 12:12:44 +02:00
Roman Zeyde
2a4297c5fc add PEP8 to travis 2014-12-27 12:12:24 +02:00
Roman Zeyde
ca93de06af PEP8 fixes for tests 2014-12-27 12:11:51 +02:00
Roman Zeyde
f88820e9c3 PEP8 fixes 2014-12-27 12:06:01 +02:00
Roman Zeyde
2f90ac7e46 split carrier detection into detect.py 2014-12-27 09:46:09 +02:00
Roman Zeyde
0d29eecaa2 travis: add Python 3.2 2014-12-27 09:27:46 +02:00
Roman Zeyde
67b69a62ec dsp: don't use dict comprehension 2014-12-26 22:45:57 +02:00
Roman Zeyde
f532895dd4 travis: use new infrastructure 2014-12-26 22:41:56 +02:00
Roman Zeyde
ec5b5fa4c0 fix string formatting 2014-12-26 22:18:35 +02:00
Roman Zeyde
b9dc85e857 amodem-cli: move -v/-q flags to subparsers. 2014-12-25 11:08:38 +02:00
Roman Zeyde
a1f58436d2 recv: add timeout for carrier waiting 2014-12-23 17:54:24 +02:00
Roman Zeyde
8378a273c3 ashow: fix configuration usage 2014-12-23 17:44:50 +02:00
Roman Zeyde
f4d8c8a06e README: add note regarding bitrate selection. 2014-12-03 09:58:47 +02:00
Roman Zeyde
353f8b8211 add donation address 2014-12-03 09:16:05 +02:00
Roman Zeyde
ca14f0862b configuration should be specified explicitly 2014-12-02 22:18:24 +02:00
Roman Zeyde
6bf0d4eeda CLI: handle missing argcomplete package 2014-11-30 12:48:57 +02:00
Roman Zeyde
3985aa4f34 bump version 2014-11-30 12:18:30 +02:00
Roman Zeyde
ea5e577953 recv: fix detection logic a bit 2014-11-14 16:53:33 +02:00
Roman Zeyde
b23a38295b update README.md with apt-get instructions 2014-11-13 11:46:37 +02:00
Roman Zeyde
75b990473a update README.md with installation from GitHub 2014-11-13 11:41:52 +02:00
Roman Zeyde
f4f742a7a4 config: simplify symbols' constellation
use rectangular QAM for simplicity
2014-11-11 09:06:15 +02:00
Roman Zeyde
da5e971d94 test_audio: improve coverage 2014-11-09 19:13:07 +02:00
Roman Zeyde
c84e081b1c amodem-cli: fix description logging 2014-11-09 17:37:18 +02:00
Roman Zeyde
6b1e39f48f calib: allow lower coherency 2014-11-09 17:27:34 +02:00
Roman Zeyde
ceb826728a don't use global configuration 2014-11-09 17:27:34 +02:00
Roman Zeyde
c8f5924c12 move and rename CLI script 2014-11-02 09:28:43 +02:00
Roman Zeyde
9d754b04cf recv: increase lookahead a bit 2014-11-01 08:25:30 +02:00
Roman Zeyde
59435e44a5 config: use "AMODEM_" prefix for settings' update
allow easy dumping of configuration
2014-11-01 08:25:30 +02:00
Roman Zeyde
86848fec1a rename "show" script 2014-10-23 17:58:14 +03:00
Roman Zeyde
77078d6150 calib: remove unused code. 2014-10-23 17:49:39 +03:00
Roman Zeyde
1da1e22553 README: elaborate installation 2014-10-23 16:28:09 +03:00
Roman Zeyde
6f90289d6b README: fix typo. 2014-10-23 16:22:05 +03:00
Roman Zeyde
02b28fc87c recv: plot equalization filter 2014-10-23 15:58:17 +03:00
Roman Zeyde
7c334db8c4 README: update usage section 2014-10-23 09:51:53 +03:00
Roman Zeyde
61b0299bbb calib: remove unused code. 2014-10-23 09:38:10 +03:00
Roman Zeyde
3a59a54107 refactor calibration recv script 2014-10-21 10:42:13 +03:00
Roman Zeyde
6b483335e9 add arcomplete support 2014-10-21 10:35:48 +03:00
Roman Zeyde
4248a0f08a calib: pep8 2014-10-14 19:35:23 +03:00
Roman Zeyde
1196c2c25e setup.py: add Python 3.4 to supported list 2014-10-14 19:35:15 +03:00
56 changed files with 2177 additions and 1228 deletions

2
.gitignore vendored
View File

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

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

View File

@@ -1,16 +1,23 @@
sudo: false
language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "3.5"
install:
- pip install .
- pip install coveralls
- pip install pytest>=2.7.3 --upgrade
- pip install coveralls pep8 mock
script:
- cd tests
- coverage run --source=amodem -m py.test
- pep8 amodem/ scripts/
- echo "Hello World!" | amodem send -vv -l- -o- | amodem recv -vv -l- -i-
- coverage run --source=amodem --omit="*/__main__.py" -m py.test -vvs
after_success:
- coverage report
- coveralls

View File

@@ -1,94 +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)
# 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 file,
which is played to the sound card, using `aplay` Linux utility.
The receiver side uses `arecord` Linux utility to record the transmitted audio
to an audio file, 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: 64-QAM
- Carriers: (1,2,3,4,5,6,7,8) kHz
This way, modem achieves 48kpbs bitrate = 6.0 kB/s.
A simple CRC-32 checksum is used for data integrity verification on each 1KB data frame.
# Installation
Run the following command (will also download and install `numpy` and `bitarray` packages):
$ sudo pip install amodem
For graphs and visualization (optional), install:
$ sudo pip install matplotlib
# Calibration
Connect the audio cable between the sender and the receiver, and run the
following scripts:
- On the sender's side:
```
~/sender $ amodem send --calibrate
```
- On the receiver's side:
```
~/receiver $ amodem recv --calibrate
```
Increase the sender computer's output audio level, until the
received **amplitude** and **peak** values are not higher than 0.5,
while the **coherence** is 1.0 (to avoid saturation).
# Testing
- Prepare the sender (generate random binary data file to be sent):
```
~/sender $ dd if=/dev/urandom of=data.tx bs=125kB count=1 status=none
~/sender $ sha256sum data.tx
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.tx
```
- Start the receiver:
```
~/receiver $ amodem recv >data.rx
```
- Start the sender:
```
~/sender $ amodem send <data.tx
```
- After the receiver has finished, verify that the file's hash is the same:
```
~/receiver $ sha256sum data.rx
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.rx
```
# Visualization
Make sure that `matplotlib` package is installed, and run (at the receiver side):
```
~/receiver $ amodem recv --plot >data.rx
```

239
README.rst Normal file
View File

@@ -0,0 +1,239 @@
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://readthedocs.org/projects/amodem/badge/?version=latest
:target: http://amodem.readthedocs.org/en/latest/
:alt: Documentation
.. image:: https://img.shields.io/pypi/pyversions/amodem.svg
:target: https://pypi.python.org/pypi/amodem/
:alt: Python Versions
.. image:: https://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
.. image:: https://img.shields.io/pypi/dm/amodem.svg
:target: https://pypi.python.org/pypi/amodem/
:alt: Downloads
.. image:: https://badge.waffle.io/romanz/amodem.svg?label=ready&title=ready
:target: https://waffle.io/romanz/amodem
:alt: 'Ready'
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.
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 ``numpy`` and ``PortAudio v19`` packages are installed (on Debian)::
$ sudo apt-get install python-numpy portaudio19-dev
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
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>`_.
Visualization
-------------
Make sure that ``matplotlib`` package is installed, and run (at the receiver side)::
~/receiver $ amodem recv --plot -o data.rx

View File

@@ -0,0 +1,2 @@
import logging
log = logging.getLogger(__name__)

250
amodem/__main__.py Normal file
View File

@@ -0,0 +1,250 @@
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
from . import main, calib, audio, async
from .config import bitrates
from . import version
import os
import sys
import zlib
import logging
import argparse
# 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:
argcomplete = None
log = logging.getLogger('__name__')
bitrate = os.environ.get('BITRATE', 1)
config = bitrates.get(int(bitrate))
class Compressor(object):
def __init__(self, stream):
self.obj = zlib.compressobj()
log.info('Using zlib compressor')
self.stream = stream
def read(self, size):
while True:
data = self.stream.read(size)
if data:
result = self.obj.compress(data)
if not result: # compression is too good :)
continue # try again (since falsy data = EOF)
elif self.obj:
result = self.obj.flush()
self.obj = None
else:
result = '' # EOF marker
return result
class Decompressor(object):
def __init__(self, stream):
self.obj = zlib.decompressobj()
log.info('Using zlib decompressor')
self.stream = stream
def write(self, data):
self.stream.write(self.obj.decompress(bytes(data)))
def flush(self):
self.stream.write(self.obj.flush())
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 = '-'
if fname is None:
assert audio_interface is not None
if 'r' in mode:
s = audio_interface.recorder()
return async.AsyncReader(stream=s, bufsize=s.bufsize)
if 'w' in mode:
return audio_interface.player()
if fname == '-':
if 'r' in mode:
return _stdin
if 'w' in mode:
return _stdout
return open(fname, mode)
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@')
]
if args.calibrate == 'auto':
for c in volume_controllers:
if os.system(c['test']) == 0:
return c[args.command]
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()
# Modulator
sender = subparsers.add_parser(
'send', help='modulate binary data into audio signal.')
sender.add_argument(
'-i', '--input', help='input file (use "-" for stdin).')
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.set_defaults(
main=lambda config, args: main.send(
config, src=wrap(Compressor, args.src, args.zlib), dst=args.dst,
gain=args.gain
),
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_factory),
command='send'
)
# Demodulator
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.')
receiver.add_argument(
'-o', '--output', help='output file (use "-" for stdout).')
receiver.add_argument(
'-d', '--dump', type=FileType('wb'),
help='Filename to save recorded audio')
receiver.add_argument(
'--plot', action='store_true', default=False,
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.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_factory),
output_type=FileType('wb'),
command='recv'
)
calibration_help = ('Run calibration '
'(specify "auto" for automatic gain control)')
for sub in subparsers.choices.values():
sub.add_argument('-c', '--calibrate', nargs='?', default=False,
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', '--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')
if argcomplete:
argcomplete.autocomplete(p)
return p
class _Dummy(object):
def __enter__(self):
return self
def __exit__(self, *args):
pass
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.__doc__,
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()
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)
# Parsing and execution
log.debug(description)
args.pylab = None
if getattr(args, 'plot', False):
import pylab # pylint: disable=import-error
args.pylab = pylab
if args.audio_library == 'ALSA':
from . import alsa
interface = alsa.Interface(config)
elif args.audio_library == '-':
interface = _Dummy()
else:
interface = audio.Interface(config)
interface.load(args.audio_library)
with interface:
args.src = args.input_type(args.input)
args.dst = args.output_type(args.output)
try:
if args.calibrate is False:
args.main(config=config, args=args)
else:
args.calib(config=config, args=args)
finally:
args.src.close()
args.dst.close()
log.debug('Finished I/O')
if __name__ == '__main__':
_main()

65
amodem/alsa.py Normal file
View File

@@ -0,0 +1,65 @@
import subprocess
import logging
log = logging.getLogger(__name__)
class Interface(object):
RECORDER = 'arecord'
PLAYER = 'aplay'
def __init__(self, config):
self.config = config
rate = int(config.Fs)
bits_per_sample = config.bits_per_sample
assert bits_per_sample == 16
args = '-f S{0:d}_LE -c 1 -r {1:d} -T 100 -q -'
args = args.format(bits_per_sample, rate).split()
self.record_cmd = [self.RECORDER] + args
self.play_cmd = [self.PLAYER] + args
self.processes = []
def __enter__(self):
return self
def __exit__(self, *args):
for p in self.processes:
try:
p.wait()
except OSError:
log.warning('%s failed', p)
def launch(self, **kwargs):
log.debug('Launching subprocess: %s', kwargs)
p = subprocess.Popen(**kwargs)
self.processes.append(p)
return p
def recorder(self):
return Recorder(self)
def player(self):
return Player(self)
class Recorder(object):
def __init__(self, lib):
self.p = lib.launch(args=lib.record_cmd, stdout=subprocess.PIPE)
self.read = self.p.stdout.read
self.bufsize = 4096
def close(self):
self.p.kill()
class Player(object):
def __init__(self, lib):
self.p = lib.launch(args=lib.play_cmd, stdin=subprocess.PIPE)
self.write = self.p.stdin.write
def close(self):
self.p.stdin.close()
self.p.wait()

49
amodem/async.py Normal file
View File

@@ -0,0 +1,49 @@
import threading
import six # since `Queue` module was renamed to `queue` (in Python 3)
import logging
log = logging.getLogger()
class AsyncReader(object):
def __init__(self, stream, bufsize):
self.stream = stream
self.queue = six.moves.queue.Queue()
self.stop = threading.Event()
args = (stream, bufsize, self.queue, self.stop)
self.thread = threading.Thread(target=AsyncReader._thread,
args=args, name='AsyncReader')
self.thread.start()
self.buf = b''
@staticmethod
def _thread(src, bufsize, queue, stop):
total = 0
try:
log.debug('AsyncReader thread started')
while not stop.isSet():
buf = src.read(bufsize)
queue.put(buf)
total += len(buf)
log.debug('AsyncReader thread stopped (read %d bytes)', total)
except BaseException:
log.exception('AsyncReader thread failed')
queue.put(None)
def read(self, size):
while len(self.buf) < size:
buf = self.queue.get()
if buf is None:
raise IOError('cannot read from stream')
self.buf += buf
result = self.buf[:size]
self.buf = self.buf[size:]
return result
def close(self):
if self.stream is not None:
self.stop.set()
self.thread.join()
self.stream.close()
self.stream = None

131
amodem/audio.py Normal file
View File

@@ -0,0 +1,131 @@
import ctypes
import logging
import time
log = logging.getLogger(__name__)
class Interface(object):
def __init__(self, config, debug=False):
self.debug = bool(debug)
self.config = config
self.streams = []
self.lib = None
def load(self, name):
self.lib = ctypes.CDLL(name)
assert self._error_string(0) == b'Success'
version = self.call('GetVersionText', restype=ctypes.c_char_p)
log.info('%s loaded', version)
return self
def _error_string(self, code):
return self.call('GetErrorText', code, restype=ctypes.c_char_p)
def call(self, name, *args, **kwargs):
assert self.lib is not None
func_name = 'Pa_{0}'.format(name)
if self.debug:
log.debug('API: %s%s', name, args)
func = getattr(self.lib, func_name)
func.restype = kwargs.get('restype', self._error_check)
return func(*args)
def _error_check(self, res):
if res != 0:
raise Exception(res, self._error_string(res))
def __enter__(self):
self.call('Initialize')
return self
def __exit__(self, *args):
for s in self.streams:
s.close()
self.call('Terminate')
def recorder(self):
return Stream(self, config=self.config, read=True)
def player(self):
return Stream(self, config=self.config, write=True)
class Stream(object):
timer = time.time
class Parameters(ctypes.Structure):
_fields_ = [
('device', ctypes.c_int),
('channelCount', ctypes.c_int),
('sampleFormat', ctypes.c_ulong),
('suggestedLatency', ctypes.c_double),
('hostApiSpecificStreamInfo', ctypes.POINTER(None))
]
def __init__(self, interface, config, read=False, write=False):
self.interface = interface
self.stream = ctypes.POINTER(ctypes.c_void_p)()
self.user_data = ctypes.c_void_p(None)
self.stream_callback = ctypes.c_void_p(None)
self.bytes_per_sample = config.sample_size
self.latency = float(config.latency) # in seconds
self.bufsize = int(self.latency * config.Fs * self.bytes_per_sample)
assert config.bits_per_sample == 16 # just to make sure :)
read = bool(read)
write = bool(write)
assert read != write # don't support full duplex
direction = 'Input' if read else 'Output'
api_name = 'GetDefault{0}Device'.format(direction)
index = interface.call(api_name, restype=ctypes.c_int)
self.params = Stream.Parameters(
device=index, # choose default device
channelCount=1, # mono audio
sampleFormat=0x00000008, # 16-bit samples (paInt16)
suggestedLatency=self.latency,
hostApiSpecificStreamInfo=None)
self.interface.call(
'OpenStream',
ctypes.byref(self.stream),
ctypes.byref(self.params) if read else None,
ctypes.byref(self.params) if write else None,
ctypes.c_double(config.Fs),
ctypes.c_ulong(0), # (paFramesPerBufferUnspecified)
ctypes.c_ulong(0), # no flags (paNoFlag)
self.stream_callback,
self.user_data)
self.interface.streams.append(self)
self.interface.call('StartStream', self.stream)
self.start_time = self.timer()
self.io_time = 0
def close(self):
if self.stream:
self.interface.call('StopStream', self.stream)
self.interface.call('CloseStream', self.stream)
self.stream = None
def read(self, size):
assert size % self.bytes_per_sample == 0
buf = ctypes.create_string_buffer(size)
frames = ctypes.c_ulong(size // self.bytes_per_sample)
t0 = self.timer()
self.interface.call('ReadStream', self.stream, buf, frames)
t1 = self.timer()
self.io_time += (t1 - t0)
if self.interface.debug:
io_wait = self.io_time / (t1 - self.start_time)
log.debug('I/O wait: %.1f%%', io_wait * 100)
return buf.raw
def write(self, data):
data = bytes(data)
assert len(data) % self.bytes_per_sample == 0
buf = ctypes.c_char_p(data)
frames = ctypes.c_ulong(len(data) // self.bytes_per_sample)
self.interface.call('WriteStream', self.stream, buf, frames)

View File

@@ -1,66 +1,138 @@
from . import common
from . import dsp
from . import sampling
from . import stream
import numpy as np
import itertools
import logging
import sys
import subprocess
log = logging.getLogger(__name__)
from . import common
from . import config
from . import wave
CALIBRATION_SYMBOLS = int(1.0 * config.Fs)
ALLOWED_EXCEPTIONS = (IOError, KeyboardInterrupt)
def send(wave_play=wave.play):
t = np.arange(0, CALIBRATION_SYMBOLS) * config.Ts
signal = [np.sin(2 * np.pi * f * t) for f in config.frequencies]
signal = common.dumps(np.concatenate(signal))
p = wave_play(stdin=wave.sp.PIPE)
fd = p.stdin
try:
while True:
fd.write(signal)
except ALLOWED_EXCEPTIONS:
pass
finally:
p.kill()
def volume_controller(cmd):
def controller(level):
assert 0 < level <= 1
percent = 100 * level
args = '{0} {1:.0f}%'.format(cmd, percent)
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)
FRAME_LENGTH = 100 * config.Nsym
def send(config, dst, volume_cmd=None, gain=1.0, limit=None):
volume_ctl = volume_controller(volume_cmd)
volume_ctl(1.0) # full scale output volume
def recorder(process):
frame_size = int(wave.bytes_per_sample * FRAME_LENGTH)
fd = process.stdout
try:
while True:
data = fd.read(frame_size)
if len(data) < frame_size:
return
data = common.loads(data)
data = data - np.mean(data)
yield data
except ALLOWED_EXCEPTIONS:
pass
finally:
process.kill()
calibration_symbols = int(1.0 * config.Fs)
t = np.arange(0, calibration_symbols) * config.Ts
signals = [gain * np.sin(2 * np.pi * f * t) for f in config.frequencies]
signals = [common.dumps(s) for s in signals]
def recv(wave_record=wave.record, log=sys.stdout.write):
t = np.arange(0, FRAME_LENGTH) * config.Ts
carriers = [np.exp(2j * np.pi * f * t) for f in config.frequencies]
carriers = np.array(carriers) / (0.5 * len(t))
for signal in itertools.islice(itertools.cycle(signals), limit):
dst.write(signal)
def frame_iter(config, src, frame_length):
frame_size = frame_length * config.Nsym * config.sample_size
omegas = 2 * np.pi * np.array(config.frequencies) / config.Fs
while True:
data = src.read(frame_size)
if len(data) < frame_size:
return
data = common.loads(data)
frame = data - np.mean(data)
sampler = sampling.Sampler(frame)
symbols = dsp.Demux(sampler, omegas, config.Nsym)
symbols = np.array(list(symbols))
coeffs = np.mean(np.abs(symbols) ** 2, axis=0) ** 0.5
for frame in recorder(wave_record(stdout=wave.sp.PIPE)):
peak = np.max(np.abs(frame))
coeffs = np.dot(carriers, frame)
max_index = np.argmax(np.abs(coeffs))
max_coeff = coeffs[max_index]
total = np.sqrt(np.dot(frame, frame) / (0.5 * len(frame)))
yield coeffs, peak, total
def detector(config, src, frame_length=200):
errors = ['weak', 'strong', 'noisy']
for coeffs, peak, total in frame_iter(config, src, frame_length):
max_index = np.argmax(coeffs)
freq = config.frequencies[max_index]
rms = abs(max_coeff)
total = np.sqrt(np.dot(frame, frame) / (0.5 * len(t)))
rms = abs(coeffs[max_index])
coherency = rms / total
log(fmt.format(freq / 1e3, 100 * coherency, rms, total, peak))
flags = [total > 0.1, peak < 1.0, coherency > 0.99]
fmt = '{:4.0f} kHz @ {:6.2f}% : RMS = {:.4f}, Total = {:.4f}, Peak = {:.4f}\n'
success = all(flags)
if success:
msg = 'good signal'
else:
msg = 'too {0} signal'.format(errors[flags.index(False)])
yield dict(
freq=freq, rms=rms, peak=peak, coherency=coherency,
total=total, success=success, msg=msg
)
def volume_calibration(result_iterator, volume_ctl):
min_level = 0.01
max_level = 1.0
level = 0.5
step = 0.25
target_level = 0.4 # not too strong, not too weak
iters_per_update = 10 # update every 2 seconds
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
level = level + step * sign
level = min(max(level, min_level), max_level)
step = step * 0.5
volume_ctl(level) # should run "before" first actual iteration
if index > 0: # skip dummy (first result)
yield result
def iter_window(iterable, size):
block = []
while True:
item = next(iterable)
block.append(item)
block = block[-size:]
if len(block) == size:
yield block
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)
for _prev, curr, _next in iter_window(result_iterator, size=3):
# don't log errors during frequency changes
if _prev['success'] and _next['success']:
if _prev['freq'] != _next['freq']:
if not curr['success']:
curr['msg'] = 'frequency change'
yield curr
def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
fmt = '{freq:6.0f} Hz: {msg:20s}'
log.info('verbose: %s', verbose)
if verbose:
fields = ['total', 'rms', 'coherency', 'peak']
fmt += ', '.join('{0}={{{0}:.4f}}'.format(f) for f in fields)
for state in recv_iter(config, src, volume_cmd, dump_audio):
log.info(fmt.format(**state))

View File

@@ -1,3 +1,6 @@
''' Common package functionality.
'''
import itertools
import numpy as np
@@ -5,37 +8,28 @@ import logging
log = logging.getLogger(__name__)
scaling = 32000.0 # out of 2**15
SATURATION_THRESHOLD = (2**15 - 1) / scaling
class SaturationError(ValueError):
pass
def check_saturation(x):
peak = np.max(np.abs(x))
if peak >= SATURATION_THRESHOLD:
raise SaturationError(peak)
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, n=1):
def dumps(sym):
''' Dump signal to memory buffer. '''
sym = sym.real * scaling
sym = sym.astype('int16')
data = sym.tostring()
return data * n
return sym.astype('int16').tostring()
def iterate(data, size, func=None, truncate=True, enumerate=False):
def iterate(data, size, func=None, truncate=True, index=False):
''' Iterate over a signal, taking each time *size* elements. '''
offset = 0
data = iter(data)
@@ -48,11 +42,14 @@ def iterate(data, size, func=None, truncate=True, enumerate=False):
done = True
result = func(buf) if func else np.array(buf)
yield (offset, result) if enumerate else result
yield (offset, result) if index else result
offset += size
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]
@@ -62,23 +59,26 @@ def split(iterable, n):
def icapture(iterable, result):
''' Appends each yielded item to result. '''
for i in iter(iterable):
result.append(i)
yield i
def take(iterable, n):
''' Take n elements from iterable, and return them as a numpy array. '''
return np.array(list(itertools.islice(iterable, n)))
# "Python 3" zip re-implementation for Python 2
def izip(iterables):
''' "Python 3" zip re-implementation for Python 2. '''
iterables = [iter(iterable) for iterable in iterables]
while True:
yield tuple([next(iterable) for iterable in iterables])
class Dummy(object):
''' Dummy placeholder object for testing and mocking. '''
def __getattr__(self, name):
return self

View File

@@ -1,42 +1,88 @@
Fs = 32000.0 # sampling frequency [Hz]
Tsym = 0.001 # symbol duration [seconds]
Nfreq = 8 # number of frequencies used
Npoints = 64
F0 = 1e3
# Update default configuration from environment variables
settings = {k: v for k, v in locals().items() if not k.startswith('_')}
import os
for k in settings.keys():
v = settings[k]
settings[k] = type(v)(os.environ.get(k, v))
locals().update(settings)
import numpy as np
Ts = 1.0 / Fs
Fsym = 1 / Tsym
frequencies = F0 + np.arange(Nfreq) * Fsym
carrier_index = 0
Fc = frequencies[carrier_index]
Tc = 1.0 / Fc
# Hexagonal symbol constellation (optimal "sphere packing")
I = np.arange(-Npoints, Npoints+1)
imag_factor = np.exp(1j * np.pi / 3.0)
offset = 0.5
symbols = [(x + y*imag_factor + offset) for x in I for y in I]
symbols.sort(key=lambda z: (z*z.conjugate()).real)
symbols = np.array(symbols[:Npoints])
symbols = symbols / np.max(np.abs(symbols))
class Configuration(object):
Fs = 32000.0 # sampling frequency [Hz]
Tsym = 0.001 # symbol duration [seconds]
Npoints = 64
frequencies = [1e3, 8e3] # use 1..8 kHz carriers
Nsym = int(Tsym / Ts)
baud = int(1/Tsym)
# audio config
bits_per_sample = 16
latency = 0.1
bits_per_symbol = np.log2(Npoints)
bits_per_baud = bits_per_symbol * Nfreq
modem_bps = baud * bits_per_baud
carriers = np.array([
np.exp(2j * np.pi * f * np.arange(0, Nsym) * Ts) for f in frequencies
])
# sender config
silence_start = 0.25
silence_stop = 0.25
# 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.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]
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([
np.exp(2j * np.pi * f * np.arange(0, self.Nsym) * self.Ts)
for f in self.frequencies
])
# QAM constellation
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(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]),
}
def fastest():
return bitrates[max(bitrates)]
def slowest():
return bitrates[min(bitrates)]

116
amodem/detect.py Normal file
View File

@@ -0,0 +1,116 @@
from . import dsp
from . import equalizer
from . import common
import numpy as np
import logging
import itertools
import collections
log = logging.getLogger(__name__)
class Detector(object):
COHERENCE_THRESHOLD = 0.9
CARRIER_DURATION = sum(equalizer.prefix)
CARRIER_THRESHOLD = int(0.9 * CARRIER_DURATION)
SEARCH_WINDOW = int(0.1 * CARRIER_DURATION)
START_PATTERN_LENGTH = SEARCH_WINDOW // 4
def __init__(self, config, pylab):
self.freq = config.Fc
self.omega = 2 * np.pi * self.freq / config.Fs
self.Nsym = config.Nsym
self.Tsym = config.Tsym
self.maxlen = config.baud # 1 second of symbols
self.max_offset = config.timeout * config.Fs
self.plt = pylab
def _wait(self, samples):
counter = 0
bufs = collections.deque([], maxlen=self.maxlen)
for offset, buf in common.iterate(samples, self.Nsym, index=True):
if offset > self.max_offset:
raise ValueError('Timeout waiting for carrier')
bufs.append(buf)
coeff = dsp.coherence(buf, self.omega)
if abs(coeff) > self.COHERENCE_THRESHOLD:
counter += 1
else:
counter = 0
if counter == self.CARRIER_THRESHOLD:
return offset, bufs
raise ValueError('No carrier detected')
def run(self, samples):
offset, bufs = self._wait(samples)
length = (self.CARRIER_THRESHOLD - 1) * self.Nsym
begin = offset - length
start_time = begin * self.Tsym / self.Nsym
log.info('Carrier detected at ~%.1f ms @ %.1f kHz',
start_time * 1e3, self.freq / 1e3)
log.debug('Buffered %d ms of audio', len(bufs))
bufs = list(bufs)[-self.CARRIER_THRESHOLD-self.SEARCH_WINDOW:]
n = self.SEARCH_WINDOW + self.CARRIER_DURATION - self.CARRIER_THRESHOLD
trailing = list(itertools.islice(samples, n * self.Nsym))
bufs.append(np.array(trailing))
buf = np.concatenate(bufs)
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)
buf = buf[offset:]
prefix_length = self.CARRIER_DURATION * self.Nsym
amplitude, freq_err = self.estimate(buf[:prefix_length])
return itertools.chain(buf, samples), amplitude, freq_err
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)
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)
index = np.argmax(coeffs)
log.info('Carrier coherence: %.3f%%', coeffs[index] * 100)
offset = index + len(zeroes)
return offset
def estimate(self, buf, skip=5):
filt = dsp.exp_iwt(-self.omega, self.Nsym) / (0.5 * self.Nsym)
frames = common.iterate(buf, self.Nsym)
symbols = [np.dot(filt, frame) for frame in frames]
symbols = np.array(symbols[skip:-skip])
amplitude = np.mean(np.abs(symbols))
log.info('Carrier symbols amplitude : %.3f', amplitude)
phase = np.unwrap(np.angle(symbols)) / (2 * np.pi)
indices = np.arange(len(phase))
a, b = dsp.linear_regression(indices, phase)
self.plt.figure()
self.plt.plot(indices, phase, ':')
self.plt.plot(indices, a * indices + b)
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))
return amplitude, freq_err

View File

@@ -1,33 +1,8 @@
import numpy as np
from numpy import linalg
import logging
log = logging.getLogger(__name__)
from . import config
from . import common
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_
class FIR(object):
def __init__(self, h):
self.h = np.array(h)
@@ -42,31 +17,10 @@ class FIR(object):
self.x_state = x_
def lfilter(b, a, x):
f = IIR(b=b, a=a)
y = list(f(x))
return np.array(y)
def estimate(x, y, order, lookahead=0):
offset = order - 1
assert offset >= lookahead
b = y[offset-lookahead:len(x)-lookahead]
A = [] # columns of x
N = len(x) - order + 1
for i in range(order):
A.append(x[i:N+i])
# switch to rows for least-squares
h = linalg.lstsq(np.array(A).T, b)[0]
return h[::-1]
class Demux(object):
def __init__(self, sampler, freqs):
Nsym = config.Nsym
self.filters = [exp_iwt(-f, Nsym) / (0.5*Nsym) for f in freqs]
def __init__(self, sampler, omegas, Nsym):
self.Nsym = Nsym
self.filters = [exp_iwt(-w, Nsym) / (0.5*self.Nsym) for w in omegas]
self.filters = np.array(self.filters)
self.sampler = sampler
@@ -74,8 +28,8 @@ class Demux(object):
return self
def next(self):
frame = self.sampler.take(size=config.Nsym)
if len(frame) == config.Nsym:
frame = self.sampler.take(size=self.Nsym)
if len(frame) == self.Nsym:
return np.dot(self.filters, frame)
else:
raise StopIteration
@@ -83,18 +37,21 @@ class Demux(object):
__next__ = next
def exp_iwt(freq, n):
iwt = 2j * np.pi * freq * np.arange(n) * config.Ts
return np.exp(iwt)
def exp_iwt(omega, n):
return np.exp(1j * omega * np.arange(n))
def norm(x):
return np.sqrt(np.dot(x.conj(), x).real)
def coherence(x, freq):
def rms(x):
return np.mean(np.abs(x) ** 2, axis=0) ** 0.5
def coherence(x, omega):
n = len(x)
Hc = exp_iwt(-freq, n) / np.sqrt(0.5*n)
Hc = exp_iwt(-omega, n) / np.sqrt(0.5*n)
norm_x = norm(x)
if norm_x:
return np.dot(Hc, x) / norm_x
@@ -106,16 +63,17 @@ def linear_regression(x, y):
''' 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
def __init__(self, symbols):
self.encode_map = {}
symbols = np.array(list(symbols))
@@ -132,7 +90,7 @@ class MODEM(object):
self.symbols = symbols
self.bits_per_symbol = bits_per_symbol
bits_map = {symbol: bits for bits, symbol in self.encode_map.items()}
bits_map = dict(item[::-1] for item in self.encode_map.items())
self.decode_list = [(s, bits_map[s]) for s in self.symbols]
def encode(self, bits):
@@ -143,17 +101,25 @@ class MODEM(object):
''' 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 __repr__(self):
return '<{:.3f} kbps, {:d}-QAM, {:d} carriers>'.format(
config.modem_bps / 1e3, len(self.symbols), len(config.carriers))
__str__ = __repr__
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,85 +1,63 @@
from . import dsp
from . import sampling
from . import levinson
import numpy as np
from numpy.linalg import lstsq
from amodem import dsp
from amodem import config
from amodem import sampling
import itertools
import random
_constellation = [1, 1j, -1, -1j]
def train_symbols(length, seed=0, Nfreq=config.Nfreq):
r = random.Random(seed)
choose = lambda: [r.choice(_constellation) for j in range(Nfreq)]
return np.array([choose() for i in range(length)])
class Equalizer(object):
def __init__(self, config):
self.carriers = config.carriers
self.omegas = 2 * np.pi * np.array(config.frequencies) / config.Fs
self.Nfreq = config.Nfreq
self.Nsym = config.Nsym
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
def modulator(self, symbols):
gain = 1.0 / len(self.carriers)
result = []
for s in symbols:
result.append(np.dot(s, self.carriers))
result = np.concatenate(result).real * gain
assert np.max(np.abs(result)) <= 1
return result
def demodulator(self, signal, size):
signal = itertools.chain(signal, itertools.repeat(0))
symbols = dsp.Demux(sampler=sampling.Sampler(signal),
omegas=self.omegas, Nsym=self.Nsym)
return np.array(list(itertools.islice(symbols, size)))
def modulator(symbols):
carriers = config.carriers
gain = 1.0 / len(carriers)
result = []
for s in symbols:
result.append(np.dot(s, carriers))
result = np.concatenate(result).real * gain
assert np.max(np.abs(result)) <= 1
return result
prefix = [1]*200 + [0]*50
equalizer_length = 200
silence_length = 50
def demodulator(signal, size):
signal = itertools.chain(signal, itertools.repeat(0))
symbols = dsp.Demux(sampling.Sampler(signal), config.frequencies)
return np.array(list(itertools.islice(symbols, size)))
def train(signal, expected, order, lookahead=0):
padding = np.zeros(lookahead)
assert len(signal) == len(expected)
x = np.concatenate([signal, padding])
y = np.concatenate([padding, expected])
def equalize_symbols(signal, symbols, order, lookahead=0):
Nsym = config.Nsym
Nfreq = config.Nfreq
carriers = config.carriers
assert symbols.shape[1] == Nfreq
length = symbols.shape[0]
matched = np.array(carriers) / (0.5*Nsym)
matched = matched[:, ::-1].transpose().conj()
signal = np.concatenate([signal, np.zeros(lookahead)])
y = dsp.lfilter(x=signal, b=matched, a=[1])
A = []
b = []
for j in range(Nfreq):
for i in range(length):
offset = (i+1)*Nsym
row = y[offset-order:offset+lookahead, j]
A.append(row)
b.append(symbols[i, j])
A = np.array(A)
b = np.array(b)
h, residuals, rank, sv = lstsq(A, b)
h = h[::-1].real
return h
def equalize_signal(signal, expected, order, lookahead=0):
signal = np.concatenate([np.zeros(order-1), signal, np.zeros(lookahead)])
length = len(expected)
A = []
b = []
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, residuals, rank, sv = lstsq(A, b)
h = h[::-1].real
return h
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,6 +1,4 @@
''' Reed-Solomon CODEC. '''
from . import common
import bitarray
import functools
import itertools
@@ -9,23 +7,24 @@ import struct
import logging
log = logging.getLogger(__name__)
_crc32 = lambda x, mask: binascii.crc32(x) & mask
# (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):
fmt = '>L' # unsigned longs (32-bit)
size = struct.calcsize(fmt)
func = functools.partial(_crc32, mask=0xFFFFFFFF)
def encode(self, payload):
checksum = self.func(payload)
checksum = _checksum_func(payload)
return struct.pack(self.fmt, checksum) + payload
def decode(self, data):
received, = struct.unpack(self.fmt, data[:self.size])
received, = struct.unpack(self.fmt, bytes(data[:self.size]))
payload = data[self.size:]
expected = self.func(payload)
expected = _checksum_func(payload)
if received != expected:
log.warning('Invalid checksum: %04x != %04x', received, expected)
raise ValueError('Invalid checksum')
@@ -33,8 +32,8 @@ class Checksum(object):
class Framer(object):
block_size = 1024
prefix_fmt = '>L'
block_size = 250
prefix_fmt = '>B'
prefix_len = struct.calcsize(prefix_fmt)
checksum = Checksum()
@@ -42,7 +41,7 @@ class Framer(object):
def _pack(self, block):
frame = self.checksum.encode(block)
return struct.pack(self.prefix_fmt, len(frame)) + frame
return bytearray(struct.pack(self.prefix_fmt, len(frame)) + frame)
def encode(self, data):
for block in common.iterate(data=data, size=self.block_size,
@@ -53,8 +52,8 @@ class Framer(object):
def decode(self, data):
data = iter(data)
while True:
length, = self._take_fmt(data, self.prefix_fmt)
frame = self._take_len(data, length)
length, = _take_fmt(data, self.prefix_fmt)
frame = _take_len(data, length)
block = self.checksum.decode(frame)
if block == self.EOF:
log.debug('EOF frame detected')
@@ -62,18 +61,20 @@ class Framer(object):
yield block
def _take_fmt(self, data, fmt):
length = struct.calcsize(fmt)
chunk = bytearray(itertools.islice(data, length))
if len(chunk) < length:
raise ValueError('missing prefix data')
return struct.unpack(fmt, chunk)
def _take_len(self, data, length):
chunk = bytearray(itertools.islice(data, length))
if len(chunk) < length:
raise ValueError('missing payload data')
return chunk
def _take_fmt(data, fmt):
length = struct.calcsize(fmt)
chunk = bytearray(itertools.islice(data, length))
if len(chunk) < length:
raise ValueError('missing prefix data')
return struct.unpack(fmt, bytes(chunk))
def _take_len(data, length):
chunk = bytearray(itertools.islice(data, length))
if len(chunk) < length:
raise ValueError('missing payload data')
return chunk
def chain_wrapper(func):
@@ -84,24 +85,37 @@ def chain_wrapper(func):
return wrapped
class BitPacker(object):
byte_size = 8
def __init__(self):
bits_list = []
for index in range(2 ** self.byte_size):
bits = [index & (2 ** k) for k in range(self.byte_size)]
bits_list.append(tuple((1 if b else 0) for b in bits))
self.to_bits = dict((i, bits) for i, bits in enumerate(bits_list))
self.to_byte = dict((bits, i) for i, bits in enumerate(bits_list))
@chain_wrapper
def encode(data, framer=None):
converter = BitPacker()
framer = framer or Framer()
for frame in framer.encode(data):
bits = bitarray.bitarray(endian='little')
bits.frombytes(bytes(frame))
yield bits
for byte in frame:
yield converter.to_bits[byte]
@chain_wrapper
def _to_bytes(bits, block_size=1):
for chunk in common.iterate(data=bits, size=8*block_size,
func=lambda x: x, truncate=True):
yield bitarray.bitarray(chunk, endian='little').tobytes()
def _to_bytes(bits):
converter = BitPacker()
for chunk in common.iterate(data=bits, size=8,
func=tuple, truncate=True):
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

68
amodem/main.py Normal file
View File

@@ -0,0 +1,68 @@
import numpy as np
import logging
import itertools
from . import send as _send
from . import recv as _recv
from . import framing, common, stream, detect, sampling
log = logging.getLogger(__name__)
def send(config, src, dst, gain=1.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.start()
training_duration = sender.offset
log.info('Sending %.3f seconds of training audio', training_duration / Fs)
reader = stream.Reader(src, eof=True)
data = itertools.chain.from_iterable(reader)
bits = framing.encode(data)
log.info('Starting modulation')
sender.modulate(bits=bits)
data_duration = sender.offset - training_duration
log.info('Sent %.3f kB @ %.3f seconds',
reader.total / 1e3, data_duration / Fs)
# post-padding audio with silence
sender.write(np.zeros(int(Fs * config.silence_stop)))
return True
def recv(config, src, dst, dump_audio=None, pylab=None):
if dump_audio:
src = stream.Dumper(src, dump_audio)
reader = stream.Reader(src, data_type=common.loads)
signal = itertools.chain.from_iterable(reader)
log.debug('Skipping %.3f seconds', config.skip_start)
common.take(signal, int(config.skip_start * config.Fs))
pylab = pylab or common.Dummy()
detector = detect.Detector(config=config, pylab=pylab)
receiver = _recv.Receiver(config=config, pylab=pylab)
try:
log.info('Waiting for carrier tone: %.1f kHz', config.Fc / 1e3)
signal, amplitude, freq_error = detector.run(signal)
freq = 1 / (1.0 + freq_error) # receiver's compensated frequency
log.debug('Frequency correction: %.3f ppm', (freq - 1) * 1e6)
gain = 1.0 / amplitude
log.debug('Gain correction: %.3f', gain)
sampler = sampling.Sampler(signal, sampling.Interpolator(), freq=freq)
receiver.run(sampler, gain=1.0/amplitude, output=dst)
return True
except BaseException:
log.exception('Decoding failed')
return False
finally:
dst.flush()
receiver.report()

View File

@@ -1,260 +1,199 @@
from . import dsp
from . import common
from . import framing
from . import equalizer
import numpy as np
import logging
import itertools
import functools
import collections
import time
log = logging.getLogger(__name__)
from . import stream
from . import dsp
from . import sampling
from . import train
from . import common
from . import config
from . import framing
from . import equalizer
modem = dsp.MODEM(config.symbols)
# Plots' size (WIDTH x HEIGHT)
HEIGHT = np.floor(np.sqrt(config.Nfreq))
WIDTH = np.ceil(config.Nfreq / float(HEIGHT))
COHERENCE_THRESHOLD = 0.99
CARRIER_DURATION = sum(train.prefix)
CARRIER_THRESHOLD = int(0.99 * CARRIER_DURATION)
SEARCH_WINDOW = 10 # symbols
def report_carrier(bufs, begin):
x = np.concatenate(tuple(bufs)[-CARRIER_THRESHOLD:-1])
Hc = dsp.exp_iwt(-config.Fc, len(x))
Zc = np.dot(Hc, x) / (0.5*len(x))
amp = abs(Zc)
log.info('Carrier detected at ~%.1f ms @ %.1f kHz:'
' coherence=%.3f%%, amplitude=%.3f',
begin * config.Tsym * 1e3 / config.Nsym, config.Fc / 1e3,
np.abs(dsp.coherence(x, config.Fc)) * 100, amp)
return amp
def detect(samples, freq):
counter = 0
bufs = collections.deque([], maxlen=config.baud) # 1 second of symbols
for offset, buf in common.iterate(samples, config.Nsym, enumerate=True):
bufs.append(buf)
coeff = dsp.coherence(buf, config.Fc)
if abs(coeff) > COHERENCE_THRESHOLD:
counter += 1
else:
counter = 0
if counter == CARRIER_THRESHOLD:
length = (CARRIER_THRESHOLD - 1) * config.Nsym
begin = offset - length
amplitude = report_carrier(bufs, begin=begin)
break
else:
raise ValueError('No carrier detected')
log.debug('Buffered %d ms of audio', len(bufs))
bufs = list(bufs)[-CARRIER_THRESHOLD-SEARCH_WINDOW:]
trailing = list(itertools.islice(samples, SEARCH_WINDOW*config.Nsym))
bufs.append(np.array(trailing))
buf = np.concatenate(bufs)
offset = find_start(buf, CARRIER_DURATION*config.Nsym)
log.debug('Carrier starts at %.3f ms',
offset * config.Tsym * 1e3 / config.Nsym)
return itertools.chain(buf[offset:], samples), amplitude
def find_start(buf, length):
N = len(buf)
carrier = dsp.exp_iwt(config.Fc, N)
z = np.cumsum(buf * carrier)
z = np.concatenate([[0], z])
correlations = np.abs(z[length:] - z[:-length])
return np.argmax(correlations)
class Receiver(object):
def __init__(self, plt=None):
def __init__(self, config, pylab=None):
self.stats = {}
self.plt = plt or common.Dummy()
self.plt = pylab
self.modem = dsp.MODEM(config.symbols)
self.frequencies = np.array(config.frequencies)
self.omegas = 2 * np.pi * self.frequencies / config.Fs
self.Nsym = config.Nsym
self.Tsym = config.Tsym
self.iters_per_update = 100 # [ms]
self.iters_per_report = 1000 # [ms]
self.modem_bitrate = config.modem_bps
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, sampler, freq, gain=1.0, skip=5):
symbols = dsp.Demux(sampler, [freq])
S = common.take(symbols, len(train.prefix)).squeeze() * gain
def _prefix(self, symbols, gain=1.0):
S = common.take(symbols, len(equalizer.prefix))
S = S[:, self.carrier_index] * gain
sliced = np.round(np.abs(S))
self.plt.figure()
self.plt.subplot(121)
self.plt.subplot(1, 2, 1)
self._constellation(S, sliced, 'Prefix')
bits = np.array(sliced, dtype=int)
self.plt.subplot(122)
self.plt.subplot(1, 2, 2)
self.plt.plot(np.abs(S))
self.plt.plot(train.prefix)
if any(bits != train.prefix):
raise ValueError('Incorrect prefix')
self.plt.plot(equalizer.prefix)
errors = (bits != equalizer.prefix)
if any(errors):
msg = 'Incorrect prefix: {0} errors'.format(sum(errors))
raise ValueError(msg)
log.debug('Prefix OK')
nonzeros = np.array(train.prefix, dtype=bool)
pilot_tone = S[nonzeros]
phase = np.unwrap(np.angle(pilot_tone)) / (2 * np.pi)
indices = np.arange(len(phase))
a, b = dsp.linear_regression(indices[skip:-skip], phase[skip:-skip])
self.plt.figure()
self.plt.plot(indices, phase, ':')
self.plt.plot(indices, a * indices + b)
freq_err = a / (config.Tsym * config.Fc)
last_phase = a * indices[-1] + b
log.debug('Current phase on carrier: %.3f', last_phase)
log.debug('Frequency error: %.2f ppm', freq_err * 1e6)
self.plt.title('Frequency drift: {:.3f} ppm'.format(freq_err * 1e6))
return freq_err
def _train(self, sampler, order, lookahead):
gain = config.Nfreq
train_symbols = equalizer.train_symbols(train.equalizer_length)
train_signal = equalizer.modulator(train_symbols) * gain
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
prefix = postfix = train.silence_length * config.Nsym
signal_length = train.equalizer_length * config.Nsym + prefix + postfix
prefix = postfix = equalizer.silence_length * self.Nsym
signal_length = equalizer_length * self.Nsym + prefix + postfix
signal = sampler.take(signal_length + lookahead)
coeffs = equalizer.equalize_signal(
coeffs = equalizer.train(
signal=signal[prefix:-postfix],
expected=train_signal,
expected=np.concatenate([train_signal, np.zeros(lookahead)]),
order=order, lookahead=lookahead
)
log.debug(
'Equalization filter: [%s]',
', '.join('{:.2f}'.format(c) for c in coeffs)
)
self.plt.figure()
self.plt.plot(np.arange(order+lookahead), coeffs)
equalization_filter = dsp.FIR(h=coeffs)
# 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)
return equalization_filter
symbols = equalizer.demodulator(equalized, train.equalizer_length)
def _verify_training(self, equalized, train_symbols):
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)
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()
for i, freq, snr in zip(range(config.Nfreq), config.frequencies, SNRs):
for (i, freq), snr in zip(enumerate(self.frequencies), SNRs):
log.debug('%5.1f kHz: SNR = %5.2f dB', freq / 1e3, snr)
self.plt.subplot(HEIGHT, WIDTH, i+1)
self._constellation(symbols[:, i], train_symbols[:, i],
'$F_c = {} Hz$'.format(freq))
'$F_c = {0} Hz$'.format(freq), index=i)
assert error_rate == 0, error_rate
return equalization_filter
def _demodulate(self, sampler, freqs):
def _bitstream(self, symbols, error_handler):
streams = []
symbol_list = []
errors = {}
def error_handler(received, decoded, freq):
errors.setdefault(freq, []).append(received / decoded)
symbols = dsp.Demux(sampler, freqs)
generators = common.split(symbols, n=len(freqs))
for freq, S in zip(freqs, generators):
generators = common.split(symbols, n=len(self.omegas))
for freq, S in zip(self.frequencies, generators):
equalized = []
S = common.icapture(S, result=equalized)
symbol_list.append(equalized)
freq_handler = functools.partial(error_handler, freq=freq)
bits = modem.decode(S, freq_handler) # list of bit tuples
streams.append(bits) # stream per frequency
bits = self.modem.decode(S, freq_handler) # list of bit tuples
streams.append(bits) # bit stream per frequency
return common.izip(streams), symbol_list
def _demodulate(self, sampler, symbols):
symbol_list = []
errors = {}
noise = {}
def _handler(received, decoded, freq):
errors.setdefault(freq, []).append(received / decoded)
noise.setdefault(freq, []).append(received - decoded)
stream, symbol_list = self._bitstream(symbols, _handler)
self.stats['symbol_list'] = symbol_list
self.stats['rx_bits'] = 0
self.stats['rx_start'] = time.time()
log.info('Starting demodulation: %s', modem)
for i, block in enumerate(common.izip(streams)): # block per frequency
for bits in block:
log.info('Starting demodulation')
for i, block_of_bits in enumerate(stream, 1):
for bits in block_of_bits:
self.stats['rx_bits'] = self.stats['rx_bits'] + len(bits)
yield bits
if i > 0 and i % config.baud == 0:
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
errors.clear()
if i % self.iters_per_update == 0:
self._update_sampler(errors, sampler)
duration = time.time() - self.stats['rx_start']
sampler.freq -= 0.01 * err / config.Fc
sampler.offset -= err
log.debug(
'Got %8.1f kB, realtime: %6.2f%%, drift: %+5.2f ppm',
self.stats['rx_bits'] / 8e3,
duration * 100.0 / (i*config.Tsym),
(1.0 - sampler.freq) * 1e6
)
if i % self.iters_per_report == 0:
self._report_progress(noise, sampler)
def start(self, signal, freqs, gain=1.0):
sampler = sampling.Sampler(signal, sampling.Interpolator())
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
errors.clear()
freq_err = self._prefix(sampler, freq=freqs[0], gain=gain)
sampler.freq -= freq_err
sampler.freq -= self.freq_err_gain * err
sampler.offset -= err
filt = self._train(sampler, order=11, lookahead=5)
def _report_progress(self, noise, sampler):
e = np.array([e for v in noise.values() for e in v])
noise.clear()
log.debug(
'Got %10.3f kB, SNR: %5.2f dB, drift: %+5.2f ppm',
self.stats['rx_bits'] / 8e3,
-10 * np.log10(np.mean(np.abs(e) ** 2)),
(1.0 - sampler.freq) * 1e6
)
def run(self, sampler, gain, output):
symbols = dsp.Demux(sampler, omegas=self.omegas, Nsym=self.Nsym)
self._prefix(symbols, gain=gain)
filt = self._train(sampler, order=10, lookahead=10)
sampler.equalizer = lambda x: list(filt(x))
bitstream = self._demodulate(sampler, freqs)
self.bitstream = itertools.chain.from_iterable(bitstream)
bitstream = self._demodulate(sampler, symbols)
bitstream = itertools.chain.from_iterable(bitstream)
def run(self, output):
data = framing.decode(self.bitstream)
self.size = 0
for chunk in common.iterate(data=data, size=256,
truncate=False, func=bytearray):
output.write(chunk)
self.size += len(chunk)
for frame in framing.decode_frames(bitstream):
output.write(frame)
self.output_size += len(frame)
def report(self):
if self.stats:
duration = time.time() - self.stats['rx_start']
audio_time = self.stats['rx_bits'] / float(config.modem_bps)
audio_time = self.stats['rx_bits'] / float(self.modem_bitrate)
log.debug('Demodulated %.3f kB @ %.3f seconds (%.1f%% realtime)',
self.stats['rx_bits'] / 8e3, duration,
100 * duration / audio_time if audio_time else 0)
log.info('Received %.3f kB @ %.3f seconds = %.3f kB/s',
self.size * 1e-3, duration, self.size * 1e-3 / duration)
self.output_size * 1e-3, duration,
self.output_size * 1e-3 / duration)
self.plt.figure()
symbol_list = np.array(self.stats['symbol_list'])
for i, freq in enumerate(config.frequencies):
self.plt.subplot(HEIGHT, WIDTH, i+1)
self._constellation(symbol_list[i], config.symbols,
'$F_c = {} Hz$'.format(freq))
for i, freq in enumerate(self.frequencies):
self._constellation(symbol_list[i], self.modem.symbols,
'$F_c = {0} Hz$'.format(freq), index=i)
self.plt.show()
def _constellation(self, y, symbols, title):
def _constellation(self, y, symbols, title, index=None):
if index is not None:
Nfreq = len(self.frequencies)
height = np.floor(np.sqrt(Nfreq))
width = np.ceil(Nfreq / float(height))
self.plt.subplot(height, width, index + 1)
theta = np.linspace(0, 2*np.pi, 1000)
y = np.array(y)
self.plt.plot(y.real, y.imag, '.')
@@ -265,27 +204,3 @@ class Receiver(object):
self.plt.axis('equal')
self.plt.axis(np.array([-1, 1, -1, 1])*1.1)
self.plt.title(title)
def main(args):
reader = stream.Reader(args.input, data_type=common.loads)
signal = itertools.chain.from_iterable(reader)
skipped = common.take(signal, args.skip)
log.debug('Skipping %.3f seconds', len(skipped) / float(config.baud))
reader.check = common.check_saturation
receiver = Receiver(plt=args.plot)
success = False
try:
log.info('Waiting for carrier tone: %.1f kHz', config.Fc / 1e3)
signal, amplitude = detect(signal, config.Fc)
receiver.start(signal, config.frequencies, gain=1.0/amplitude)
receiver.run(args.output)
success = True
except Exception:
log.exception('Decoding failed')
receiver.report()
return success

View File

@@ -1,81 +1,87 @@
#!/usr/bin/env python
import numpy as np
import itertools
import logging
from amodem import common
log = logging.getLogger(__name__)
class Interpolator(object):
def __init__(self, resolution=10000, width=128):
def __init__(self, resolution=1024, width=128):
self.width = width
self.resolution = resolution
N = resolution * width
u = np.arange(-N, N, dtype=float)
window = (1 + np.cos(0.5 * np.pi * u / N)) / 2.0
window = np.cos(0.5 * np.pi * u / N) ** 2.0 # raised cosine
h = np.sinc(u / resolution) * window
self.filt = []
for index in range(resolution): # split into multiphase filters
filt = h[index::resolution]
filt = filt[::-1]
filt = filt[::-1] # flip (due to convolution)
self.filt.append(filt)
lengths = map(len, self.filt)
self.coeff_len = 2*width
assert set(lengths) == set([self.coeff_len])
lengths = [len(f) for f in self.filt]
self.coeff_len = 2 * width
assert set(lengths) == set([self.coeff_len]) # verify same lengths
assert len(self.filt) == resolution
class Sampler(object):
def __init__(self, src, interp=None):
self.freq = 1.0
self.equalizer = lambda x: x
def __init__(self, src, interp=None, freq=1.0):
self.freq = freq
self.equalizer = lambda x: x # LTI equalization filter
if interp is not None:
self.interp = interp
self.resolution = self.interp.resolution
self.filt = self.interp.filt
self.width = self.interp.width
# TODO: explain indices arithmetic
# polyphase filters are centered at (width + 1) index
padding = [0.0] * self.interp.width
# pad with zeroes to "simulate" regular sampling
self.src = itertools.chain(padding, src)
self.offset = self.interp.width + 1
# samples' buffer to be used by interpolation
self.buff = np.zeros(self.interp.coeff_len)
self.index = 0
self.take = self._take
else:
# skip interpolation
# skip interpolation (for testing)
src = iter(src)
self.take = lambda size: common.take(src, size)
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]
end = k + self.width
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
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])
def resample(src, dst, df=0.0):
from . import common
x = common.load(src)
sampler = Sampler(x, Interpolator())
sampler.freq += df

View File

@@ -1,82 +1,49 @@
from . import common
from . import equalizer
from . import dsp
import numpy as np
import logging
import itertools
log = logging.getLogger(__name__)
from . import train
from . import wave
from . import common
from . import config
from . import stream
from . import framing
from . import equalizer
from . import dsp
modem = dsp.MODEM(config.symbols)
class Writer(object):
def __init__(self, fd):
class Sender(object):
def __init__(self, fd, config, gain=1.0):
self.gain = gain
self.offset = 0
self.fd = fd
self.modem = dsp.MODEM(config.symbols)
self.carriers = config.carriers / config.Nfreq
self.pilot = config.carriers[config.carrier_index]
self.silence = np.zeros(equalizer.silence_length * config.Nsym)
self.iters_per_report = config.baud # report once per second
self.padding = [0] * config.bits_per_baud
self.equalizer = equalizer.Equalizer(config)
def write(self, sym, n=1):
sym = np.array(sym)
data = common.dumps(sym, n)
def write(self, sym):
sym = np.array(sym) * self.gain
data = common.dumps(sym)
self.fd.write(data)
self.offset += len(data)
self.offset += len(sym)
def start(self):
carrier = config.carriers[config.carrier_index]
for value in train.prefix:
self.write(carrier * value)
for value in equalizer.prefix:
self.write(self.pilot * value)
silence = np.zeros(train.silence_length * config.Nsym)
symbols = equalizer.train_symbols(train.equalizer_length)
signal = equalizer.modulator(symbols)
self.write(silence)
symbols = self.equalizer.train_symbols(equalizer.equalizer_length)
signal = self.equalizer.modulator(symbols)
self.write(self.silence)
self.write(signal)
self.write(silence)
self.write(self.silence)
def modulate(self, bits):
padding = [0] * config.bits_per_baud
bits = itertools.chain(bits, padding)
symbols_iter = modem.encode(bits)
carriers = config.carriers / config.Nfreq
for i, symbols in common.iterate(symbols_iter,
size=config.Nfreq, enumerate=True):
symbols = np.array(list(symbols))
self.write(np.dot(symbols, carriers))
data_duration = (i / config.Nfreq + 1) * config.Tsym
if data_duration % 1 == 0:
bits_size = data_duration * config.modem_bps
log.debug('Sent %8.1f kB', bits_size / 8e3)
def main(args):
writer = Writer(args.output)
# pre-padding audio with silence
writer.write(np.zeros(int(config.Fs * args.silence_start)))
writer.start()
training_size = writer.offset
training_duration = training_size / wave.bytes_per_second
log.info('Sending %.3f seconds of training audio', training_duration)
reader = stream.Reader(args.input, bufsize=(64 << 10), eof=True)
data = itertools.chain.from_iterable(reader)
bits = framing.encode(data)
log.info('Starting modulation: %s', modem)
writer.modulate(bits=bits)
data_size = writer.offset - training_size
log.info('Sent %.3f kB @ %.3f seconds',
reader.total / 1e3, data_size / wave.bytes_per_second)
# post-padding audio with silence
writer.write(np.zeros(int(config.Fs * args.silence_stop)))
bits = itertools.chain(bits, self.padding)
Nfreq = len(self.carriers)
symbols_iter = common.iterate(self.modem.encode(bits), size=Nfreq)
for i, symbols in enumerate(symbols_iter, 1):
self.write(np.dot(symbols, self.carriers))
if i % self.iters_per_report == 0:
total_bits = i * Nfreq * self.modem.bits_per_symbol
log.debug('Sent %10.3f kB', total_bits / 8e3)

View File

@@ -1,21 +1,17 @@
import time
import logging
log = logging.getLogger(__name__)
class Reader(object):
def __init__(self, fd, data_type=None, bufsize=4096,
eof=False, timeout=2.0, wait=0.2):
wait = 0.2
timeout = 2.0
bufsize = (8 << 10)
def __init__(self, fd, data_type=None, eof=False):
self.fd = fd
self.data_type = data_type if (data_type is not None) else lambda x: x
self.bufsize = bufsize
self.eof = eof
self.timeout = timeout
self.wait = wait
self.total = 0
self.check = None
def __iter__(self):
return self
@@ -40,13 +36,21 @@ class Reader(object):
block.extend(data)
if len(block) == self.bufsize:
values = self.data_type(block)
if self.check:
self.check(values)
return values
return self.data_type(block)
time.sleep(self.wait)
raise IOError('timeout')
__next__ = next
class Dumper(object):
def __init__(self, src, dst):
self.src = src
self.dst = dst
def read(self, size):
data = self.src.read(size)
self.dst.write(data)
return data

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

@@ -0,0 +1,30 @@
import mock
import time
import pytest
from amodem import async
import logging
logging.basicConfig(format='%(message)s')
def test_async_reader():
def _read(n):
time.sleep(n * 0.1)
return b'\x00' * n
s = mock.Mock()
s.read = _read
r = async.AsyncReader(s, 1)
n = 5
assert r.read(n) == b'\x00' * n
r.close()
assert r.stream is None
r.close()
def test_async_reader_error():
s = mock.Mock()
s.read.side_effect = IOError()
r = async.AsyncReader(s, 1)
with pytest.raises(IOError):
r.read(3)

View File

@@ -0,0 +1,34 @@
from amodem import audio, config
import mock
import pytest
def test():
length = 1024
data = b'\x12\x34' * length
with mock.patch('ctypes.CDLL') as cdll:
lib = mock.Mock()
lib.Pa_GetErrorText = lambda code: b'Error' if code else b'Success'
lib.Pa_GetDefaultOutputDevice.return_value = 1
lib.Pa_GetDefaultInputDevice.return_value = 2
lib.Pa_OpenStream.return_value = 0
cdll.return_value = lib
interface = audio.Interface(config=config.fastest(), debug=True)
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
s.write(data=data)
s.close()
with interface:
s = interface.recorder()
assert s.params.device == 2
s.stream = 2 # simulate non-zero input stream handle
s.read(len(data))
s.close()
with pytest.raises(Exception):
interface._error_check(1)

160
amodem/tests/test_calib.py Normal file
View File

@@ -0,0 +1,160 @@
from amodem import calib
from amodem import common
from amodem import config
from io import BytesIO
import numpy as np
import random
import pytest
import mock
config = config.fastest()
class ProcessMock(object):
def __init__(self):
self.buf = BytesIO()
self.stdin = self
self.stdout = self
self.bytes_per_sample = 2
def write(self, data):
assert self.buf.tell() < 10e6
self.buf.write(data)
def read(self, n):
return self.buf.read(n)
def test_success():
p = ProcessMock()
calib.send(config, p, gain=0.5, limit=32)
p.buf.seek(0)
calib.recv(config, p)
def test_too_strong():
p = ProcessMock()
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'
def test_too_weak():
p = ProcessMock()
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'
def test_too_noisy():
r = random.Random(0) # generate random binary signal
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'
def test_errors():
class WriteError(ProcessMock):
def write(self, data):
raise KeyboardInterrupt()
p = WriteError()
with pytest.raises(KeyboardInterrupt):
calib.send(config, p, limit=32)
assert p.buf.tell() == 0
class ReadError(ProcessMock):
def read(self, n):
raise KeyboardInterrupt()
p = ReadError()
with pytest.raises(KeyboardInterrupt):
calib.recv(config, p, verbose=True)
assert p.buf.tell() == 0
@pytest.fixture(params=[0] + [sign * mag for sign in (+1, -1)
for mag in (0.1, 1, 10, 100, 1e3, 2e3)])
def freq_err(request):
return request.param * 1e-6
def test_drift(freq_err):
freq = config.Fc * (1 + freq_err / 1e6)
t = np.arange(int(1.0 * config.Fs)) * config.Ts
frame_length = 100
rms = 0.5
signal = rms * np.cos(2 * np.pi * freq * t)
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
iters += 1
assert iters > 0
assert iters == config.baud / frame_length
def test_volume():
with mock.patch('subprocess.check_call') as check_call:
ctl = calib.volume_controller('volume-control')
ctl(0.01)
ctl(0.421)
ctl(0.369)
ctl(1)
assert check_call.mock_calls == [
mock.call(shell=True, args='volume-control 1%'),
mock.call(shell=True, args='volume-control 42%'),
mock.call(shell=True, args='volume-control 37%'),
mock.call(shell=True, args='volume-control 100%')
]
with pytest.raises(AssertionError):
ctl(0)
with pytest.raises(AssertionError):
ctl(-0.5)
with pytest.raises(AssertionError):
ctl(12.3)
def test_send_max_volume():
with mock.patch('subprocess.check_call') as check_call:
calib.send(config, dst=BytesIO(), volume_cmd='ctl', limit=1)
assert check_call.mock_calls == [mock.call(shell=True, args='ctl 100%')]
def test_recv_binary_search():
buf = BytesIO()
gains = [0.5, 0.25, 0.38, 0.44, 0.41, 0.39, 0.40, 0.40]
for gain in gains:
calib.send(config, buf, gain=gain, limit=2)
buf.seek(0)
dump = BytesIO()
with mock.patch('subprocess.check_call') as check_call:
calib.recv(config, src=buf, volume_cmd='ctl', dump_audio=dump)
assert dump.getvalue() == buf.getvalue()
gains.append(gains[-1])
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
@@ -6,7 +7,7 @@ def iterlist(x, *args, **kwargs):
x = np.array(x)
return list(
(i, list(x))
for i, x in common.iterate(x, enumerate=True, *args, **kwargs)
for i, x in common.iterate(x, index=True, *args, **kwargs)
)
@@ -47,15 +48,15 @@ def test_dumps_loads():
assert all(x == y)
def test_saturation():
x = np.array([1, -1, 1, -1]) * 1e10
try:
common.check_saturation(x)
assert False
except common.SaturationError as e:
assert e.args == (max(x),)
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()
slowest = config.slowest()
assert slowest.modem_bps <= default.modem_bps
assert fastest.modem_bps >= default.modem_bps

View File

@@ -0,0 +1,6 @@
from amodem import config
def test_bitrates():
for rate, cfg in sorted(config.bitrates.items()):
assert rate * 1000 == cfg.modem_bps

View File

@@ -0,0 +1,61 @@
import numpy as np
import pytest
from amodem import dsp
from amodem import recv
from amodem import detect
from amodem import equalizer
from amodem import sampling
from amodem import config
from amodem import common
config = config.fastest()
def test_detect():
P = sum(equalizer.prefix)
t = np.arange(P * config.Nsym) * config.Ts
x = np.cos(2 * np.pi * config.Fc * t)
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-12
x = np.cos(2 * np.pi * (2*config.Fc) * t)
with pytest.raises(ValueError):
detector.run(x)
with pytest.raises(ValueError):
detector.max_offset = 0
detector.run(x)
def test_prefix():
omega = 2 * np.pi * config.Fc / config.Fs
symbol = np.cos(omega * np.arange(config.Nsym))
signal = np.concatenate([c * symbol for c in equalizer.prefix])
def symbols_stream(signal):
sampler = sampling.Sampler(signal)
return dsp.Demux(sampler=sampler, omegas=[omega], Nsym=config.Nsym)
r = recv.Receiver(config, pylab=common.Dummy())
r._prefix(symbols_stream(signal))
with pytest.raises(ValueError):
silence = 0 * signal
r._prefix(symbols_stream(silence))
def test_find_start():
sym = np.cos(2 * np.pi * config.Fc * np.arange(config.Nsym) * config.Ts)
detector = detect.Detector(config, pylab=common.Dummy())
length = 200
prefix = postfix = np.tile(0 * sym, 50)
carrier = np.tile(sym, length)
for offset in range(32):
bufs = [prefix, [0] * offset, carrier, postfix]
buf = np.concatenate(bufs)
start = detector.find_start(buf)
expected = offset + len(prefix)
assert expected == start

View File

@@ -1,13 +1,14 @@
import numpy as np
from numpy.linalg import norm
from amodem import dsp
from amodem import config
from amodem import sampling
from amodem import config
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])
@@ -20,50 +21,21 @@ 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))]
def test_estimate():
r = np.random.RandomState(seed=0)
x = r.uniform(-1, 1, [1000])
x[:10] = 0
x[len(x)-10:] = 0
c = 1.23
y = c * x
c_, = dsp.estimate(x=x, y=y, order=1)
assert abs(c - c_) < 1e-12
h = [1, 1]
y = dsp.lfilter(b=h, a=[1], x=x)
h_ = dsp.estimate(x=x, y=y, order=len(h))
assert norm(h - h_) < 1e-12
h = [0.1, 0.6, 0.9, 0.7, -0.2]
L = len(h) // 2
y = dsp.lfilter(b=h, a=[1], x=x)
h_ = dsp.estimate(
x=x[:len(x)-L], y=y[L:],
order=len(h), lookahead=L
)
assert norm(h - h_) < 1e-12
y_ = dsp.lfilter(b=h_, a=[1], x=x)
assert norm(y - y_) < 1e-12
def test_demux():
freqs = [1e3, 2e3]
carriers = [dsp.exp_iwt(f, config.Nsym) for f in freqs]
freqs = np.array([1e3, 2e3])
omegas = 2 * np.pi * freqs / config.Fs
carriers = [dsp.exp_iwt(2*np.pi*f/config.Fs, config.Nsym) for f in freqs]
syms = [3, 2j]
sig = np.dot(syms, carriers)
res = dsp.Demux(sampling.Sampler(sig.real), freqs)
res = dsp.Demux(sampling.Sampler(sig.real), omegas, config.Nsym)
res = np.array(list(res))
assert np.max(np.abs(res - syms)) < 1e-12
@@ -78,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
@@ -98,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

@@ -0,0 +1,67 @@
from numpy.random import RandomState
import numpy as np
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):
x = x.flatten()
y = y.flatten()
assert dsp.norm(x - y) < e * dsp.norm(x)
def test_training():
L = 1000
e = equalizer.Equalizer(config)
t1 = e.train_symbols(L)
t2 = e.train_symbols(L)
assert (t1 == t2).all()
def test_commutation():
x = np.random.RandomState(seed=0).normal(size=1000)
b = [1, 1j, -1, -1j]
a = [1, 0.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 = utils.lfilter(x=y, b=a, a=[1])
z_ = utils.lfilter(x=x, b=b, a=[1])
assert_approx(z, z_)
def test_modem():
L = 1000
e = equalizer.Equalizer(config)
sent = e.train_symbols(L)
gain = config.Nfreq
x = e.modulator(sent) * gain
received = e.demodulator(x, L)
assert_approx(sent, received)
def test_signal():
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 = utils.lfilter(x=x, b=num, a=den)
lookahead = 2
h = equalizer.train(
signal=y, expected=x, order=len(den), lookahead=lookahead)
assert dsp.norm(h[:lookahead]) < 1e-12
h = h[lookahead:]
assert_approx(h, den / num)
x_ = utils.lfilter(x=y, b=h, a=[1])
assert_approx(x_, x)

View File

@@ -31,19 +31,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'\x00'))
concat(f.decode(b''))
with pytest.raises(ValueError):
list(f.decode(b'\x01\x02\x03\x04'))
concat(f.decode(b'\x01'))
with pytest.raises(ValueError):
concat(f.decode(b'\xff'))

View File

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

View File

@@ -1,34 +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 send
from amodem import recv
from amodem import common
from amodem import dsp
from amodem import sampling
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):
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()
send.main(Args(silence_start=1, silence_stop=1,
input=BytesIO(tx_data), output=tx_audio))
main.send(config=cfg, src=BytesIO(tx_data), dst=tx_audio, gain=0.5)
data = tx_audio.getvalue()
data = common.loads(data)
@@ -41,10 +32,17 @@ def run(size, chan=None, df=0, success=True):
data = common.dumps(data)
rx_audio = BytesIO(data)
rx_data = BytesIO()
result = recv.main(Args(skip=0, input=rx_audio, output=rx_data))
dump = BytesIO()
try:
result = main.recv(config=cfg, src=rx_audio, dst=rx_data,
dump_audio=dump, pylab=None)
finally:
rx_audio.close()
rx_data = rx_data.getvalue()
assert data.startswith(dump.getvalue())
assert result == success
if success:
@@ -60,26 +58,36 @@ def test_small(small_size):
run(small_size, chan=lambda x: x)
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():
skip = 1 * send.config.Fs # remove trailing silence
skip = 32000 # remove trailing silence
run(1024, chan=lambda x: x[:-skip], success=False)
@pytest.fixture(params=[s*x for s in (+1, -1) for x in (0.1, 1, 10)])
@pytest.fixture(params=[sign * mag for sign in (+1, -1)
for mag in (0.1, 1, 10, 100, 1e3, 10e3)])
def freq_err(request):
return request.param * 1e-6
def test_timing(freq_err):
run(1024, df=freq_err)
run(8192, df=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():
@@ -98,3 +106,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(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)

View File

@@ -1,3 +0,0 @@
prefix = [1]*400 + [0]*50
equalizer_length = 500
silence_length = 100

1
amodem/version.py Normal file
View File

@@ -0,0 +1 @@
'1.13'

View File

@@ -1,27 +0,0 @@
import subprocess as sp
import logging
import functools
log = logging.getLogger(__name__)
from . import config
Fs = int(config.Fs) # sampling rate
bits_per_sample = 16
bytes_per_sample = bits_per_sample / 8.0
bytes_per_second = bytes_per_sample * Fs
audio_format = 'S{}_LE'.format(bits_per_sample) # PCM signed little endian
def launch(tool, fname=None, **kwargs):
fname = fname or '-'
args = [tool, fname, '-q', '-f', audio_format, '-c', '1', '-r', str(Fs)]
log.debug('Running: %r', args)
p = sp.Popen(args=args, **kwargs)
return p
# Use ALSA tools for audio playing/recording
play = functools.partial(launch, tool='aplay')
record = functools.partial(launch, tool='arecord')

View File

@@ -1,3 +0,0 @@
numpy
bitarray
reedsolo

View File

@@ -1,163 +0,0 @@
#!/usr/bin/env python
import sys
if sys.version_info.major == 2:
_stdin = sys.stdin
_stdout = sys.stdout
else:
_stdin = sys.stdin.buffer
_stdout = sys.stdout.buffer
import argparse
import logging
log = logging.getLogger('__name__')
from amodem import config
from amodem import recv
from amodem import send
from amodem import wave
from amodem import calib
null = open('/dev/null', 'wb')
def FileType(mode, process=None):
def opener(fname):
assert 'r' in mode or 'w' in mode
if process is None and fname is None:
fname = '-'
if fname is None:
if 'r' in mode:
return process(stdout=wave.sp.PIPE, stderr=null).stdout
if 'w' in mode:
return process(stdin=wave.sp.PIPE, stderr=null).stdin
if fname == '-':
if 'r' in mode:
return _stdin
if 'w' in mode:
return _stdout
return open(fname, mode)
return opener
def main():
fmt = ('Audio OFDM MODEM: {:.1f} kb/s ({:d}-QAM x {:d} carriers) '
'Fs={:.1f} kHz')
description = fmt.format(config.modem_bps / 1e3, len(config.symbols),
config.Nfreq, config.Fs / 1e3)
p = argparse.ArgumentParser(description=description)
g = p.add_mutually_exclusive_group()
g.add_argument('-v', '--verbose', default=0, action='count')
g.add_argument('-q', '--quiet', default=False, action='store_true')
subparsers = p.add_subparsers()
# Modulator
sender = subparsers.add_parser(
'send', help='modulate binary data into audio signal.')
sender.add_argument(
'-i', '--input', help='input file (use "-" for stdin).')
sender.add_argument(
'-o', '--output', help='output file (use "-" for stdout).'
' if not specified, `aplay` tool will be used.')
sender.add_argument(
'-c', '--calibrate', default=False, action='store_true')
sender.add_argument(
'-w', '--wave', default=False, action='store_true')
sender.add_argument(
'--silence-start', type=float, default=1.0,
help='seconds of silence before transmission starts')
sender.add_argument(
'--silence-stop', type=float, default=1.0,
help='seconds of silence after transmission stops')
sender.set_defaults(
main=run_send,
input_type=FileType('rb'),
output_type=FileType('wb', wave.play)
)
# Demodulator
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.')
receiver.add_argument(
'-o', '--output', help='output file (use "-" for stdout).')
receiver.add_argument(
'-c', '--calibrate', default=False, action='store_true')
receiver.add_argument(
'-w', '--wave', default=False, action='store_true')
receiver.add_argument(
'--skip', type=int, default=128,
help='skip initial N samples, due to spurious spikes')
receiver.add_argument(
'--plot', action='store_true', default=False,
help='plot results using pylab module')
receiver.set_defaults(
main=run_recv,
input_type=FileType('rb', wave.record),
output_type=FileType('wb')
)
args = p.parse_args()
if args.verbose == 0:
level, format = 'INFO', '%(message)s'
elif args.verbose == 1:
level, format = 'DEBUG', '%(message)s'
elif args.verbose >= 2:
level, format = ('DEBUG', '%(asctime)s %(levelname)-10s '
'%(message)-100s '
'%(filename)s:%(lineno)d')
if args.quiet:
level, format = 'WARNING', '%(message)s'
logging.basicConfig(level=level, format=format)
# Parsing and execution
log.debug('MODEM settings: %r', config.settings)
if getattr(args, 'plot', False):
import pylab
args.plot = pylab
args.main(args)
def join_process(process):
exitcode = 0
try:
exitcode = process.wait()
except KeyboardInterrupt:
process.kill()
exitcode = process.wait()
sys.exit(exitcode)
def run_modem(args, func):
args.input = args.input_type(args.input)
args.output = args.output_type(args.output)
func(args)
def run_send(args):
if args.calibrate:
calib.send()
elif args.wave:
join_process(wave.play(fname=args.input))
else:
run_modem(args, send.main)
def run_recv(args):
if args.calibrate:
calib.recv()
elif args.wave:
join_process(wave.record(fname=args.output))
else:
run_modem(args, recv.main)
if __name__ == '__main__':
main()

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,9 @@
#!/usr/bin/env python
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,16 +14,18 @@ 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 Fs, Ts
def main():
config = Configuration()
for fname in sys.argv[1:]:
x = common.load(open(fname, 'rb'))
t = np.arange(len(x)) * Ts
t = np.arange(len(x)) * config.Ts
pylab.figure()
pylab.title(fname)
spectrogram(t, x, Fs)
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

31
scripts/record.py Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python
import argparse
from amodem import audio
from amodem.config import Configuration
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()
size = int(config.sample_size * config.Fs) # one second of audio
while True:
dst.write(src.read(size))
def main():
p = argparse.ArgumentParser()
p.add_argument('-l', '--audio-library', default='libportaudio.so')
p.add_argument('filename')
try:
run(args=p.parse_args())
except KeyboardInterrupt:
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]))

15
scripts/resample.py Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python
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

@@ -1,16 +1,17 @@
#!/usr/bin/env python
import os
import sys
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
pwd = os.path.dirname(__file__)
from setuptools import setup
from setuptools.command.test import test as TestCommand
import os
import ast
def parse_vesrion():
cwd = os.path.dirname(__name__)
version_file = os.path.join(cwd, 'amodem', 'version.py')
tree = ast.parse(open(version_file).read())
expr, = tree.body
return expr.value.s
class PyTest(TestCommand):
@@ -19,33 +20,38 @@ class PyTest(TestCommand):
self.test_suite = True
def run_tests(self):
import sys
import pytest
sys.exit(pytest.main(['tests']))
sys.exit(pytest.main(['.']))
setup(
name="amodem",
version="1.2",
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=parse_vesrion(),
description='Audio Modem Communication Library',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
license='MIT',
url='http://github.com/romanz/amodem',
packages=['amodem'],
tests_require=['pytest'],
cmdclass={'test': PyTest},
install_requires=['numpy', 'bitarray', 'reedsolo'],
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.7",
"Programming Language :: Python :: 3.3",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Networking",
"Topic :: Communications",
'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',
'Programming Language :: Python :: 3.5',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
],
scripts=['scripts/amodem'],
entry_points={'console_scripts': ['amodem = amodem.__main__:_main']},
)

View File

@@ -1,47 +0,0 @@
from amodem import calib
from io import BytesIO
class ProcessMock(object):
def __init__(self):
self.buf = BytesIO()
self.stdin = self
self.stdout = self
def __call__(self, *args, **kwargs):
return self
def kill(self):
pass
def write(self, data):
self.buf.write(data)
if self.buf.tell() > 1e6:
raise KeyboardInterrupt
def read(self, n):
return self.buf.read(n)
def test_success():
p = ProcessMock()
calib.send(p)
p.buf.seek(0)
calib.recv(p)
def test_errors():
p = ProcessMock()
def _write(data):
raise IOError()
p.write = _write
calib.send(p)
assert p.buf.tell() == 0
def _read(data):
raise KeyboardInterrupt()
p.read = _read
calib.recv(p)
assert p.buf.tell() == 0

View File

@@ -1,85 +0,0 @@
from numpy.linalg import norm
from numpy.random import RandomState
import numpy as np
from amodem import dsp
from amodem import config
from amodem import equalizer
def assert_approx(x, y, e=1e-12):
assert norm(x - y) < e * norm(x)
def test_training():
L = 1000
t1 = equalizer.train_symbols(L)
t2 = equalizer.train_symbols(L)
assert (t1 == t2).all()
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])
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])
assert_approx(z, z_)
def test_modem():
L = 1000
sent = equalizer.train_symbols(L)
gain = config.Nfreq
x = equalizer.modulator(sent) * gain
received = equalizer.demodulator(x, L)
assert_approx(sent, received)
def test_symbols():
length = 100
gain = float(config.Nfreq)
symbols = equalizer.train_symbols(length=length)
x = equalizer.modulator(symbols) * gain
assert_approx(equalizer.demodulator(x, size=length), symbols)
den = np.array([1, -0.6, 0.1])
num = np.array([0.5])
y = dsp.lfilter(x=x, b=num, a=den)
lookahead = 2
h = equalizer.equalize_symbols(
signal=y, symbols=symbols, order=len(den), lookahead=lookahead
)
assert norm(h[:lookahead]) < 1e-12
assert_approx(h[lookahead:], den / num)
y = dsp.lfilter(x=y, b=h[lookahead:], a=[1])
z = equalizer.demodulator(y, size=length)
assert_approx(z, symbols)
def test_signal():
length = 100
x = np.sign(RandomState(0).normal(size=length))
den = np.array([1, -0.6, 0.1])
num = np.array([0.5])
y = dsp.lfilter(x=x, b=num, a=den)
lookahead = 2
h = equalizer.equalize_signal(
signal=y, expected=x, order=len(den), lookahead=lookahead)
assert norm(h[:lookahead]) < 1e-12
h = h[lookahead:]
assert_approx(h, den / num)
x_ = dsp.lfilter(x=y, b=h, a=[1])
assert_approx(x_, x)

View File

@@ -1,53 +0,0 @@
import numpy as np
from amodem import config
from amodem import recv
from amodem import train
from amodem import sampling
def test_detect():
P = sum(train.prefix)
t = np.arange(P * config.Nsym) * config.Ts
x = np.cos(2 * np.pi * config.Fc * t)
samples, amp = recv.detect(x, config.Fc)
assert abs(1 - amp) < 1e-12
x = np.cos(2 * np.pi * (2*config.Fc) * t)
try:
recv.detect(x, config.Fc)
assert False
except ValueError:
pass
def test_prefix():
symbol = np.cos(2 * np.pi * config.Fc * np.arange(config.Nsym) * config.Ts)
signal = np.concatenate([c * symbol for c in train.prefix])
sampler = sampling.Sampler(signal)
r = recv.Receiver()
freq_err = r._prefix(sampler, freq=config.Fc)
assert abs(freq_err) < 1e-16
try:
silence = 0 * signal
r._prefix(sampling.Sampler(silence), freq=config.Fc)
assert False
except ValueError:
pass
def test_find_start():
sym = np.cos(2 * np.pi * config.Fc * np.arange(config.Nsym) * config.Ts)
length = 200
prefix = postfix = np.tile(0 * sym, 50)
carrier = np.tile(sym, length)
for offset in range(10):
prefix = [0] * offset
bufs = [prefix, prefix, carrier, postfix]
buf = np.concatenate(bufs)
start = recv.find_start(buf, length*config.Nsym)
expected = offset + len(prefix)
assert expected == start

View File

@@ -1,27 +0,0 @@
from amodem import wave
import subprocess as sp
import signal
def test_launch():
p = wave.launch(tool='true', fname='fname')
assert p.wait() == 0
def test_exit():
p = wave.launch(tool='python', fname='-', stdin=sp.PIPE)
s = b'import sys; sys.exit(42)'
p.stdin.write(s)
p.stdin.close()
assert p.wait() == 42
def test_io():
p = wave.launch(tool='python', fname='-', stdin=sp.PIPE, stdout=sp.PIPE)
s = b'Hello World!'
p.stdin.write(b'print("' + s + b'")\n')
p.stdin.close()
assert p.stdout.read(len(s)) == s
def test_kill():
p = wave.launch(tool='python', fname='-', stdin=sp.PIPE, stdout=sp.PIPE)
p.kill()
assert p.wait() == -signal.SIGKILL

16
tox.ini
View File

@@ -1,5 +1,15 @@
[tox]
envlist = py27,py33,py34
envlist = py27,py34
[testenv]
deps=pytest
commands=py.test tests/
deps=
pytest
mock
pep8
coverage
pylint
six
commands=
pep8 amodem/ scripts/
pylint --extension-pkg-whitelist=numpy --report=no amodem --rcfile .pylintrc
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v
coverage report