Compare commits

...

123 Commits
v1.2 ... v1.6

Author SHA1 Message Date
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
33 changed files with 1178 additions and 892 deletions

View File

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

145
README.md
View File

@@ -2,6 +2,9 @@
[![Build Status](https://travis-ci.org/romanz/amodem.svg?branch=master)](https://travis-ci.org/romanz/amodem)
[![Coverage Status](https://coveralls.io/repos/romanz/amodem/badge.png?branch=master)](https://coveralls.io/r/romanz/amodem?branch=master)
[![Code Health](https://landscape.io/github/romanz/amodem/master/landscape.svg)](https://landscape.io/github/romanz/amodem/master)
[![Supported Python versions](https://pypip.in/py_versions/amodem/badge.svg)](https://pypi.python.org/pypi/amodem/)
[![License](https://pypip.in/license/amodem/badge.svg)](https://pypi.python.org/pypi/amodem/)
# Description
@@ -9,11 +12,11 @@ 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 sender modulates an input binary data file into an 32kHz audio,
which is played to the sound card.
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 receiver side records the transmitted audio,
which is demodulated concurrently into an output binary data file.
The process requires a single manual calibration step: the transmitter has to
find maximal output volume for its sound card, which will not saturate the
@@ -22,24 +25,45 @@ 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
- Baud rate: 1 kHz
- Symbol modulation: BPSK, 4-PSK, 16-QAM ,64-QAM
- Carriers: 2-11 kHz
This way, modem achieves 48kpbs bitrate = 6.0 kB/s.
This way, modem may achieve 60kbps bitrate = 7.5 kB/s.
A simple CRC-32 checksum is used for data integrity verification on each 1KB data frame.
A simple CRC-32 checksum is used for data integrity verification
on each 250 byte data frame.
# Installation
Run the following command (will also download and install `numpy` and `bitarray` packages):
Make sure that `numpy` and `PortAudio v19` packages are installed (on Debian):
$ sudo pip install amodem
$ sudo apt-get install python-numpy portaudio19-dev
For graphs and visualization (optional), install:
Clone and install latest version:
$ git clone https://github.com/romanz/amodem.git
$ pip install --user -e amodem
For graphs and visualization (optional), install `matplotlib` Python package.
For validation, run:
$ export BITRATE=16 # explicitly select high MODEM bit rate (assuming good SNR).
$ amodem-cli -h
usage: amodem-cli [-h] {send,recv} ...
Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
positional arguments:
{send,recv}
send modulate binary data into audio signal.
recv demodulate audio signal into binary data.
optional arguments:
-h, --help show this help message and exit
$ sudo pip install matplotlib
# Calibration
@@ -48,39 +72,98 @@ following scripts:
- On the sender's side:
```
~/sender $ amodem send --calibrate
~/sender $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
~/sender $ amodem-cli send --calibrate
```
- On the receiver's side:
```
~/receiver $ amodem recv --calibrate
~/receiver $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
~/receiver $ amodem-cli 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).
If BITRATE is not set, the MODEM will use 1 kbps settings (single frequency with BPSK modulation).
# Testing
Change the sender computer's output audio level, until
all frequencies are received well:
```
1000 Hz: good signal
2000 Hz: good signal
3000 Hz: good signal
4000 Hz: good signal
5000 Hz: good signal
6000 Hz: good signal
7000 Hz: good signal
8000 Hz: good signal
```
- Prepare the sender (generate random binary data file to be sent):
If the signal is "too weak", increase the sender's output audio level.
If the signal is "too strong", decrease the sender's output audio level.
If the signal is "too noisy", the SNR is probably too low: decrease the
background noise or increase the signal (without causing saturation).
# Usage
- Prepare the sender (generate a random binary data file to be sent):
```
~/sender $ dd if=/dev/urandom of=data.tx bs=125kB count=1 status=none
~/sender $ dd if=/dev/urandom of=data.tx bs=16kB count=1 status=none
~/sender $ sha256sum data.tx
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.tx
```
- Start the receiver:
- Start the receiver (will wait for the sender to start):
```
~/receiver $ amodem recv >data.rx
~/receiver $ amodem-cli recv -vv -i data.rx
```
- Start the sender:
- Start the sender (will modulate the data and start the transmission):
```
~/sender $ amodem send <data.tx
~/sender $ amodem-cli send -vv -o data.tx
```
- After the receiver has finished, verify that the file's hash is the same:
- A similar log should be emitted by the sender:
```
2014-10-23 09:46:36,116 DEBUG MODEM settings: {'F0': 1000.0, 'Nfreq': 8, 'Fs': 32000.0, 'Npoints': 64, 'Tsym': 0.001} amodem:126
2014-10-23 09:46:36,116 DEBUG Running: ['aplay', '-', '-q', '-f', 'S16_LE', '-c', '1', '-r', '32000'] wave.py:20
2014-10-23 09:46:36,665 INFO Sending 2.150 seconds of training audio send.py:69
2014-10-23 09:46:36,665 INFO Starting modulation: <48.000 kbps, 64-QAM, 8 carriers> send.py:74
2014-10-23 09:46:37,735 DEBUG Sent 6.0 kB send.py:56
2014-10-23 09:46:38,794 DEBUG Sent 12.0 kB send.py:56
2014-10-23 09:46:39,440 INFO Sent 16.384 kB @ 2.754 seconds send.py:79
```
- A similar log should be emitted by the receiver:
```
2014-10-23 09:46:36,116 DEBUG MODEM settings: {'F0': 1000.0, 'Nfreq': 8, 'Fs': 32000.0, 'Npoints': 64, 'Tsym': 0.001} amodem:126
2014-10-23 09:46:36,238 DEBUG Running: ['arecord', '-', '-q', '-f', 'S16_LE', '-c', '1', '-r', '32000'] wave.py:20
2014-10-23 09:46:36,408 DEBUG Skipping 0.128 seconds recv.py:275
2014-10-23 09:46:36,409 INFO Waiting for carrier tone: 1.0 kHz recv.py:282
2014-10-23 09:46:37,657 INFO Carrier detected at ~886.0 ms @ 1.0 kHz: coherence=99.996%, amplitude=0.475 recv.py:40
2014-10-23 09:46:37,657 DEBUG Buffered 1000 ms of audio recv.py:64
2014-10-23 09:46:37,660 DEBUG Carrier starts at 9.531 ms recv.py:73
2014-10-23 09:46:38,119 DEBUG Prefix OK recv.py:108
2014-10-23 09:46:38,153 DEBUG Current phase on carrier: -0.497 recv.py:121
2014-10-23 09:46:38,153 DEBUG Frequency error: 0.02 ppm recv.py:123
2014-10-23 09:46:38,682 DEBUG 1.0 kHz: SNR = 34.20 dB recv.py:165
2014-10-23 09:46:38,715 DEBUG 2.0 kHz: SNR = 35.05 dB recv.py:165
2014-10-23 09:46:38,766 DEBUG 3.0 kHz: SNR = 35.52 dB recv.py:165
2014-10-23 09:46:38,803 DEBUG 4.0 kHz: SNR = 35.65 dB recv.py:165
2014-10-23 09:46:38,837 DEBUG 5.0 kHz: SNR = 35.03 dB recv.py:165
2014-10-23 09:46:38,869 DEBUG 6.0 kHz: SNR = 35.05 dB recv.py:165
2014-10-23 09:46:38,907 DEBUG 7.0 kHz: SNR = 34.80 dB recv.py:165
2014-10-23 09:46:38,943 DEBUG 8.0 kHz: SNR = 33.74 dB recv.py:165
2014-10-23 09:46:38,977 INFO Starting demodulation: <48.000 kbps, 64-QAM, 8 carriers> recv.py:197
2014-10-23 09:46:39,619 DEBUG Got 6.0 kB, realtime: 64.18%, drift: +0.02 ppm recv.py:215
2014-10-23 09:46:40,538 DEBUG Got 12.0 kB, realtime: 78.03%, drift: +0.02 ppm recv.py:215
2014-10-23 09:46:41,306 DEBUG EOF frame detected framing.py:60
2014-10-23 09:46:41,306 DEBUG Demodulated 16.520 kB @ 2.329 seconds (84.6% realtime) recv.py:244
2014-10-23 09:46:41,306 INFO Received 16.384 kB @ 2.329 seconds = 7.034 kB/s recv.py:247
```
- After the receiver has finished, verify the received file's hash:
```
~/receiver $ sha256sum data.rx
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.rx
@@ -90,5 +173,13 @@ while the **coherence** is 1.0 (to avoid saturation).
Make sure that `matplotlib` package is installed, and run (at the receiver side):
```
~/receiver $ amodem recv --plot >data.rx
~/receiver $ amodem-cli recv --plot -o data.rx
```
# Donations
Want to donate? Feel free.
Send to [1C1snTrkHAHM5XnnfuAtiTBaA11HBxjJyv](https://blockchain.info/address/1C1snTrkHAHM5XnnfuAtiTBaA11HBxjJyv).
Thanks :)

191
amodem-cli Executable file
View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
import os
import sys
import zlib
import logging
import argparse
if sys.version_info.major == 2:
_stdin = sys.stdin
_stdout = sys.stdout
else:
_stdin = sys.stdin.buffer
_stdout = sys.stdout.buffer
try:
import argcomplete
except ImportError:
argcomplete = None
log = logging.getLogger('__name__')
from amodem import recv, send, calib, audio
from amodem.config import bitrates
bitrate = os.environ.get('BITRATE', 1)
config = bitrates.get(int(bitrate))
class Compressor(object):
def __init__(self, stream):
self.obj = zlib.compressobj()
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()
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, audio_interface=None):
def opener(fname):
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:
return audio_interface.recorder()
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 main():
fmt = ('Audio OFDM MODEM: {0:.1f} kb/s ({1:d}-QAM x {2:d} carriers) '
'Fs={3:.1f} kHz')
description = fmt.format(config.modem_bps / 1e3, len(config.symbols),
config.Nfreq, config.Fs / 1e3)
interface = audio.Interface('libportaudio.so', config=config)
p = argparse.ArgumentParser(description=description)
subparsers = p.add_subparsers()
def wrap(cls, stream, enable):
return cls(stream) if enable else stream
# Modulator
sender = subparsers.add_parser(
'send', help='modulate binary data into audio signal.')
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.set_defaults(
main=lambda config, args: send.main(
config, src=wrap(Compressor, args.src, args.zip), dst=args.dst
),
calib=lambda config, args: calib.send(
config, dst=args.dst
),
input_type=FileType('rb'),
output_type=FileType('wb', interface)
)
# 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(
'-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: recv.main(
config, src=args.src, dst=wrap(Decompressor, args.dst, args.zip),
pylab=args.pylab, dump_audio=args.dump
),
calib=lambda config, args: calib.recv(
config, src=args.src, verbose=args.verbose
),
input_type=FileType('rb', interface),
output_type=FileType('wb')
)
for sub in subparsers.choices.values():
sub.add_argument('-z', '--zip', default=False, action='store_true')
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)
with interface:
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(description)
args.pylab = None
if getattr(args, 'plot', False):
import pylab
args.pylab = pylab
args.src = args.input_type(args.input)
args.dst = args.output_type(args.output)
if args.calibrate:
args.calib(config=config, args=args)
else:
return args.main(config=config, args=args)
if __name__ == '__main__':
success = main()
sys.exit(0 if success else 1)

View File

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

111
amodem/audio.py Normal file
View File

@@ -0,0 +1,111 @@
import ctypes
import logging
log = logging.getLogger(__name__)
class Interface(object):
def __init__(self, name, config, debug=False):
self.debug = bool(debug)
self.lib = ctypes.CDLL(name)
self.config = config
self.streams = []
assert self._error_string(0) == 'Success'
def _error_string(self, code):
return self.call('GetErrorText', code, restype=ctypes.c_char_p)
def call(self, name, *args, **kwargs):
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(lib=self, config=self.config, read=True)
def player(self):
return Stream(lib=self, config=self.config, write=True)
class Stream(object):
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, lib, config, read=False, write=False):
self.lib = lib
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
assert config.bits_per_sample == 16 # just to make sure :)
read = bool(read)
write = bool(write)
assert read != write
direction = 'Input' if read else 'Output'
api_name = 'GetDefault{0}Device'.format(direction)
index = lib.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=0.1, # 100ms should be good enough
hostApiSpecificStreamInfo=None)
self.lib.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(config.samples_per_buffer),
ctypes.c_ulong(0), # no flags (paNoFlag)
self.stream_callback,
self.user_data)
self.lib.streams.append(self)
self.lib.call('StartStream', self.stream)
def close(self):
if self.stream:
self.lib.call('StopStream', self.stream)
self.lib.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)
self.lib.call('ReadStream', self.stream, buf, frames)
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.lib.call('WriteStream', self.stream, buf, frames)

View File

@@ -1,66 +1,88 @@
import numpy as np
import itertools
import logging
import sys
log = logging.getLogger(__name__)
from . import common
from . import config
from . import wave
CALIBRATION_SYMBOLS = int(1.0 * config.Fs)
from . import dsp
from . import sampling
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
def send(config, dst):
calibration_symbols = int(1.0 * config.Fs)
t = np.arange(0, calibration_symbols) * config.Ts
signals = [np.sin(2 * np.pi * f * t) for f in config.frequencies]
signals = [common.dumps(s) for s in signals]
try:
while True:
fd.write(signal)
for signal in itertools.cycle(signals):
dst.write(signal)
except ALLOWED_EXCEPTIONS:
pass
finally:
p.kill()
FRAME_LENGTH = 100 * config.Nsym
def frame_iter(config, src, frame_length):
frame_size = frame_length * config.Nsym * config.sample_size
omegas = 2 * np.pi * config.frequencies / config.Fs
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()
while True:
data = src.read(frame_size)
if len(data) < frame_size:
return
data = common.loads(data)
frame = data - np.mean(data)
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))
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
freq = config.frequencies[max_index]
rms = abs(max_coeff)
total = np.sqrt(np.dot(frame, frame) / (0.5 * len(t)))
coherency = rms / total
log(fmt.format(freq / 1e3, 100 * coherency, rms, total, peak))
fmt = '{:4.0f} kHz @ {:6.2f}% : RMS = {:.4f}, Total = {:.4f}, Peak = {:.4f}\n'
def detector(config, src, frame_length=200):
states = [True]
errors = ['weak', 'strong', 'noisy']
try:
for coeffs, peak, total in frame_iter(config, src, frame_length):
max_index = np.argmax(coeffs)
freq = config.frequencies[max_index]
rms = abs(coeffs[max_index])
coherency = rms / total
flags = [rms > 0.1, peak < 1.0, coherency > 0.99]
states.append(all(flags))
states = states[-2:]
message = 'good signal'
error = not any(states)
if error:
message = 'too {0} signal'.format(errors[flags.index(False)])
yield common.AttributeHolder(dict(
freq=freq, rms=rms, peak=peak, coherency=coherency,
total=total, error=error, message=message
))
except ALLOWED_EXCEPTIONS:
pass
def recv(config, src, verbose=False):
fmt = '{0.freq:6.0f} Hz: {0.message:s}'
if verbose:
fields = ['peak', 'total', 'rms', 'coherency']
fmt += ''.join(', {0}={{0.{0}:.4f}}'.format(f) for f in fields)
for result in detector(config=config, src=src):
msg = fmt.format(result)
if not result.error:
log.info(msg)
else:
log.error(msg)

