mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
279 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e637e701df | ||
|
|
5be6684fa6 | ||
|
|
0876be18e4 | ||
|
|
3b8f913fcb | ||
|
|
45d4ccae76 | ||
|
|
7cb05aaaf7 | ||
|
|
3911f16bd7 | ||
|
|
b5f8e07ae2 | ||
|
|
835841bf2e | ||
|
|
c70b3c9dc7 | ||
|
|
544dd28ddd | ||
|
|
c887dbf4e6 | ||
|
|
bf6282127c | ||
|
|
65f2559a19 | ||
|
|
1c6f8894a5 | ||
|
|
c19d11744f | ||
|
|
4d60cac7ed | ||
|
|
a3adda625b | ||
|
|
a44f55a608 | ||
|
|
e0a38bf5d7 | ||
|
|
6b67721374 | ||
|
|
20efa6a688 | ||
|
|
327a7f9d0f | ||
|
|
7b0ba1714f | ||
|
|
ec76a1394c | ||
|
|
bf7b59db11 | ||
|
|
f51cf8c4db | ||
|
|
aec1648ae7 | ||
|
|
2ad3ffced4 | ||
|
|
318081fca4 | ||
|
|
f82a4f4a39 | ||
|
|
8d72621b9b | ||
|
|
2e6196416b | ||
|
|
01c78bae8f | ||
|
|
c56f696e9e | ||
|
|
3fe515ea59 | ||
|
|
55e7152da6 | ||
|
|
66c639b597 | ||
|
|
a4ebf68223 | ||
|
|
ccfe5c00cf | ||
|
|
f476055cf2 | ||
|
|
fce906df0a | ||
|
|
129b9a4ad0 | ||
|
|
0a4584f1b8 | ||
|
|
5d6b47574d | ||
|
|
ef7467efd7 | ||
|
|
449d4eac0a | ||
|
|
66148650ed | ||
|
|
3a33338425 | ||
|
|
18462289f8 | ||
|
|
c5f8b48554 | ||
|
|
7b4b2dd7ef | ||
|
|
6d317df465 | ||
|
|
daca119c6f | ||
|
|
13cbd82d5a | ||
|
|
37ee53d8e4 | ||
|
|
66cecb9be4 | ||
|
|
de426c6187 | ||
|
|
c637b3e914 | ||
|
|
5cf0fa4e27 | ||
|
|
eb6ace3cc3 | ||
|
|
e94fd0e2ff | ||
|
|
3b4b64253d | ||
|
|
ddfdf2f7f4 | ||
|
|
9b2ebf05df | ||
|
|
c06e842eb7 | ||
|
|
23ce7bba08 | ||
|
|
03a600ddd2 | ||
|
|
1aa41db6cb | ||
|
|
078e429340 | ||
|
|
da636212e8 | ||
|
|
65c0892367 | ||
|
|
34a892e72c | ||
|
|
a73b09c186 | ||
|
|
cac280cf3f | ||
|
|
52ee71fad1 | ||
|
|
8fe7f1d716 | ||
|
|
6b77534bc2 | ||
|
|
964b5e0df4 | ||
|
|
06cc8918f0 | ||
|
|
6a2e320808 | ||
|
|
97e992ea56 | ||
|
|
61dc35c122 | ||
|
|
e06cb37e2b | ||
|
|
d34d2fdbea | ||
|
|
42ad312418 | ||
|
|
e0718596e2 | ||
|
|
fdf6e7e882 | ||
|
|
90dd3e55f0 | ||
|
|
b3619a75ba | ||
|
|
1ddc693683 | ||
|
|
b3804a42be | ||
|
|
c0634a34d0 | ||
|
|
807c03a8e8 | ||
|
|
e5ff6297b1 | ||
|
|
07a3d5cc98 | ||
|
|
d81ec630a5 | ||
|
|
4cebb06e11 | ||
|
|
a43e674fbe | ||
|
|
ffc9ece45c | ||
|
|
e374a65920 | ||
|
|
9709ffc523 | ||
|
|
8295b0865d | ||
|
|
cbdf4d1616 | ||
|
|
a2b220c8e4 | ||
|
|
5b94d7fd49 | ||
|
|
cbf14a5153 | ||
|
|
d09391f43f | ||
|
|
6361f8a257 | ||
|
|
b39334bfe8 | ||
|
|
1c13671a4c | ||
|
|
6a35820155 | ||
|
|
1b64e2874a | ||
|
|
537db23b34 | ||
|
|
ef8a75f10e | ||
|
|
53559ff8df | ||
|
|
1988144752 | ||
|
|
1d5d564f4d | ||
|
|
5b6d1881ab | ||
|
|
221c7c03b9 | ||
|
|
0e9d8c04da | ||
|
|
66b8e377ac | ||
|
|
bfbb7f3588 | ||
|
|
32e01dd4e0 | ||
|
|
200ec3b0c2 | ||
|
|
6a37dd74ec | ||
|
|
dc8e876f13 | ||
|
|
a208151fbb | ||
|
|
ef40b498bb | ||
|
|
db3ae11bc0 | ||
|
|
4c4027b84a | ||
|
|
4193fa88d6 | ||
|
|
5275f25d14 | ||
|
|
33385b294e | ||
|
|
128675955b | ||
|
|
c4d583612b | ||
|
|
779ba09c46 | ||
|
|
7e8f1e8994 | ||
|
|
3450c2c570 | ||
|
|
95ed9e0eda | ||
|
|
95bbfbfe98 | ||
|
|
066c27843e | ||
|
|
323145c44b | ||
|
|
f523b7579c | ||
|
|
3b6a54150f | ||
|
|
2d202cf587 | ||
|
|
ee7db32418 | ||
|
|
81165799fd | ||
|
|
c7251b641e | ||
|
|
9036069323 | ||
|
|
ff8427f5f9 | ||
|
|
35487966d8 | ||
|
|
5c4849be1c | ||
|
|
cbcf2860b9 | ||
|
|
d03205fe02 | ||
|
|
603fc75db4 | ||
|
|
2727ff014a | ||
|
|
fe1714a0bc | ||
|
|
cb8ce9e8ec | ||
|
|
b4dc0922eb | ||
|
|
36f52f9346 | ||
|
|
babd4a5438 | ||
|
|
904966365f | ||
|
|
aa0dd2c2c8 | ||
|
|
1da258ebf8 | ||
|
|
5401206178 | ||
|
|
9b6160ec43 | ||
|
|
04b0142955 | ||
|
|
9dbfcaa870 | ||
|
|
af59eb5bdf | ||
|
|
e4267f236b | ||
|
|
cfc6de9eb0 | ||
|
|
2d890339e2 | ||
|
|
2ee79870c5 | ||
|
|
2bb4956439 | ||
|
|
fd8dc1d8b7 | ||
|
|
5bf2d0f566 | ||
|
|
b91f51df12 | ||
|
|
fcd58d404d | ||
|
|
4ca1cdf23a | ||
|
|
8e8d43b041 | ||
|
|
c2a4bfbd5e | ||
|
|
ae7d742ee4 | ||
|
|
c2c1b89a0e | ||
|
|
750eb5428f | ||
|
|
15f330330c | ||
|
|
96a1abb714 | ||
|
|
a83888ff02 | ||
|
|
a866301774 | ||
|
|
3dcd9f4ccc | ||
|
|
3b1d193b0b | ||
|
|
318a0644de | ||
|
|
807cbc31a2 | ||
|
|
004ad2403f | ||
|
|
a1ad9ff32c | ||
|
|
f086bbfdeb | ||
|
|
bd329c19d0 | ||
|
|
7f9e84dd02 | ||
|
|
93f0396bc5 | ||
|
|
75dd7d28c9 | ||
|
|
b3510c18b3 | ||
|
|
a30ee7f92c | ||
|
|
3901b32cc5 | ||
|
|
1728bba109 | ||
|
|
28bac11e9a | ||
|
|
3ae72091cb | ||
|
|
72ee5147f4 | ||
|
|
bb6e262b57 | ||
|
|
dfd19df01f | ||
|
|
43ed34ede5 | ||
|
|
ffdfce78fc | ||
|
|
5f664e5944 | ||
|
|
3e7b61205c | ||
|
|
cfca2d6cb5 | ||
|
|
4cfe1711b9 | ||
|
|
4b6a7fcf1c | ||
|
|
477013fcdd | ||
|
|
c38208e10b | ||
|
|
c37cf741bc | ||
|
|
2c408907c4 | ||
|
|
ad5e688547 | ||
|
|
11279e26a6 | ||
|
|
1e09e4961e | ||
|
|
2430588077 | ||
|
|
d5b18f922c | ||
|
|
e7bcf5cbe0 | ||
|
|
1d326304e1 | ||
|
|
1f0363941d | ||
|
|
5988586c08 | ||
|
|
e2ed9915ee | ||
|
|
bc33bc1428 | ||
|
|
f3409b8638 | ||
|
|
4d75dba0bc | ||
|
|
fbd34844cf | ||
|
|
0ae80e6d8b | ||
|
|
8557271da7 | ||
|
|
29ec1c4864 | ||
|
|
13bedf50a4 | ||
|
|
a73fddf988 | ||
|
|
3c6ec642eb | ||
|
|
9696e8796a | ||
|
|
2a4297c5fc | ||
|
|
ca93de06af | ||
|
|
f88820e9c3 | ||
|
|
2f90ac7e46 | ||
|
|
0d29eecaa2 | ||
|
|
67b69a62ec | ||
|
|
f532895dd4 | ||
|
|
ec5b5fa4c0 | ||
|
|
b9dc85e857 | ||
|
|
a1f58436d2 | ||
|
|
8378a273c3 | ||
|
|
f4d8c8a06e | ||
|
|
353f8b8211 | ||
|
|
ca14f0862b | ||
|
|
6bf0d4eeda | ||
|
|
3985aa4f34 | ||
|
|
ea5e577953 | ||
|
|
b23a38295b | ||
|
|
75b990473a | ||
|
|
f4f742a7a4 | ||
|
|
da5e971d94 | ||
|
|
c84e081b1c | ||
|
|
6b1e39f48f | ||
|
|
ceb826728a | ||
|
|
c8f5924c12 | ||
|
|
9d754b04cf | ||
|
|
59435e44a5 | ||
|
|
86848fec1a | ||
|
|
77078d6150 | ||
|
|
1da1e22553 | ||
|
|
6f90289d6b | ||
|
|
02b28fc87c | ||
|
|
7c334db8c4 | ||
|
|
61b0299bbb | ||
|
|
3a59a54107 | ||
|
|
6b483335e9 | ||
|
|
4248a0f08a | ||
|
|
1196c2c25e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
*~
|
||||
\#*\#
|
||||
*.out
|
||||
*.pyc
|
||||
*.sublime-*
|
||||
@@ -23,3 +24,4 @@ htmlcov
|
||||
*_ext.so
|
||||
/.tox
|
||||
/dist
|
||||
*.html
|
||||
|
||||
2
.pylintrc
Normal file
2
.pylintrc
Normal file
@@ -0,0 +1,2 @@
|
||||
[MESSAGES CONTROL]
|
||||
disable=invalid-name, missing-docstring, too-many-instance-attributes, too-few-public-methods, logging-format-interpolation
|
||||
13
.travis.yml
13
.travis.yml
@@ -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
|
||||
|
||||
94
README.md
94
README.md
@@ -1,94 +0,0 @@
|
||||
# Audio Modem Communication Library
|
||||
|
||||
[](https://travis-ci.org/romanz/amodem)
|
||||
[](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
239
README.rst
Normal 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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
250
amodem/__main__.py
Normal file
250
amodem/__main__.py
Normal 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
65
amodem/alsa.py
Normal 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
49
amodem/async.py
Normal 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
131
amodem/audio.py
Normal 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)
|
||||
172
amodem/calib.py
172
amodem/calib.py
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
120
amodem/config.py
120
amodem/config.py
@@ -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
116
amodem/detect.py
Normal 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
|
||||
114
amodem/dsp.py
114
amodem/dsp.py
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
30
amodem/levinson.py
Normal 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
68
amodem/main.py
Normal 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()
|
||||
311
amodem/recv.py
311
amodem/recv.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
40
amodem/tests/test_alsa.py
Normal 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'])]
|
||||
30
amodem/tests/test_async.py
Normal file
30
amodem/tests/test_async.py
Normal 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)
|
||||
34
amodem/tests/test_audio.py
Normal file
34
amodem/tests/test_audio.py
Normal 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
160
amodem/tests/test_calib.py
Normal 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']
|
||||
@@ -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
|
||||
6
amodem/tests/test_configs.py
Normal file
6
amodem/tests/test_configs.py
Normal 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
|
||||
61
amodem/tests/test_detect.py
Normal file
61
amodem/tests/test_detect.py
Normal 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
|
||||
@@ -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))
|
||||
67
amodem/tests/test_equalizer.py
Normal file
67
amodem/tests/test_equalizer.py
Normal 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)
|
||||
@@ -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'))
|
||||
@@ -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',)
|
||||
@@ -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
27
amodem/tests/utils.py
Normal 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)
|
||||
@@ -1,3 +0,0 @@
|
||||
prefix = [1]*400 + [0]*50
|
||||
equalizer_length = 500
|
||||
silence_length = 100
|
||||
1
amodem/version.py
Normal file
1
amodem/version.py
Normal file
@@ -0,0 +1 @@
|
||||
'1.13'
|
||||
@@ -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')
|
||||
@@ -1,3 +0,0 @@
|
||||
numpy
|
||||
bitarray
|
||||
reedsolo
|
||||
163
scripts/amodem
163
scripts/amodem
@@ -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
2
scripts/autocalib.sh
Executable 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
2
scripts/play.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
aplay -f S16_LE -c 1 -r 32000 $*
|
||||
@@ -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
15
scripts/profile.sh
Executable 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
31
scripts/record.py
Executable 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
2
scripts/record.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
arecord -f S16_LE -c 1 -r 32000 $*
|
||||
@@ -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
15
scripts/resample.py
Executable 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()
|
||||
66
setup.py
66
setup.py
@@ -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']},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
16
tox.ini
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user