View File

@@ -5,17 +5,6 @@ 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):
@@ -28,14 +17,12 @@ def loads(data):
return x
def dumps(sym, n=1):
def dumps(sym):
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):
offset = 0
data = iter(data)
@@ -48,7 +35,7 @@ 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
@@ -85,3 +72,14 @@ class Dummy(object):
def __call__(self, *args, **kwargs):
return self
class AttributeHolder(object):
def __init__(self, d):
self.__dict__.update(d)
def __repr__(self):
items = sorted(self.__dict__.items())
args = ', '.join('{0}={1}'.format(k, v) for k, v in items)
return '{0}({1})'.format(self.__class__.__name__, args)

View File

@@ -1,42 +1,66 @@
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]
Nfreq = 8 # number of frequencies used
Npoints = 64
F0 = 1e3
Nsym = int(Tsym / Ts)
baud = int(1/Tsym)
# audio config
bits_per_sample = 16
sample_size = bits_per_sample // 8
samples_per_buffer = 4096
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 = 1.0
silence_stop = 1.0
# receiver config
skip_start = 0.1
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
self.Ts = 1.0 / self.Fs
self.Fsym = 1 / self.Tsym
self.frequencies = self.F0 + np.arange(self.Nfreq) * self.Fsym
self.carrier_index = 0
self.Fc = self.frequencies[self.carrier_index]
self.Nsym = int(self.Tsym / self.Ts)
self.baud = int(1.0 / self.Tsym)
bits_per_symbol = np.log2(self.Npoints)
assert int(bits_per_symbol) == bits_per_symbol
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(F0=8e3, Npoints=2, Nfreq=1),
2: Configuration(F0=8e3, Npoints=4, Nfreq=1),
4: Configuration(F0=8e3, Npoints=16, Nfreq=1),
8: Configuration(F0=8e3, Npoints=16, Nfreq=2),
16: Configuration(F0=6e3, Npoints=16, Nfreq=4),
32: Configuration(F0=3e3, Npoints=16, Nfreq=8),
48: Configuration(F0=3e3, Npoints=64, Nfreq=8),
54: Configuration(F0=3e3, Npoints=64, Nfreq=9),
60: Configuration(F0=2e3, Npoints=64, Nfreq=10),
}
fastest = lambda: bitrates[max(bitrates)]
slowest = lambda: bitrates[min(bitrates)]

116
amodem/detect.py Normal file
View File

@@ -0,0 +1,116 @@
import numpy as np
import logging
import itertools
import collections
log = logging.getLogger(__name__)
from . import dsp
from . import equalizer
from . import common
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)
TIMEOUT = 10.0 # [seconds]
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 = self.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
x = np.concatenate(tuple(bufs)[-self.CARRIER_THRESHOLD:-1])
Hc = dsp.exp_iwt(-self.omega, len(x))
amplitude = np.abs(np.dot(Hc, x) / (0.5 * len(x)))
start_time = begin * self.Tsym / self.Nsym
log.info('Carrier detected at ~%.1f ms @ %.1f kHz:'
' coherence=%.3f%%, amplitude=%.3f',
start_time * 1e3, self.freq / 1e3,
np.abs(dsp.coherence(x, self.omega)) * 100, amplitude)
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, duration=self.CARRIER_DURATION)
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, duration):
filt = dsp.FIR(dsp.exp_iwt(self.omega, self.Nsym))
p = np.abs(list(filt(buf))) ** 2
p = np.cumsum(p)[self.Nsym-1:]
p = np.concatenate([[0], p])
length = (duration - 1) * self.Nsym
correlations = np.abs(p[length:] - p[:-length])
offset = np.argmax(correlations)
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.debug('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)
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: {0:.3f} ppm'.format(freq_err * 1e6))
return amplitude, freq_err

View File

@@ -1,13 +1,23 @@
import numpy as np
from numpy import linalg
import logging
log = logging.getLogger(__name__)
from . import config
from . import common
class FIR(object):
def __init__(self, h):
self.h = np.array(h)
self.x_state = [0] * len(self.h)
def __call__(self, x):
x_ = self.x_state
h = self.h
for v in x:
x_ = [v] + x_[:-1]
yield np.dot(x_, h)
self.x_state = x_
class IIR(object):
def __init__(self, b, a):
self.b = np.array(b) / a[0]
@@ -28,45 +38,16 @@ class IIR(object):
self.x_state, self.y_state = x_, y_
class FIR(object):
def __init__(self, h):
self.h = np.array(h)
self.x_state = [0] * len(self.h)
def __call__(self, x):
x_ = self.x_state
h = self.h
for v in x:
x_ = [v] + x_[:-1]
yield np.dot(x_, h)
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 +55,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 +64,17 @@ 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 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
@@ -132,7 +112,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):
@@ -151,9 +131,3 @@ class MODEM(object):
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__

View File

@@ -2,7 +2,6 @@ import numpy as np
from numpy.linalg import lstsq
from amodem import dsp
from amodem import config
from amodem import sampling
import itertools
@@ -11,62 +10,38 @@ 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, seed=0):
r = random.Random(seed)
choose = lambda: [r.choice(_constellation) for j in range(self.Nfreq)]
return np.array([choose() for _ in range(length)])
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
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 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)])
def train(signal, expected, order, lookahead=0):
signal = [np.zeros(order-1), signal, np.zeros(lookahead)]
signal = np.concatenate(signal)
length = len(expected)
A = []
@@ -80,6 +55,11 @@ def equalize_signal(signal, expected, order, lookahead=0):
A = np.concatenate(A, axis=0)
b = np.array(b)
h, residuals, rank, sv = lstsq(A, b)
h = lstsq(A, b)[0]
h = h[::-1].real
return h
prefix = [1]*400 + [0]*50
equalizer_length = 500
silence_length = 100

View File

@@ -1,6 +1,4 @@
''' Reed-Solomon CODEC. '''
from . import common
import bitarray
import functools
import itertools
@@ -9,23 +7,22 @@ import struct
import logging
log = logging.getLogger(__name__)
_crc32 = lambda x, mask: binascii.crc32(x) & mask
_checksum_func = lambda x: binascii.crc32(bytes(x)) & 0xFFFFFFFF
# (so the result will be unsigned on Python 2/3)
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 +30,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 +39,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 +50,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 +59,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,20 +83,34 @@ 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

View File

@@ -2,7 +2,6 @@ import numpy as np
import logging
import itertools
import functools
import collections
import time
log = logging.getLogger(__name__)
@@ -10,89 +9,31 @@ 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)
from . import detect
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.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
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)
@@ -101,54 +42,40 @@ class Receiver(object):
bits = np.array(sliced, dtype=int)
self.plt.subplot(122)
self.plt.plot(np.abs(S))
self.plt.plot(train.prefix)
if any(bits != train.prefix):
self.plt.plot(equalizer.prefix)
if any(bits != equalizer.prefix):
raise ValueError('Incorrect prefix')
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,
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)
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
@@ -161,17 +88,13 @@ class Receiver(object):
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 _demodulate(self, sampler, symbols):
streams = []
symbol_list = []
errors = {}
@@ -179,82 +102,86 @@ class Receiver(object):
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
bits = self.modem.decode(S, freq_handler) # list of bit tuples
streams.append(bits) # stream per frequency
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
log.info('Starting demodulation')
for i, block in enumerate(common.izip(streams), 1):
for bits in block:
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(i, 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
)
def _update_sampler(self, iter_index, 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()
def start(self, signal, freqs, gain=1.0):
sampler = sampling.Sampler(signal, sampling.Interpolator())
duration = time.time() - self.stats['rx_start']
sampler.freq -= 0.01 * err * self.Tsym
sampler.offset -= err
log.debug(
'Got %10.3f kB, realtime: %6.2f%%, drift: %+5.2f ppm',
self.stats['rx_bits'] / 8e3,
duration * 100.0 / (iter_index * self.Tsym),
(1.0 - sampler.freq) * 1e6
)
freq_err = self._prefix(sampler, freq=freqs[0], gain=gain)
sampler.freq -= freq_err
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=11, lookahead=5)
filt = self._train(sampler, order=20, lookahead=20)
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
data = framing.decode(bitstream)
for chunk in common.iterate(data=data, size=256,
truncate=False, func=bytearray):
output.write(chunk)
self.size += len(chunk)
self.output_size += len(chunk)
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, '.')
@@ -267,25 +194,45 @@ class Receiver(object):
self.plt.title(title)
def main(args):
reader = stream.Reader(args.input, data_type=common.loads)
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
def main(config, src, dst, dump_audio=None, pylab=None):
if dump_audio:
src = Dumper(src, dump_audio)
reader = stream.Reader(src, 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))
log.debug('Skipping %.3f seconds', config.skip_start)
common.take(signal, int(config.skip_start * config.Fs))
reader.check = common.check_saturation
receiver = Receiver(plt=args.plot)
success = False
pylab = pylab or common.Dummy()
detector = detect.Detector(config=config, pylab=pylab)
receiver = Receiver(config=config, pylab=pylab)
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
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 Exception:
log.exception('Decoding failed')
receiver.report()
return success
return False
finally:
dst.flush()
receiver.report()

View File

@@ -1,51 +1,56 @@
#!/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 = (1 + np.cos(0.5 * np.pi * u / N)) / 2.0 # (Hann window)
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)
@@ -58,14 +63,16 @@ class Sampler(object):
# offset = k + (j / self.resolution)
k = int(offset) # integer part
j = int((offset - k) * self.resolution) # fractional part
coeffs = self.filt[j]
coeffs = self.filt[j] # choose correct filter phase
end = k + self.width
# process input until all buffer is full with samples
while self.index < end:
self.buff[:-1] = self.buff[1:]
self.buff[-1] = next(self.src) # throws StopIteration
self.index += 1
self.offset += self.freq
# apply interpolation filter
frame[frame_index] = np.dot(coeffs, self.buff)
count = frame_index + 1
except StopIteration:
@@ -75,7 +82,6 @@ class Sampler(object):
def resample(src, dst, df=0.0):
from . import common
x = common.load(src)
sampler = Sampler(x, Interpolator())
sampler.freq += df

View File

@@ -4,79 +4,74 @@ 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):
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):
def write(self, sym):
sym = np.array(sym)
data = common.dumps(sym, n)
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)
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)
def main(args):
writer = Writer(args.output)
def main(config, src, dst):
sender = Sender(dst, config=config)
Fs = config.Fs
# pre-padding audio with silence
writer.write(np.zeros(int(config.Fs * args.silence_start)))
sender.write(np.zeros(int(Fs * config.silence_start)))
writer.start()
sender.start()
training_size = writer.offset
training_duration = training_size / wave.bytes_per_second
log.info('Sending %.3f seconds of training audio', training_duration)
training_duration = sender.offset
log.info('Sending %.3f seconds of training audio', training_duration / Fs)
reader = stream.Reader(args.input, bufsize=(64 << 10), eof=True)
reader = stream.Reader(src, eof=True)
data = itertools.chain.from_iterable(reader)
bits = framing.encode(data)
log.info('Starting modulation: %s', modem)
writer.modulate(bits=bits)
log.info('Starting modulation')
sender.modulate(bits=bits)
data_size = writer.offset - training_size
data_duration = sender.offset - training_duration
log.info('Sent %.3f kB @ %.3f seconds',
reader.total / 1e3, data_size / wave.bytes_per_second)
reader.total / 1e3, data_duration / Fs)
# post-padding audio with silence
writer.write(np.zeros(int(config.Fs * args.silence_stop)))
sender.write(np.zeros(int(Fs * config.silence_stop)))
return True

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 = 4096
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,10 +36,7 @@ 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)

View File

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

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 +1,2 @@
numpy
bitarray
reedsolo
argcomplete

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

View File

@@ -14,13 +14,14 @@ def spectrogram(t, x, Fs, NFFT=256):
if __name__ == '__main__':
import sys
from amodem import common
from amodem.config import Fs, Ts
from amodem.config import Configuration
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()

View File

@@ -1,14 +1,5 @@
#!/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
@@ -19,12 +10,13 @@ class PyTest(TestCommand):
self.test_suite = True
def run_tests(self):
import sys
import pytest
sys.exit(pytest.main(['tests']))
setup(
name="amodem",
version="1.2",
version="1.6",
description="Audio Modem Communication Library",
author="Roman Zeyde",
author_email="roman.zeyde@gmail.com",
@@ -33,7 +25,7 @@ setup(
packages=['amodem'],
tests_require=['pytest'],
cmdclass={'test': PyTest},
install_requires=['numpy', 'bitarray', 'reedsolo'],
install_requires=['numpy'],
platforms=['POSIX'],
classifiers=[
"Development Status :: 4 - Beta",
@@ -41,11 +33,14 @@ setup(
"Intended Audience :: Information Technology",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Networking",
"Topic :: Communications",
],
scripts=['scripts/amodem'],
scripts=['amodem-cli'],
)

35
tests/test_audio.py Normal file
View File

@@ -0,0 +1,35 @@
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: 'Error' if code else '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(
name='portaudio', config=config.fastest(), debug=True
)
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)

View File

@@ -1,17 +1,26 @@
from amodem import calib
from amodem import common
from amodem import config
config = config.fastest()
from io import BytesIO
import numpy as np
import pytest
class ProcessMock(object):
def __init__(self):
self.buf = BytesIO()
self.stdin = self
self.stdout = self
self.bytes_per_sample = 2
def __call__(self, *args, **kwargs):
def launch(self, *args, **kwargs):
return self
__call__ = launch
def kill(self):
pass
@@ -26,22 +35,46 @@ class ProcessMock(object):
def test_success():
p = ProcessMock()
calib.send(p)
calib.send(config, p)
p.buf.seek(0)
calib.recv(p)
calib.recv(config, p)
def test_errors():
p = ProcessMock()
def _write(data):
raise IOError()
p.write = _write
calib.send(p)
class WriteError(ProcessMock):
def write(self, data):
raise IOError()
p = WriteError()
calib.send(config, p)
assert p.buf.tell() == 0
def _read(data):
raise KeyboardInterrupt()
p.read = _read
calib.recv(p)
class ReadError(ProcessMock):
def read(self, n):
raise KeyboardInterrupt()
p = ReadError()
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 not r.error
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

View File

@@ -6,7 +6,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 +47,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_holder():
d = {'x': 1, 'y': 2.3}
a = common.AttributeHolder(d)
assert a.x == d['x']
assert a.y == d['y']
assert repr(a) == 'AttributeHolder(x=1, y=2.3)'

61
tests/test_detect.py Normal file
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-16
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, length)
expected = offset + len(prefix)
assert expected == start

View File

@@ -2,8 +2,9 @@ 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
config = config.fastest()
import random
import itertools
@@ -28,42 +29,13 @@ def test_filter():
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

View File

@@ -3,8 +3,9 @@ from numpy.random import RandomState
import numpy as np
from amodem import dsp
from amodem import config
from amodem import equalizer
from amodem import config
config = config.fastest()
def assert_approx(x, y, e=1e-12):
@@ -13,8 +14,9 @@ def assert_approx(x, y, e=1e-12):
def test_training():
L = 1000
t1 = equalizer.train_symbols(L)
t2 = equalizer.train_symbols(L)
e = equalizer.Equalizer(config)
t1 = e.train_symbols(L)
t2 = e.train_symbols(L)
assert (t1 == t2).all()
@@ -35,37 +37,14 @@ def test_commutation():
def test_modem():
L = 1000
sent = equalizer.train_symbols(L)
e = equalizer.Equalizer(config)
sent = e.train_symbols(L)
gain = config.Nfreq
x = equalizer.modulator(sent) * gain
received = equalizer.demodulator(x, L)
x = e.modulator(sent) * gain
received = e.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))
@@ -74,7 +53,7 @@ def test_signal():
y = dsp.lfilter(x=x, b=num, a=den)
lookahead = 2
h = equalizer.equalize_signal(
h = equalizer.train(
signal=y, expected=x, order=len(den), lookahead=lookahead)
assert norm(h[:lookahead]) < 1e-12

View File

@@ -34,16 +34,19 @@ def test_main(data):
decoded = framing.decode(encoded)
assert bytearray(decoded) == data
def test_fail():
encoded = list(framing.encode(''))
encoded[-1] = not encoded[-1]
with pytest.raises(ValueError):
list(framing.decode(encoded))
def test_missing():
f = framing.Framer()
with pytest.raises(ValueError):
list(f.decode(b'\x00'))
list(f.decode(b''))
with pytest.raises(ValueError):
list(f.decode(b'\x01\x02\x03\x04'))
list(f.decode(b'\x01'))
with pytest.raises(ValueError):
list(f.decode(b'\xff'))

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

@@ -8,6 +8,8 @@ from amodem import recv
from amodem import common
from amodem import dsp
from amodem import sampling
from amodem import config
config = config.fastest()
import logging
logging.basicConfig(level=logging.DEBUG,
@@ -27,8 +29,7 @@ class Args(object):
def run(size, chan=None, df=0, success=True):
tx_data = os.urandom(size)
tx_audio = BytesIO()
send.main(Args(silence_start=1, silence_stop=1,
input=BytesIO(tx_data), output=tx_audio))
send.main(config=config, src=BytesIO(tx_data), dst=tx_audio)
data = tx_audio.getvalue()
data = common.loads(data)
@@ -43,8 +44,11 @@ def run(size, chan=None, df=0, success=True):
rx_audio = BytesIO(data)
rx_data = BytesIO()
result = recv.main(Args(skip=0, input=rx_audio, output=rx_data))
d = BytesIO()
result = recv.main(config=config, src=rx_audio, dst=rx_data,
dump_audio=d)
rx_data = rx_data.getvalue()
assert data.startswith(d.getvalue())
assert result == success
if success:
@@ -61,17 +65,18 @@ def test_small(small_size):
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, 2e3)])
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():

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

15
tox.ini
View File

@@ -1,5 +1,14 @@
[tox]
envlist = py27,py33,py34
envlist = py27,py34
[testenv]
deps=pytest
commands=py.test tests/
deps=
pytest
mock
pep8
coverage
pylint
commands=
pep8 amodem/ scripts/ tests/ amodem-cli
pylint --disable no-member --report=no amodem
coverage run --source amodem/ -m py.test tests/
coverage report