mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee5c543737 | ||
|
|
1311c58005 | ||
|
|
9c6ef2884e | ||
|
|
21fe42e68d | ||
|
|
9352088389 | ||
|
|
db47cda390 | ||
|
|
4cd3def507 | ||
|
|
9192a8ff67 | ||
|
|
3b88a23dfb | ||
|
|
10c06f7646 | ||
|
|
df9cbfdf13 | ||
|
|
02f45de4f1 | ||
|
|
6d6bd44dd8 | ||
|
|
4598cfc7f6 | ||
|
|
ff6c9968e8 | ||
|
|
0bd691320e | ||
|
|
4527eaa931 | ||
|
|
1dd59f9f8f | ||
|
|
c3cefee85f | ||
|
|
2ab3c62d0d | ||
|
|
601a14297f | ||
|
|
71dab0e7bc | ||
|
|
53df2d5934 | ||
|
|
ac2e66bddd | ||
|
|
0534b6891a | ||
|
|
4f0bc6883b | ||
|
|
4846cdaf8f | ||
|
|
0e171a58f2 | ||
|
|
20f90edf8d | ||
|
|
72397e541c | ||
|
|
6d17cc132f | ||
|
|
9dbb3826c7 | ||
|
|
c991b2264e | ||
|
|
f30d28e39a | ||
|
|
fff10853a9 | ||
|
|
147404645c | ||
|
|
0e29f9a606 | ||
|
|
93a174142b | ||
|
|
3210638003 | ||
|
|
ceaf893675 | ||
|
|
6629cb6762 | ||
|
|
555186c2d8 | ||
|
|
66acac3e35 | ||
|
|
e1bdae2069 | ||
|
|
1ff777d226 | ||
|
|
40460e0291 | ||
|
|
43b68779a4 | ||
|
|
90aae24600 | ||
|
|
4c6315daf2 | ||
|
|
f7a151534f | ||
|
|
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 |
7
.bumpversion.cfg
Normal file
7
.bumpversion.cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 1.15.2
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
37
.github/workflows/build.yml
vendored
Normal file
37
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest mock pycodestyle coverage pylint six
|
||||
pip install -e .
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
pycodestyle amodem/ scripts/
|
||||
pylint --extension-pkg-whitelist=numpy --reports=no amodem --rcfile .pylintrc
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v
|
||||
coverage report
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
*~
|
||||
\#*\#
|
||||
*.out
|
||||
*.pyc
|
||||
*.sublime-*
|
||||
@@ -23,3 +24,6 @@ htmlcov
|
||||
*_ext.so
|
||||
/.tox
|
||||
/dist
|
||||
/deb_dist
|
||||
*.html
|
||||
.pytest_cache/
|
||||
|
||||
2
.pylintrc
Normal file
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, consider-using-with
|
||||
20
.travis.yml
20
.travis.yml
@@ -1,20 +0,0 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
install:
|
||||
- pip install .
|
||||
- pip install coveralls pep8 mock
|
||||
|
||||
script:
|
||||
- pep8 amodem/ scripts/ tests/ amodem-cli
|
||||
- cd tests
|
||||
- coverage run --source=amodem -m py.test
|
||||
|
||||
after_success:
|
||||
- coveralls
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
amodem -- Audio Modem Communication Library
|
||||
|
||||
Copyright (C) 2014, Roman Zeyde.
|
||||
Copyright (C) 2014, Roman Zeyde, Google Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
211
README.md
211
README.md
@@ -1,211 +0,0 @@
|
||||
# Audio Modem Communication Library
|
||||
|
||||
[](https://travis-ci.org/romanz/amodem)
|
||||
[](https://coveralls.io/r/romanz/amodem?branch=master)
|
||||
[](https://landscape.io/github/romanz/amodem/master)
|
||||
[](https://pypi.python.org/pypi/amodem/)
|
||||
[](https://pypi.python.org/pypi/amodem/)
|
||||
[](https://pypi.python.org/pypi/amodem/)
|
||||
|
||||
# Description
|
||||
|
||||
This program can be used to transmit a specified file between 2 computers, using
|
||||
a simple audio cable (for better SNR and higher speeds) or a simple headset,
|
||||
allowing true air-gapped communication (via a speaker and a microphone).
|
||||
|
||||
The sender modulates an input binary data file into an 32kHz audio,
|
||||
which is played to the sound card.
|
||||
|
||||
The receiver side records the transmitted audio,
|
||||
which is demodulated concurrently into an output binary data file.
|
||||
|
||||
The process requires a single manual calibration step: the transmitter has to
|
||||
find maximal output volume for its sound card, which will not saturate the
|
||||
receiving microphone.
|
||||
|
||||
The modem is using OFDM over an audio cable with the following parameters:
|
||||
|
||||
- Sampling rate: 32 kHz
|
||||
- Baud rate: 1 kHz
|
||||
- Symbol modulation: BPSK, 4-PSK, 16-QAM ,64-QAM
|
||||
- Carriers: 2-11 kHz
|
||||
|
||||
This way, modem may achieve 60kbps bitrate = 7.5 kB/s.
|
||||
|
||||
A simple CRC-32 checksum is used for data integrity verification
|
||||
on each 250 byte data frame.
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
Make sure that `numpy` and `PortAudio v19` packages are installed (on Debian):
|
||||
|
||||
$ sudo apt-get install python-numpy portaudio19-dev
|
||||
|
||||
Get the latest released version from [PyPI](https://pypi.python.org/pypi/amodem/):
|
||||
|
||||
$ pip install --user amodem
|
||||
|
||||
Or, try the latest (unstable) development version from GitHub:
|
||||
|
||||
$ git clone https://github.com/romanz/amodem.git
|
||||
$ cd amodem
|
||||
$ pip install --user -e .
|
||||
|
||||
For graphs and visualization (optional), install `matplotlib` Python package.
|
||||
|
||||
For validation, run:
|
||||
|
||||
$ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
$ amodem-cli -h
|
||||
usage: amodem-cli [-h] {send,recv} ...
|
||||
|
||||
Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
|
||||
|
||||
positional arguments:
|
||||
{send,recv}
|
||||
send modulate binary data into audio signal.
|
||||
recv demodulate audio signal into binary data.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
|
||||
# Calibration
|
||||
|
||||
Connect the audio cable between the sender and the receiver, and run the
|
||||
following scripts:
|
||||
|
||||
- On the sender's side:
|
||||
```
|
||||
~/sender $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
~/sender $ amodem-cli send --calibrate
|
||||
```
|
||||
|
||||
- On the receiver's side:
|
||||
```
|
||||
~/receiver $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
~/receiver $ amodem-cli recv --calibrate
|
||||
```
|
||||
|
||||
If BITRATE is not set, the MODEM will use 1 kbps settings (single frequency with BPSK modulation).
|
||||
|
||||
Change the sender computer's output audio level, until
|
||||
all frequencies are received well:
|
||||
```
|
||||
3000 Hz: good signal
|
||||
4000 Hz: good signal
|
||||
5000 Hz: good signal
|
||||
6000 Hz: good signal
|
||||
7000 Hz: good signal
|
||||
8000 Hz: good signal
|
||||
9000 Hz: good signal
|
||||
10000 Hz: good signal
|
||||
```
|
||||
|
||||
If the signal is "too weak", increase the sender's output audio level.
|
||||
|
||||
If the signal is "too strong", decrease the sender's output audio level.
|
||||
|
||||
If the signal is "too noisy", the SNR is probably too low: decrease the
|
||||
background noise or increase the signal (without causing saturation).
|
||||
|
||||
You can see a video of the calibration process [here](http://www.youtube.com/watch?v=jRUj2Ifk-Po).
|
||||
|
||||
# Usage
|
||||
|
||||
- Prepare the sender (generate a random binary data file to be sent):
|
||||
|
||||
```
|
||||
~/sender $ dd if=/dev/urandom of=data.tx bs=10KB count=1 status=none
|
||||
~/sender $ sha256sum data.tx
|
||||
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.tx
|
||||
```
|
||||
|
||||
- Start the receiver (will wait for the sender to start):
|
||||
```
|
||||
~/receiver $ amodem-cli recv -vv -i data.rx
|
||||
```
|
||||
|
||||
- Start the sender (will modulate the data and start the transmission):
|
||||
```
|
||||
~/sender $ amodem-cli send -vv -o data.tx
|
||||
```
|
||||
|
||||
- A similar log should be emitted by the sender:
|
||||
```
|
||||
2015-01-16 11:49:25,181 DEBUG Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz amodem-cli:174
|
||||
2015-01-16 11:49:27,028 INFO Sending 2.150 seconds of training audio send.py:63
|
||||
2015-01-16 11:49:27,029 INFO Starting modulation send.py:68
|
||||
2015-01-16 11:49:28,016 DEBUG Sent 6.000 kB send.py:50
|
||||
2015-01-16 11:49:28,776 INFO Sent 10.000 kB @ 1.701 seconds send.py:73
|
||||
|
||||
```
|
||||
|
||||
- A similar log should be emitted by the receiver:
|
||||
```
|
||||
2015-01-16 11:49:24,369 DEBUG Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz amodem-cli:174
|
||||
2015-01-16 11:49:24,382 DEBUG Skipping 0.100 seconds recv.py:214
|
||||
2015-01-16 11:49:24,535 INFO Waiting for carrier tone: 3.0 kHz recv.py:221
|
||||
2015-01-16 11:49:26,741 INFO Carrier detected at ~1761.0 ms @ 3.0 kHz: coherence=99.944%, amplitude=0.499 detect.py:64
|
||||
2015-01-16 11:49:26,741 DEBUG Buffered 1000 ms of audio detect.py:66
|
||||
2015-01-16 11:49:26,912 DEBUG Carrier starts at 1761.000 ms detect.py:76
|
||||
2015-01-16 11:49:26,917 DEBUG Carrier symbols amplitude : 0.499 detect.py:101
|
||||
2015-01-16 11:49:26,917 DEBUG Current phase on carrier: -0.466 detect.py:112
|
||||
2015-01-16 11:49:26,917 DEBUG Frequency error: -0.01 ppm detect.py:113
|
||||
2015-01-16 11:49:26,917 DEBUG Frequency correction: 0.005 ppm recv.py:225
|
||||
2015-01-16 11:49:26,917 DEBUG Gain correction: 2.004 recv.py:228
|
||||
2015-01-16 11:49:27,099 DEBUG Prefix OK recv.py:48
|
||||
2015-01-16 11:49:27,925 DEBUG 3.0 kHz: SNR = 40.58 dB recv.py:92
|
||||
2015-01-16 11:49:27,925 DEBUG 4.0 kHz: SNR = 41.98 dB recv.py:92
|
||||
2015-01-16 11:49:27,925 DEBUG 5.0 kHz: SNR = 42.81 dB recv.py:92
|
||||
2015-01-16 11:49:27,925 DEBUG 6.0 kHz: SNR = 43.71 dB recv.py:92
|
||||
2015-01-16 11:49:27,926 DEBUG 7.0 kHz: SNR = 43.43 dB recv.py:92
|
||||
2015-01-16 11:49:27,926 DEBUG 8.0 kHz: SNR = 42.96 dB recv.py:92
|
||||
2015-01-16 11:49:27,926 DEBUG 9.0 kHz: SNR = 42.66 dB recv.py:92
|
||||
2015-01-16 11:49:27,926 DEBUG 10.0 kHz: SNR = 42.22 dB recv.py:92
|
||||
2015-01-16 11:49:27,928 INFO Starting demodulation recv.py:119
|
||||
2015-01-16 11:49:28,008 DEBUG Got 0.600 kB, realtime: 80.73%, drift: -0.00 ppm recv.py:140
|
||||
2015-01-16 11:49:28,081 DEBUG Got 1.200 kB, realtime: 76.60%, drift: -0.00 ppm recv.py:140
|
||||
2015-01-16 11:49:28,153 DEBUG Got 1.800 kB, realtime: 75.17%, drift: -0.00 ppm recv.py:140
|
||||
2015-01-16 11:49:28,224 DEBUG Got 2.400 kB, realtime: 74.02%, drift: -0.00 ppm recv.py:140
|
||||
2015-01-16 11:49:28,306 DEBUG Got 3.000 kB, realtime: 75.72%, drift: -0.00 ppm recv.py:140
|
||||
2015-01-16 11:49:28,382 DEBUG Got 3.600 kB, realtime: 75.71%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:28,458 DEBUG Got 4.200 kB, realtime: 75.72%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:28,528 DEBUG Got 4.800 kB, realtime: 75.10%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:28,609 DEBUG Got 5.400 kB, realtime: 75.76%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:28,686 DEBUG Got 6.000 kB, realtime: 75.80%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:28,757 DEBUG Got 6.600 kB, realtime: 75.36%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:28,828 DEBUG Got 7.200 kB, realtime: 75.03%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:28,909 DEBUG Got 7.800 kB, realtime: 75.50%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:28,980 DEBUG Got 8.400 kB, realtime: 75.15%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:29,051 DEBUG Got 9.000 kB, realtime: 74.88%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:29,261 DEBUG Got 9.600 kB, realtime: 83.31%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:29,342 DEBUG Got 10.200 kB, realtime: 83.23%, drift: -0.01 ppm recv.py:140
|
||||
2015-01-16 11:49:29,343 DEBUG EOF frame detected framing.py:57
|
||||
2015-01-16 11:49:29,343 DEBUG Demodulated 10.205 kB @ 1.415 seconds (83.2% realtime) recv.py:165
|
||||
2015-01-16 11:49:29,343 INFO Received 10.000 kB @ 1.415 seconds = 7.066 kB/s recv.py:169
|
||||
```
|
||||
|
||||
- After the receiver has finished, verify the received file's hash:
|
||||
```
|
||||
~/receiver $ sha256sum data.rx
|
||||
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.rx
|
||||
```
|
||||
|
||||
You can see a video of the data transfer process [here](http://www.youtube.com/watch?v=GZQUtHB8so4).
|
||||
|
||||
# Visualization
|
||||
Make sure that `matplotlib` package is installed, and run (at the receiver side):
|
||||
|
||||
```
|
||||
~/receiver $ amodem-cli recv --plot -o data.rx
|
||||
```
|
||||
|
||||
|
||||
# Donations
|
||||
|
||||
Want to donate? Feel free.
|
||||
Send to [1C1snTrkHAHM5XnnfuAtiTBaA11HBxjJyv](https://blockchain.info/address/1C1snTrkHAHM5XnnfuAtiTBaA11HBxjJyv).
|
||||
|
||||
Thanks :)
|
||||
248
README.rst
Normal file
248
README.rst
Normal file
@@ -0,0 +1,248 @@
|
||||
Audio Modem Communication Library
|
||||
=================================
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Python Versions
|
||||
.. image:: https://img.shields.io/pypi/l/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: License
|
||||
.. image:: https://img.shields.io/pypi/v/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Package Version
|
||||
.. image:: https://img.shields.io/pypi/status/amodem.svg
|
||||
:target: https://pypi.python.org/pypi/amodem/
|
||||
:alt: Development Status
|
||||
|
||||
|
||||
Description
|
||||
-----------
|
||||
|
||||
This program can transmit a file between 2 computers, using a simple headset,
|
||||
allowing true air-gapped communication (via a speaker and a microphone),
|
||||
or an audio cable (for higher transmission speed).
|
||||
|
||||
The sender modulates the input data into an audio signal,
|
||||
which is played to the sound card.
|
||||
|
||||
The receiver records the audio, and demodulates it back to the original data.
|
||||
|
||||
The process requires a single manual calibration step: the transmitter has to
|
||||
find the optimal output volume for its sound card, which will not saturate the
|
||||
receiving microphone and provide good enough Signal-to-Noise ratio
|
||||
for the demodulation to succeed.
|
||||
|
||||
HackerNews discussion: https://news.ycombinator.com/item?id=17333257
|
||||
|
||||
Technical Details
|
||||
-----------------
|
||||
|
||||
The modem is using OFDM over an audio cable with the following parameters:
|
||||
|
||||
- Sampling rate: 8/16/32 kHz
|
||||
- Baud rate: 1 kHz
|
||||
- Symbol modulation: BPSK, 4-PSK, 16-QAM, 64-QAM, 256-QAM
|
||||
- Carriers: 2-11 kHz (up to ten carriers)
|
||||
|
||||
This way, modem may achieve 80kbps bitrate = 10 kB/s (for best SNR).
|
||||
|
||||
A simple CRC-32 checksum is used for data integrity verification
|
||||
on each 250 byte data frame.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Make sure that all the required packages are installed (on Debian)::
|
||||
|
||||
$ sudo apt-get install python-numpy python-pip portaudio19-dev git
|
||||
|
||||
Get the latest released version from PyPI::
|
||||
|
||||
$ pip install --user amodem
|
||||
|
||||
Or, try the latest (unstable) development version from GitHub::
|
||||
|
||||
$ git clone https://github.com/romanz/amodem.git
|
||||
$ cd amodem
|
||||
$ pip install --user -e .
|
||||
|
||||
For graphs and visualization (optional), install `matplotlib` Python package.
|
||||
|
||||
For validation, run::
|
||||
|
||||
$ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
$ amodem -h
|
||||
usage: amodem [-h] {send,recv} ...
|
||||
|
||||
Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
|
||||
|
||||
positional arguments:
|
||||
{send,recv}
|
||||
send modulate binary data into audio signal.
|
||||
recv demodulate audio signal into binary data.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
On, Windows you may download the `portaudio` library from `MinGW <https://packages.msys2.org/base/mingw-w64-portaudio>`_.
|
||||
Then, you should specify the DLL using the following command-line flag::
|
||||
|
||||
-l AUDIO_LIBRARY, --audio-library AUDIO_LIBRARY
|
||||
File name of PortAudio shared library.
|
||||
|
||||
|
||||
Calibration
|
||||
-----------
|
||||
|
||||
Connect the audio cable between the sender and the receiver, and run the
|
||||
following scripts:
|
||||
|
||||
On the sender's side::
|
||||
|
||||
~/sender $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
~/sender $ amodem send --calibrate
|
||||
|
||||
On the receiver's side::
|
||||
|
||||
~/receiver $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
~/receiver $ amodem recv --calibrate
|
||||
|
||||
If BITRATE is not set, the MODEM will use 1 kbps settings (single frequency with BPSK modulation).
|
||||
|
||||
Change the sender computer's output audio level, until
|
||||
all frequencies are received well::
|
||||
|
||||
3000 Hz: good signal
|
||||
4000 Hz: good signal
|
||||
5000 Hz: good signal
|
||||
6000 Hz: good signal
|
||||
7000 Hz: good signal
|
||||
8000 Hz: good signal
|
||||
9000 Hz: good signal
|
||||
10000 Hz: good signal
|
||||
|
||||
|
||||
If the signal is "too weak", increase the sender's output audio level.
|
||||
|
||||
If the signal is "too strong", decrease the sender's output audio level.
|
||||
|
||||
If the signal is "too noisy", it may be that the noise level is too high
|
||||
or that the analog signal is being distorted.
|
||||
Please run the following command during the calibration session,
|
||||
and send me the resulting ``audio.raw`` file for debugging::
|
||||
|
||||
~/receiver $ arecord --format=S16_LE --channels=1 --rate=32000 audio.raw
|
||||
|
||||
You can see a screencast of the `calibration process <https://asciinema.org/a/25065?autoplay=1>`_.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Prepare the sender (generate a random binary data file to be sent)::
|
||||
|
||||
~/sender $ dd if=/dev/urandom of=data.tx bs=60KB count=1 status=none
|
||||
~/sender $ sha256sum data.tx
|
||||
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.tx
|
||||
|
||||
Start the receiver (will wait for the sender to start)::
|
||||
|
||||
~/receiver $ amodem recv -vv -o data.rx
|
||||
|
||||
Start the sender (will modulate the data and start the transmission)::
|
||||
|
||||
~/sender $ amodem send -vv -i data.tx
|
||||
|
||||
A similar log should be emitted by the sender::
|
||||
|
||||
2015-02-06 18:12:46,222 DEBUG Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
|
||||
2015-02-06 18:12:46,222 INFO PortAudio V19-devel (built Feb 25 2014 21:09:53) loaded
|
||||
2015-02-06 18:12:48,297 INFO Sending 2.150 seconds of training audio
|
||||
2015-02-06 18:12:48,297 INFO Starting modulation
|
||||
2015-02-06 18:12:49,303 DEBUG Sent 6.000 kB
|
||||
2015-02-06 18:12:50,296 DEBUG Sent 12.000 kB
|
||||
2015-02-06 18:12:51,312 DEBUG Sent 18.000 kB
|
||||
2015-02-06 18:12:52,290 DEBUG Sent 24.000 kB
|
||||
2015-02-06 18:12:53,299 DEBUG Sent 30.000 kB
|
||||
2015-02-06 18:12:54,299 DEBUG Sent 36.000 kB
|
||||
2015-02-06 18:12:55,306 DEBUG Sent 42.000 kB
|
||||
2015-02-06 18:12:56,296 DEBUG Sent 48.000 kB
|
||||
2015-02-06 18:12:57,311 DEBUG Sent 54.000 kB
|
||||
2015-02-06 18:12:58,293 DEBUG Sent 60.000 kB
|
||||
2015-02-06 18:12:58,514 INFO Sent 60.000 kB @ 10.201 seconds
|
||||
2015-02-06 18:12:59,506 DEBUG Closing input and output
|
||||
|
||||
A similar log should be emitted by the receiver::
|
||||
|
||||
2015-02-06 18:12:44,848 DEBUG Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
|
||||
2015-02-06 18:12:44,849 INFO PortAudio V19-devel (built Feb 25 2014 21:09:53) loaded
|
||||
2015-02-06 18:12:44,929 DEBUG AsyncReader thread started
|
||||
2015-02-06 18:12:44,930 DEBUG Skipping 0.100 seconds
|
||||
2015-02-06 18:12:45,141 INFO Waiting for carrier tone: 3.0 kHz
|
||||
2015-02-06 18:12:47,846 INFO Carrier detected at ~2265.0 ms @ 3.0 kHz
|
||||
2015-02-06 18:12:47,846 DEBUG Buffered 1000 ms of audio
|
||||
2015-02-06 18:12:48,025 DEBUG Carrier starts at 2264.000 ms
|
||||
2015-02-06 18:12:48,029 DEBUG Carrier symbols amplitude : 0.573
|
||||
2015-02-06 18:12:48,030 DEBUG Current phase on carrier: 0.061
|
||||
2015-02-06 18:12:48,030 DEBUG Frequency error: -0.009 ppm
|
||||
2015-02-06 18:12:48,030 DEBUG Frequency correction: 0.009 ppm
|
||||
2015-02-06 18:12:48,030 DEBUG Gain correction: 1.746
|
||||
2015-02-06 18:12:48,198 DEBUG Prefix OK
|
||||
2015-02-06 18:12:48,866 DEBUG 3.0 kHz: SNR = 34.82 dB
|
||||
2015-02-06 18:12:48,866 DEBUG 4.0 kHz: SNR = 36.39 dB
|
||||
2015-02-06 18:12:48,867 DEBUG 5.0 kHz: SNR = 37.88 dB
|
||||
2015-02-06 18:12:48,867 DEBUG 6.0 kHz: SNR = 38.58 dB
|
||||
2015-02-06 18:12:48,867 DEBUG 7.0 kHz: SNR = 38.86 dB
|
||||
2015-02-06 18:12:48,867 DEBUG 8.0 kHz: SNR = 38.63 dB
|
||||
2015-02-06 18:12:48,867 DEBUG 9.0 kHz: SNR = 38.07 dB
|
||||
2015-02-06 18:12:48,868 DEBUG 10.0 kHz: SNR = 37.22 dB
|
||||
2015-02-06 18:12:48,869 INFO Starting demodulation
|
||||
2015-02-06 18:12:49,689 DEBUG Got 6.000 kB, SNR: 41.19 dB, drift: -0.01 ppm
|
||||
2015-02-06 18:12:50,659 DEBUG Got 12.000 kB, SNR: 41.05 dB, drift: -0.00 ppm
|
||||
2015-02-06 18:12:51,639 DEBUG Got 18.000 kB, SNR: 40.96 dB, drift: -0.00 ppm
|
||||
2015-02-06 18:12:52,610 DEBUG Got 24.000 kB, SNR: 41.47 dB, drift: -0.01 ppm
|
||||
2015-02-06 18:12:53,610 DEBUG Got 30.000 kB, SNR: 41.06 dB, drift: -0.00 ppm
|
||||
2015-02-06 18:12:54,589 DEBUG Got 36.000 kB, SNR: 41.37 dB, drift: -0.00 ppm
|
||||
2015-02-06 18:12:55,679 DEBUG Got 42.000 kB, SNR: 41.13 dB, drift: -0.00 ppm
|
||||
2015-02-06 18:12:56,650 DEBUG Got 48.000 kB, SNR: 41.31 dB, drift: -0.00 ppm
|
||||
2015-02-06 18:12:57,631 DEBUG Got 54.000 kB, SNR: 41.23 dB, drift: +0.00 ppm
|
||||
2015-02-06 18:12:58,605 DEBUG Got 60.000 kB, SNR: 41.31 dB, drift: +0.00 ppm
|
||||
2015-02-06 18:12:58,857 DEBUG EOF frame detected
|
||||
2015-02-06 18:12:58,857 DEBUG Demodulated 61.205 kB @ 9.988 seconds (97.9% realtime)
|
||||
2015-02-06 18:12:58,858 INFO Received 60.000 kB @ 9.988 seconds = 6.007 kB/s
|
||||
2015-02-06 18:12:58,876 DEBUG Closing input and output
|
||||
2015-02-06 18:12:58,951 DEBUG AsyncReader thread stopped (read 896000 bytes)
|
||||
|
||||
After the receiver has finished, verify the received file's hash::
|
||||
|
||||
~/receiver $ sha256sum data.rx
|
||||
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.rx
|
||||
|
||||
You can see a screencast of the `data transfer process <https://asciinema.org/a/25066?autoplay=1>`_.
|
||||
|
||||
I/O redirection
|
||||
---------------
|
||||
The audio can be written/read to an intermediate PCM file (instead of the speaker/microphone) using::
|
||||
|
||||
$ echo 123 | amodem send -o /tmp/file.pcm
|
||||
Sending 0.800 seconds of training audio
|
||||
Starting modulation
|
||||
Sent 0.004 kB @ 0.113 seconds
|
||||
|
||||
$ amodem recv -i /tmp/file.pcm
|
||||
Waiting for carrier tone: 2.0 kHz
|
||||
Carrier detected at ~150.0 ms @ 2.0 kHz
|
||||
Carrier coherence: 100.000%
|
||||
Carrier symbols amplitude : 1.000
|
||||
Frequency error: 0.000 ppm
|
||||
Starting demodulation
|
||||
123
|
||||
Received 0.004 kB @ 0.011 seconds = 0.376 kB/s
|
||||
|
||||
|
||||
Visualization
|
||||
-------------
|
||||
Make sure that ``matplotlib`` package is installed, and run (at the receiver side)::
|
||||
|
||||
~/receiver $ amodem recv --plot -o data.rx
|
||||
|
||||
131
amodem-cli → amodem/__main__.py
Executable file → Normal file
131
amodem-cli → amodem/__main__.py
Executable file → Normal file
@@ -1,11 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import zlib
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from . import async_reader
|
||||
from . import audio
|
||||
from . import calib
|
||||
from . import main
|
||||
from .config import bitrates
|
||||
|
||||
|
||||
# Python 3 has `buffer` attribute for byte-based I/O
|
||||
_stdin = getattr(sys.stdin, 'buffer', sys.stdin)
|
||||
@@ -19,16 +27,14 @@ except ImportError:
|
||||
|
||||
log = logging.getLogger('__name__')
|
||||
|
||||
from amodem import main, calib, audio, async
|
||||
from amodem.config import bitrates
|
||||
|
||||
bitrate = os.environ.get('BITRATE', 1)
|
||||
config = bitrates.get(int(bitrate))
|
||||
|
||||
|
||||
class Compressor(object):
|
||||
class Compressor:
|
||||
def __init__(self, stream):
|
||||
self.obj = zlib.compressobj()
|
||||
log.info('Using zlib compressor')
|
||||
self.stream = stream
|
||||
|
||||
def read(self, size):
|
||||
@@ -46,9 +52,10 @@ class Compressor(object):
|
||||
return result
|
||||
|
||||
|
||||
class Decompressor(object):
|
||||
class Decompressor:
|
||||
def __init__(self, stream):
|
||||
self.obj = zlib.decompressobj()
|
||||
log.info('Using zlib decompressor')
|
||||
self.stream = stream
|
||||
|
||||
def write(self, data):
|
||||
@@ -58,8 +65,10 @@ class Decompressor(object):
|
||||
self.stream.write(self.obj.flush())
|
||||
|
||||
|
||||
def FileType(mode, audio_interface=None):
|
||||
def FileType(mode, interface_factory=None):
|
||||
def opener(fname):
|
||||
audio_interface = interface_factory() if interface_factory else None
|
||||
|
||||
assert 'r' in mode or 'w' in mode
|
||||
if audio_interface is None and fname is None:
|
||||
fname = '-'
|
||||
@@ -68,7 +77,7 @@ def FileType(mode, audio_interface=None):
|
||||
assert audio_interface is not None
|
||||
if 'r' in mode:
|
||||
s = audio_interface.recorder()
|
||||
return async.AsyncReader(stream=s, bufsize=s.bufsize)
|
||||
return async_reader.AsyncReader(stream=s, bufsize=s.bufsize)
|
||||
if 'w' in mode:
|
||||
return audio_interface.player()
|
||||
|
||||
@@ -93,21 +102,17 @@ def get_volume_cmd(args):
|
||||
for c in volume_controllers:
|
||||
if os.system(c['test']) == 0:
|
||||
return c[args.command]
|
||||
return None
|
||||
|
||||
|
||||
def _main():
|
||||
fmt = ('Audio OFDM MODEM: {0:.1f} kb/s ({1:d}-QAM x {2:d} carriers) '
|
||||
'Fs={3:.1f} kHz')
|
||||
description = fmt.format(config.modem_bps / 1e3, len(config.symbols),
|
||||
config.Nfreq, config.Fs / 1e3)
|
||||
interface = audio.Interface(config=config)
|
||||
def wrap(cls, stream, enable):
|
||||
return cls(stream) if enable else stream
|
||||
|
||||
|
||||
def create_parser(description, interface_factory):
|
||||
p = argparse.ArgumentParser(description=description)
|
||||
subparsers = p.add_subparsers()
|
||||
|
||||
def wrap(cls, stream, enable):
|
||||
return cls(stream) if enable else stream
|
||||
|
||||
# Modulator
|
||||
sender = subparsers.add_parser(
|
||||
'send', help='modulate binary data into audio signal.')
|
||||
@@ -116,16 +121,23 @@ def _main():
|
||||
sender.add_argument(
|
||||
'-o', '--output', help='output file (use "-" for stdout).'
|
||||
' if not specified, `aplay` tool will be used.')
|
||||
sender.add_argument(
|
||||
'-g', '--gain', type=float, default=1.0,
|
||||
help='Modulator gain (defaults to 1)')
|
||||
sender.add_argument(
|
||||
'--silence', type=float, default=0.0,
|
||||
help='Extra silence before sending the data (in seconds)')
|
||||
sender.set_defaults(
|
||||
main=lambda config, args: main.send(
|
||||
config, src=wrap(Compressor, args.src, args.zip), dst=args.dst
|
||||
config, src=wrap(Compressor, args.src, args.zlib), dst=args.dst,
|
||||
gain=args.gain, extra_silence=args.silence
|
||||
),
|
||||
calib=lambda config, args: calib.send(
|
||||
config=config, dst=args.dst,
|
||||
volume_cmd=get_volume_cmd(args)
|
||||
),
|
||||
input_type=FileType('rb'),
|
||||
output_type=FileType('wb', interface),
|
||||
output_type=FileType('wb', interface_factory),
|
||||
command='send'
|
||||
)
|
||||
|
||||
@@ -133,8 +145,7 @@ def _main():
|
||||
receiver = subparsers.add_parser(
|
||||
'recv', help='demodulate audio signal into binary data.')
|
||||
receiver.add_argument(
|
||||
'-i', '--input', help='input file (use "-" for stdin).'
|
||||
' if not specified, `arecord` tool will be used.')
|
||||
'-i', '--input', help='input file (use "-" for stdin).')
|
||||
receiver.add_argument(
|
||||
'-o', '--output', help='output file (use "-" for stdout).')
|
||||
receiver.add_argument(
|
||||
@@ -145,14 +156,14 @@ def _main():
|
||||
help='plot results using pylab module')
|
||||
receiver.set_defaults(
|
||||
main=lambda config, args: main.recv(
|
||||
config, src=args.src, dst=wrap(Decompressor, args.dst, args.zip),
|
||||
config, src=args.src, dst=wrap(Decompressor, args.dst, args.zlib),
|
||||
pylab=args.pylab, dump_audio=args.dump
|
||||
),
|
||||
calib=lambda config, args: calib.recv(
|
||||
config=config, src=args.src, verbose=args.verbose,
|
||||
volume_cmd=get_volume_cmd(args)
|
||||
),
|
||||
input_type=FileType('rb', interface),
|
||||
input_type=FileType('rb', interface_factory),
|
||||
output_type=FileType('wb'),
|
||||
command='recv'
|
||||
)
|
||||
@@ -165,8 +176,8 @@ def _main():
|
||||
metavar='SYSTEM', help=calibration_help)
|
||||
sub.add_argument('-l', '--audio-library', default='libportaudio.so',
|
||||
help='File name of PortAudio shared library.')
|
||||
sub.add_argument('-z', '--zip', default=False, action='store_true',
|
||||
help='Use ZIP to compress data.')
|
||||
sub.add_argument('-z', '--zlib', default=False, action='store_true',
|
||||
help='Use zlib to compress/decompress data.')
|
||||
g = sub.add_mutually_exclusive_group()
|
||||
g.add_argument('-v', '--verbose', default=0, action='count')
|
||||
g.add_argument('-q', '--quiet', default=False, action='store_true')
|
||||
@@ -174,7 +185,22 @@ def _main():
|
||||
if argcomplete:
|
||||
argcomplete.autocomplete(p)
|
||||
|
||||
args = p.parse_args()
|
||||
return p
|
||||
|
||||
|
||||
class _Dummy:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
def _version():
|
||||
return pkg_resources.require('amodem')[0].version
|
||||
|
||||
|
||||
def _config_log(args):
|
||||
if args.verbose == 0:
|
||||
level, fmt = 'INFO', '%(message)s'
|
||||
elif args.verbose == 1:
|
||||
@@ -187,31 +213,58 @@ def _main():
|
||||
level, fmt = 'WARNING', '%(message)s'
|
||||
logging.basicConfig(level=level, format=fmt)
|
||||
|
||||
|
||||
def _main():
|
||||
fmt = ('Audio OFDM MODEM v{0:s}: '
|
||||
'{1:.1f} kb/s ({2:d}-QAM x {3:d} carriers) '
|
||||
'Fs={4:.1f} kHz')
|
||||
description = fmt.format(_version(),
|
||||
config.modem_bps / 1e3, len(config.symbols),
|
||||
config.Nfreq, config.Fs / 1e3)
|
||||
interface = None
|
||||
|
||||
def interface_factory():
|
||||
return interface
|
||||
|
||||
p = create_parser(description, interface_factory)
|
||||
|
||||
args = p.parse_args()
|
||||
_config_log(args)
|
||||
|
||||
# Parsing and execution
|
||||
log.debug(description)
|
||||
log.info(description)
|
||||
|
||||
args.pylab = None
|
||||
if getattr(args, 'plot', False):
|
||||
import pylab
|
||||
import pylab # pylint: disable=import-error,import-outside-toplevel
|
||||
args.pylab = pylab
|
||||
|
||||
with interface.load(args.audio_library):
|
||||
if args.audio_library == 'ALSA':
|
||||
from . import alsa # pylint: disable=import-outside-toplevel
|
||||
interface = alsa.Interface(config)
|
||||
elif args.audio_library == '-':
|
||||
interface = _Dummy() # manually disable PortAudio
|
||||
elif args.command == 'send' and args.output is not None:
|
||||
interface = _Dummy() # redirected output
|
||||
elif args.command == 'recv' and args.input is not None:
|
||||
interface = _Dummy() # redirected input
|
||||
else:
|
||||
interface = audio.Interface(config)
|
||||
interface.load(args.audio_library)
|
||||
|
||||
with interface:
|
||||
args.src = args.input_type(args.input)
|
||||
args.dst = args.output_type(args.output)
|
||||
try:
|
||||
if args.calibrate is False:
|
||||
return args.main(config=config, args=args)
|
||||
args.main(config=config, args=args)
|
||||
else:
|
||||
try:
|
||||
args.calib(config=config, args=args)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
args.calib(config=config, args=args)
|
||||
finally:
|
||||
log.debug('Closing input and output')
|
||||
args.src.close()
|
||||
args.dst.close()
|
||||
log.debug('Finished I/O')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = _main()
|
||||
sys.exit(0 if success else 1)
|
||||
_main()
|
||||
70
amodem/alsa.py
Normal file
70
amodem/alsa.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Code which adds Linux ALSA support for interfaces,
|
||||
recording and playing.
|
||||
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Interface:
|
||||
|
||||
RECORDER = 'arecord'
|
||||
PLAYER = 'aplay'
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
rate = int(config.Fs)
|
||||
bits_per_sample = config.bits_per_sample
|
||||
assert bits_per_sample == 16
|
||||
|
||||
args = '-f S{0:d}_LE -c 1 -r {1:d} -T 100 -q -'
|
||||
args = args.format(bits_per_sample, rate).split()
|
||||
|
||||
self.record_cmd = [self.RECORDER] + args
|
||||
self.play_cmd = [self.PLAYER] + args
|
||||
self.processes = []
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
for p in self.processes:
|
||||
try:
|
||||
p.wait()
|
||||
except OSError:
|
||||
log.warning('%s failed', p)
|
||||
|
||||
def launch(self, **kwargs):
|
||||
log.debug('Launching subprocess: %s', kwargs)
|
||||
p = subprocess.Popen(**kwargs)
|
||||
self.processes.append(p)
|
||||
return p
|
||||
|
||||
def recorder(self):
|
||||
return Recorder(self)
|
||||
|
||||
def player(self):
|
||||
return Player(self)
|
||||
|
||||
|
||||
class Recorder:
|
||||
def __init__(self, lib):
|
||||
self.p = lib.launch(args=lib.record_cmd, stdout=subprocess.PIPE)
|
||||
self.read = self.p.stdout.read
|
||||
self.bufsize = 4096
|
||||
|
||||
def close(self):
|
||||
self.p.kill()
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, lib):
|
||||
self.p = lib.launch(args=lib.play_cmd, stdin=subprocess.PIPE)
|
||||
self.write = self.p.stdin.write
|
||||
|
||||
def close(self):
|
||||
self.p.stdin.close()
|
||||
self.p.wait()
|
||||
@@ -1,11 +1,14 @@
|
||||
import threading
|
||||
import six # since `Queue` module was renamed to `queue` (in Python 3)
|
||||
"""Asynchronous Reading capabilities for amodem."""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import six # since `Queue` module was renamed to `queue` (in Python 3)
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
class AsyncReader(object):
|
||||
class AsyncReader:
|
||||
def __init__(self, stream, bufsize):
|
||||
self.stream = stream
|
||||
self.queue = six.moves.queue.Queue()
|
||||
@@ -26,7 +29,7 @@ class AsyncReader(object):
|
||||
queue.put(buf)
|
||||
total += len(buf)
|
||||
log.debug('AsyncReader thread stopped (read %d bytes)', total)
|
||||
except BaseException:
|
||||
except BaseException: # pylint: disable=broad-except
|
||||
log.exception('AsyncReader thread failed')
|
||||
queue.put(None)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Audio capabilities for amodem."""
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import time
|
||||
@@ -5,7 +7,7 @@ import time
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Interface(object):
|
||||
class Interface:
|
||||
def __init__(self, config, debug=False):
|
||||
self.debug = bool(debug)
|
||||
self.config = config
|
||||
@@ -51,7 +53,7 @@ class Interface(object):
|
||||
return Stream(self, config=self.config, write=True)
|
||||
|
||||
|
||||
class Stream(object):
|
||||
class Stream:
|
||||
|
||||
timer = time.time
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import numpy as np
|
||||
"""Calibration capabilities for amodem."""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
import numpy as np
|
||||
|
||||
from . import common
|
||||
from . import dsp
|
||||
from . import sampling
|
||||
from . import stream
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def volume_controller(cmd):
|
||||
def controller(level):
|
||||
@@ -36,7 +39,7 @@ def send(config, dst, volume_cmd=None, gain=1.0, limit=None):
|
||||
|
||||
def frame_iter(config, src, frame_length):
|
||||
frame_size = frame_length * config.Nsym * config.sample_size
|
||||
omegas = 2 * np.pi * config.frequencies / config.Fs
|
||||
omegas = 2 * np.pi * np.array(config.frequencies) / config.Fs
|
||||
|
||||
while True:
|
||||
data = src.read(frame_size)
|
||||
@@ -72,10 +75,10 @@ def detector(config, src, frame_length=200):
|
||||
else:
|
||||
msg = 'too {0} signal'.format(errors[flags.index(False)])
|
||||
|
||||
yield common.AttributeHolder(dict(
|
||||
yield dict(
|
||||
freq=freq, rms=rms, peak=peak, coherency=coherency,
|
||||
total=total, success=success, msg=msg
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
def volume_calibration(result_iterator, volume_ctl):
|
||||
@@ -90,7 +93,7 @@ def volume_calibration(result_iterator, volume_ctl):
|
||||
for index, result in enumerate(itertools.chain([None], result_iterator)):
|
||||
if index % iters_per_update == 0:
|
||||
if index > 0: # skip dummy (first result)
|
||||
sign = 1 if (result.total < target_level) else -1
|
||||
sign = 1 if (result['total'] < target_level) else -1
|
||||
level = level + step * sign
|
||||
level = min(max(level, min_level), max_level)
|
||||
step = step * 0.5
|
||||
@@ -102,30 +105,37 @@ def volume_calibration(result_iterator, volume_ctl):
|
||||
|
||||
|
||||
def iter_window(iterable, size):
|
||||
# pylint: disable=stop-iteration-return
|
||||
block = []
|
||||
while True:
|
||||
item = next(iterable)
|
||||
for item in iterable:
|
||||
block.append(item)
|
||||
block = block[-size:]
|
||||
if len(block) == size:
|
||||
yield block
|
||||
|
||||
|
||||
def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
|
||||
fmt = '{0.freq:6.0f} Hz: {0.msg:20s}'
|
||||
if verbose:
|
||||
fields = ['total', 'rms', 'coherency', 'peak']
|
||||
fmt += ', '.join('{0}={{0.{0}:.4f}}'.format(f) for f in fields)
|
||||
|
||||
def recv_iter(config, src, volume_cmd=None, dump_audio=None):
|
||||
volume_ctl = volume_controller(volume_cmd)
|
||||
|
||||
if dump_audio:
|
||||
src = stream.Dumper(src, dump_audio)
|
||||
result_iterator = detector(config=config, src=src)
|
||||
result_iterator = volume_calibration(result_iterator, volume_ctl)
|
||||
result_iterator = iter_window(result_iterator, size=3)
|
||||
for r in result_iterator:
|
||||
for _prev, curr, _next in iter_window(result_iterator, size=3):
|
||||
# don't log errors during frequency changes
|
||||
if r[0].success and r[2].success and r[0].freq != r[2].freq:
|
||||
r[1].msg = r[1].msg if r[1].success else 'frequency change'
|
||||
log.info(fmt.format(r[1]))
|
||||
if _prev['success'] and _next['success']:
|
||||
if _prev['freq'] != _next['freq']:
|
||||
if not curr['success']:
|
||||
curr['msg'] = 'frequency change'
|
||||
yield curr
|
||||
|
||||
|
||||
def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
|
||||
fmt = '{freq:6.0f} Hz: {msg:20s}'
|
||||
log.info('verbose: %s', verbose)
|
||||
if verbose:
|
||||
fields = ['total', 'rms', 'coherency', 'peak']
|
||||
fmt += ', '.join('{0}={{{0}:.4f}}'.format(f) for f in fields)
|
||||
|
||||
for state in recv_iter(config, src, volume_cmd, dump_audio):
|
||||
log.info(fmt.format(**state))
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
""" Common package functionality.
|
||||
Commom utilities and procedures for amodem.
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
scaling = 32000.0 # out of 2**15
|
||||
|
||||
|
||||
def load(fileobj):
|
||||
""" Load signal from file object. """
|
||||
return loads(fileobj.read())
|
||||
|
||||
|
||||
def loads(data):
|
||||
""" Load signal from memory buffer. """
|
||||
x = np.frombuffer(data, dtype='int16')
|
||||
x = x / scaling
|
||||
return x
|
||||
|
||||
|
||||
def dumps(sym):
|
||||
""" Dump signal to memory buffer. """
|
||||
sym = sym.real * scaling
|
||||
return sym.astype('int16').tostring()
|
||||
|
||||
|
||||
def iterate(data, size, func=None, truncate=True, index=False):
|
||||
""" Iterate over a signal, taking each time *size* elements. """
|
||||
offset = 0
|
||||
data = iter(data)
|
||||
|
||||
@@ -40,6 +50,9 @@ def iterate(data, size, func=None, truncate=True, index=False):
|
||||
|
||||
|
||||
def split(iterable, n):
|
||||
""" Split an iterable of n-tuples into n iterables of scalars.
|
||||
The k-th iterable will be equivalent to (i[k] for i in iter).
|
||||
"""
|
||||
def _gen(it, index):
|
||||
for item in it:
|
||||
yield item[index]
|
||||
@@ -49,37 +62,22 @@ def split(iterable, n):
|
||||
|
||||
|
||||
def icapture(iterable, result):
|
||||
""" Appends each yielded item to result. """
|
||||
for i in iter(iterable):
|
||||
result.append(i)
|
||||
yield i
|
||||
|
||||
|
||||
def take(iterable, n):
|
||||
""" Take n elements from iterable, and return them as a numpy array. """
|
||||
return np.array(list(itertools.islice(iterable, n)))
|
||||
|
||||
|
||||
# "Python 3" zip re-implementation for Python 2
|
||||
def izip(iterables):
|
||||
iterables = [iter(iterable) for iterable in iterables]
|
||||
while True:
|
||||
yield tuple([next(iterable) for iterable in iterables])
|
||||
|
||||
|
||||
class Dummy(object):
|
||||
class Dummy:
|
||||
""" Dummy placeholder object for testing and mocking. """
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
|
||||
class AttributeHolder(object):
|
||||
|
||||
def __init__(self, d):
|
||||
self.__dict__.update(d)
|
||||
|
||||
def __repr__(self):
|
||||
items = sorted(self.__dict__.items())
|
||||
args = ', '.join('{0}={1}'.format(k, v) for k, v in items)
|
||||
return '{0}({1})'.format(self.__class__.__name__, args)
|
||||
|
||||
@@ -1,39 +1,48 @@
|
||||
"""Configuration class."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Configuration(object):
|
||||
class Configuration:
|
||||
Fs = 32000.0 # sampling frequency [Hz]
|
||||
Tsym = 0.001 # symbol duration [seconds]
|
||||
Nfreq = 8 # number of frequencies used
|
||||
Npoints = 64
|
||||
F0 = 1e3
|
||||
frequencies = [1e3, 8e3] # use 1..8 kHz carriers
|
||||
|
||||
# audio config
|
||||
bits_per_sample = 16
|
||||
sample_size = bits_per_sample // 8
|
||||
latency = 0.1
|
||||
|
||||
# sender config
|
||||
silence_start = 1.0
|
||||
silence_stop = 1.0
|
||||
silence_start = 0.5
|
||||
silence_stop = 0.5
|
||||
|
||||
# receiver config
|
||||
skip_start = 0.1
|
||||
timeout = 60.0
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
self.sample_size = self.bits_per_sample // 8
|
||||
assert self.sample_size * 8 == self.bits_per_sample
|
||||
|
||||
self.Ts = 1.0 / self.Fs
|
||||
self.Fsym = 1 / self.Tsym
|
||||
self.frequencies = self.F0 + np.arange(self.Nfreq) * self.Fsym
|
||||
self.Nsym = int(self.Tsym / self.Ts)
|
||||
self.baud = int(1.0 / self.Tsym)
|
||||
assert self.baud * self.Tsym == 1
|
||||
|
||||
if len(self.frequencies) != 1:
|
||||
first, last = self.frequencies
|
||||
self.frequencies = np.arange(first, last + self.baud, self.baud)
|
||||
|
||||
self.Nfreq = len(self.frequencies)
|
||||
self.carrier_index = 0
|
||||
self.Fc = self.frequencies[self.carrier_index]
|
||||
|
||||
self.Nsym = int(self.Tsym / self.Ts)
|
||||
self.baud = int(1.0 / self.Tsym)
|
||||
|
||||
bits_per_symbol = np.log2(self.Npoints)
|
||||
assert int(bits_per_symbol) == bits_per_symbol
|
||||
bits_per_symbol = int(np.log2(self.Npoints))
|
||||
assert 2 ** bits_per_symbol == self.Npoints
|
||||
self.bits_per_baud = bits_per_symbol * self.Nfreq
|
||||
self.modem_bps = self.baud * self.bits_per_baud
|
||||
self.carriers = np.array([
|
||||
@@ -42,34 +51,40 @@ class Configuration(object):
|
||||
])
|
||||
|
||||
# QAM constellation
|
||||
Nx = 2 ** int(np.ceil(bits_per_symbol / 2))
|
||||
Nx = 2 ** int(np.ceil(bits_per_symbol // 2))
|
||||
Ny = self.Npoints // Nx
|
||||
symbols = [complex(x, y) for x in range(Nx) for y in range(Ny)]
|
||||
symbols = np.array(symbols)
|
||||
symbols = symbols - symbols[-1]/2
|
||||
self.symbols = symbols / np.max(np.abs(symbols))
|
||||
|
||||
|
||||
# MODEM configurations for various bitrates [kbps]
|
||||
bitrates = {
|
||||
1: Configuration(F0=8e3, Npoints=2, Nfreq=1),
|
||||
2: Configuration(F0=8e3, Npoints=4, Nfreq=1),
|
||||
4: Configuration(F0=8e3, Npoints=16, Nfreq=1),
|
||||
8: Configuration(F0=8e3, Npoints=16, Nfreq=2),
|
||||
12: Configuration(F0=6e3, Npoints=16, Nfreq=3),
|
||||
16: Configuration(F0=6e3, Npoints=16, Nfreq=4),
|
||||
18: Configuration(F0=5e3, Npoints=16, Nfreq=5),
|
||||
24: Configuration(F0=5e3, Npoints=16, Nfreq=6),
|
||||
28: Configuration(F0=4e3, Npoints=16, Nfreq=7),
|
||||
32: Configuration(F0=3e3, Npoints=16, Nfreq=8),
|
||||
36: Configuration(F0=4e3, Npoints=64, Nfreq=6),
|
||||
42: Configuration(F0=4e3, Npoints=64, Nfreq=7),
|
||||
48: Configuration(F0=3e3, Npoints=64, Nfreq=8),
|
||||
54: Configuration(F0=3e3, Npoints=64, Nfreq=9),
|
||||
60: Configuration(F0=2e3, Npoints=64, Nfreq=10),
|
||||
64: Configuration(F0=3e3, Npoints=256, Nfreq=8),
|
||||
72: Configuration(F0=2e3, Npoints=256, Nfreq=9),
|
||||
80: Configuration(F0=2e3, Npoints=256, Nfreq=10),
|
||||
1: Configuration(Fs=8e3, Npoints=2, frequencies=[2e3]),
|
||||
2: Configuration(Fs=8e3, Npoints=4, frequencies=[2e3]),
|
||||
4: Configuration(Fs=8e3, Npoints=16, frequencies=[2e3]),
|
||||
8: Configuration(Fs=8e3, Npoints=16, frequencies=[1e3, 2e3]),
|
||||
12: Configuration(Fs=16e3, Npoints=16, frequencies=[3e3, 5e3]),
|
||||
16: Configuration(Fs=16e3, Npoints=16, frequencies=[2e3, 5e3]),
|
||||
20: Configuration(Fs=16e3, Npoints=16, frequencies=[2e3, 6e3]),
|
||||
24: Configuration(Fs=16e3, Npoints=16, frequencies=[1e3, 6e3]),
|
||||
28: Configuration(Fs=32e3, Npoints=16, frequencies=[3e3, 9e3]),
|
||||
32: Configuration(Fs=32e3, Npoints=16, frequencies=[2e3, 9e3]),
|
||||
36: Configuration(Fs=32e3, Npoints=64, frequencies=[4e3, 9e3]),
|
||||
42: Configuration(Fs=32e3, Npoints=64, frequencies=[4e3, 10e3]),
|
||||
48: Configuration(Fs=32e3, Npoints=64, frequencies=[3e3, 10e3]),
|
||||
54: Configuration(Fs=32e3, Npoints=64, frequencies=[2e3, 10e3]),
|
||||
60: Configuration(Fs=32e3, Npoints=64, frequencies=[2e3, 11e3]),
|
||||
64: Configuration(Fs=32e3, Npoints=256, frequencies=[3e3, 10e3]),
|
||||
72: Configuration(Fs=32e3, Npoints=256, frequencies=[2e3, 10e3]),
|
||||
80: Configuration(Fs=32e3, Npoints=256, frequencies=[2e3, 11e3]),
|
||||
}
|
||||
|
||||
fastest = lambda: bitrates[max(bitrates)]
|
||||
slowest = lambda: bitrates[min(bitrates)]
|
||||
|
||||
def fastest():
|
||||
return bitrates[max(bitrates)]
|
||||
|
||||
|
||||
def slowest():
|
||||
return bitrates[min(bitrates)]
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import numpy as np
|
||||
import logging
|
||||
import itertools
|
||||
import collections
|
||||
"""Signal detection capabilities for amodem."""
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import dsp
|
||||
from . import equalizer
|
||||
from . import common
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Detector(object):
|
||||
|
||||
class Detector:
|
||||
|
||||
COHERENCE_THRESHOLD = 0.9
|
||||
|
||||
CARRIER_DURATION = sum(equalizer.prefix)
|
||||
CARRIER_THRESHOLD = int(0.9 * CARRIER_DURATION)
|
||||
SEARCH_WINDOW = int(0.1 * CARRIER_DURATION)
|
||||
|
||||
TIMEOUT = 10.0 # [seconds]
|
||||
START_PATTERN_LENGTH = SEARCH_WINDOW // 4
|
||||
|
||||
def __init__(self, config, pylab):
|
||||
self.freq = config.Fc
|
||||
@@ -26,7 +28,7 @@ class Detector(object):
|
||||
self.Nsym = config.Nsym
|
||||
self.Tsym = config.Tsym
|
||||
self.maxlen = config.baud # 1 second of symbols
|
||||
self.max_offset = self.TIMEOUT * config.Fs
|
||||
self.max_offset = config.timeout * config.Fs
|
||||
self.plt = pylab
|
||||
|
||||
def _wait(self, samples):
|
||||
@@ -66,7 +68,7 @@ class Detector(object):
|
||||
bufs.append(np.array(trailing))
|
||||
|
||||
buf = np.concatenate(bufs)
|
||||
offset = self.find_start(buf, duration=self.CARRIER_DURATION)
|
||||
offset = self.find_start(buf)
|
||||
start_time += (offset / self.Nsym - self.SEARCH_WINDOW) * self.Tsym
|
||||
log.debug('Carrier starts at %.3f ms', start_time * 1e3)
|
||||
|
||||
@@ -76,14 +78,21 @@ class Detector(object):
|
||||
amplitude, freq_err = self.estimate(buf[:prefix_length])
|
||||
return itertools.chain(buf, samples), amplitude, freq_err
|
||||
|
||||
def find_start(self, buf, duration):
|
||||
filt = dsp.FIR(dsp.exp_iwt(self.omega, self.Nsym))
|
||||
p = np.abs(list(filt(buf))) ** 2
|
||||
p = np.cumsum(p)[self.Nsym-1:]
|
||||
p = np.concatenate([[0], p])
|
||||
length = (duration - 1) * self.Nsym
|
||||
correlations = np.abs(p[length:] - p[:-length])
|
||||
offset = np.argmax(correlations)
|
||||
def find_start(self, buf):
|
||||
carrier = dsp.exp_iwt(self.omega, self.Nsym)
|
||||
carrier = np.tile(carrier, self.START_PATTERN_LENGTH)
|
||||
zeroes = carrier * 0.0
|
||||
signal = np.concatenate([zeroes, carrier])
|
||||
signal = (2 ** 0.5) * signal / dsp.norm(signal)
|
||||
|
||||
corr = np.abs(np.correlate(buf, signal))
|
||||
norm_b = np.sqrt(np.correlate(np.abs(buf)**2, np.ones(len(signal))))
|
||||
coeffs = np.zeros_like(corr)
|
||||
coeffs[norm_b > 0.0] = corr[norm_b > 0.0] / norm_b[norm_b > 0.0]
|
||||
|
||||
index = np.argmax(coeffs)
|
||||
log.info('Carrier coherence: %.3f%%', coeffs[index] * 100)
|
||||
offset = index + len(zeroes)
|
||||
return offset
|
||||
|
||||
def estimate(self, buf, skip=5):
|
||||
@@ -93,7 +102,7 @@ class Detector(object):
|
||||
symbols = np.array(symbols[skip:-skip])
|
||||
|
||||
amplitude = np.mean(np.abs(symbols))
|
||||
log.debug('Carrier symbols amplitude : %.3f', amplitude)
|
||||
log.info('Carrier symbols amplitude : %.3f', amplitude)
|
||||
|
||||
phase = np.unwrap(np.angle(symbols)) / (2 * np.pi)
|
||||
indices = np.arange(len(phase))
|
||||
@@ -103,9 +112,6 @@ class Detector(object):
|
||||
self.plt.plot(indices, a * indices + b)
|
||||
|
||||
freq_err = a / (self.Tsym * self.freq)
|
||||
last_phase = a * indices[-1] + b
|
||||
log.debug('Current phase on carrier: %.3f', last_phase)
|
||||
log.debug('Frequency error: %.3f ppm', freq_err * 1e6)
|
||||
log.info('Frequency error: %.3f ppm', freq_err * 1e6)
|
||||
self.plt.title('Frequency drift: {0:.3f} ppm'.format(freq_err * 1e6))
|
||||
|
||||
return amplitude, freq_err
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Digital Signal Processing capabilities for amodem."""
|
||||
|
||||
import numpy as np
|
||||
from numpy import linalg
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class FIR(object):
|
||||
class FIR:
|
||||
def __init__(self, h):
|
||||
self.h = np.array(h)
|
||||
self.x_state = [0] * len(self.h)
|
||||
@@ -18,33 +19,7 @@ class FIR(object):
|
||||
self.x_state = x_
|
||||
|
||||
|
||||
class IIR(object):
|
||||
def __init__(self, b, a):
|
||||
self.b = np.array(b) / a[0]
|
||||
self.a = np.array(a[1:]) / a[0]
|
||||
self.x_state = [0] * len(self.b)
|
||||
self.y_state = [0] * (len(self.a) + 1)
|
||||
|
||||
def __call__(self, x):
|
||||
x_, y_ = self.x_state, self.y_state
|
||||
for v in x:
|
||||
x_ = [v] + x_[:-1]
|
||||
y_ = y_[:-1]
|
||||
num = np.dot(x_, self.b)
|
||||
den = np.dot(y_, self.a)
|
||||
y = num - den
|
||||
y_ = [y] + y_
|
||||
yield y
|
||||
self.x_state, self.y_state = x_, y_
|
||||
|
||||
|
||||
def lfilter(b, a, x):
|
||||
f = IIR(b=b, a=a)
|
||||
y = list(f(x))
|
||||
return np.array(y)
|
||||
|
||||
|
||||
class Demux(object):
|
||||
class Demux:
|
||||
def __init__(self, sampler, omegas, Nsym):
|
||||
self.Nsym = Nsym
|
||||
self.filters = [exp_iwt(-w, Nsym) / (0.5*self.Nsym) for w in omegas]
|
||||
@@ -58,8 +33,7 @@ class Demux(object):
|
||||
frame = self.sampler.take(size=self.Nsym)
|
||||
if len(frame) == self.Nsym:
|
||||
return np.dot(self.filters, frame)
|
||||
else:
|
||||
raise StopIteration
|
||||
raise StopIteration
|
||||
|
||||
__next__ = next
|
||||
|
||||
@@ -72,29 +46,33 @@ def norm(x):
|
||||
return np.sqrt(np.dot(x.conj(), x).real)
|
||||
|
||||
|
||||
def rms(x):
|
||||
return np.mean(np.abs(x) ** 2, axis=0) ** 0.5
|
||||
|
||||
|
||||
def coherence(x, omega):
|
||||
n = len(x)
|
||||
Hc = exp_iwt(-omega, n) / np.sqrt(0.5*n)
|
||||
norm_x = norm(x)
|
||||
if norm_x:
|
||||
return np.dot(Hc, x) / norm_x
|
||||
else:
|
||||
if not norm_x:
|
||||
return 0.0
|
||||
return np.dot(Hc, x) / norm_x
|
||||
|
||||
|
||||
def linear_regression(x, y):
|
||||
''' Find (a,b) such that y = a*x + b. '''
|
||||
""" Find (a,b) such that y = a*x + b. """
|
||||
x = np.array(x)
|
||||
y = np.array(y)
|
||||
ones = np.ones(len(x))
|
||||
M = np.array([x, ones]).T
|
||||
a, b = linalg.lstsq(M, y)[0]
|
||||
mean_x = np.mean(x)
|
||||
mean_y = np.mean(y)
|
||||
x_ = x - mean_x
|
||||
y_ = y - mean_y
|
||||
a = np.dot(y_, x_) / np.dot(x_, x_)
|
||||
b = mean_y - a * mean_x
|
||||
return a, b
|
||||
|
||||
|
||||
class MODEM(object):
|
||||
|
||||
buf_size = 16
|
||||
class MODEM:
|
||||
|
||||
def __init__(self, symbols):
|
||||
self.encode_map = {}
|
||||
@@ -120,14 +98,28 @@ class MODEM(object):
|
||||
yield self.encode_map[bits_tuple]
|
||||
|
||||
def decode(self, symbols, error_handler=None):
|
||||
''' Maximum-likelihood decoding, using naive nearest-neighbour. '''
|
||||
""" Maximum-likelihood decoding, using naive nearest-neighbour. """
|
||||
symbols_vec = self.symbols
|
||||
_dec = self.decode_list
|
||||
for syms in common.iterate(symbols, self.buf_size, truncate=False):
|
||||
for received in syms:
|
||||
error = np.abs(symbols_vec - received)
|
||||
index = np.argmin(error)
|
||||
decoded, bits = _dec[index]
|
||||
if error_handler:
|
||||
error_handler(received=received, decoded=decoded)
|
||||
yield bits
|
||||
for received in symbols:
|
||||
error = np.abs(symbols_vec - received)
|
||||
index = np.argmin(error)
|
||||
decoded, bits = _dec[index]
|
||||
if error_handler:
|
||||
error_handler(received=received, decoded=decoded)
|
||||
yield bits
|
||||
|
||||
|
||||
def prbs(reg, poly, bits):
|
||||
""" Simple pseudo-random number generator. """
|
||||
mask = (1 << bits) - 1
|
||||
|
||||
size = 0 # effective register size (in bits)
|
||||
while (poly >> size) > 1:
|
||||
size += 1
|
||||
|
||||
while True:
|
||||
yield reg & mask
|
||||
reg = reg << 1
|
||||
if reg >> size:
|
||||
reg = reg ^ poly
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import numpy as np
|
||||
from numpy.linalg import lstsq
|
||||
|
||||
from amodem import dsp
|
||||
from amodem import sampling
|
||||
"""Audio equalizing capabilities for amodem."""
|
||||
|
||||
import itertools
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import dsp
|
||||
from . import sampling
|
||||
from . import levinson
|
||||
|
||||
|
||||
class Equalizer(object):
|
||||
|
||||
_constellation = [1, 1j, -1, -1j]
|
||||
class Equalizer:
|
||||
|
||||
def __init__(self, config):
|
||||
self.carriers = config.carriers
|
||||
@@ -18,12 +17,15 @@ class Equalizer(object):
|
||||
self.Nfreq = config.Nfreq
|
||||
self.Nsym = config.Nsym
|
||||
|
||||
def train_symbols(self, length, seed=0, constant_prefix=16):
|
||||
r = random.Random(seed)
|
||||
# Use low-level randomness for cross-version compatibility.
|
||||
random_symbol = lambda: self._constellation[r.getrandbits(2)]
|
||||
choose = lambda: [random_symbol() for j in range(self.Nfreq)]
|
||||
symbols = np.array([choose() for _ in range(length)])
|
||||
def train_symbols(self, length, constant_prefix=16):
|
||||
r = dsp.prbs(reg=1, poly=0x1100b, bits=2)
|
||||
constellation = [1, 1j, -1, -1j]
|
||||
|
||||
symbols = []
|
||||
for _ in range(length):
|
||||
symbols.append([constellation[next(r)] for _ in range(self.Nfreq)])
|
||||
|
||||
symbols = np.array(symbols)
|
||||
# Constant symbols (for analog debugging)
|
||||
symbols[:constant_prefix, :] = 1
|
||||
return symbols
|
||||
@@ -44,28 +46,21 @@ class Equalizer(object):
|
||||
return np.array(list(itertools.islice(symbols, size)))
|
||||
|
||||
|
||||
equalizer_length = 200
|
||||
silence_length = 50
|
||||
prefix = [1]*equalizer_length + [0]*silence_length
|
||||
|
||||
|
||||
def train(signal, expected, order, lookahead=0):
|
||||
signal = [np.zeros(order-1), signal, np.zeros(lookahead)]
|
||||
signal = np.concatenate(signal)
|
||||
length = len(expected)
|
||||
padding = np.zeros(lookahead)
|
||||
assert len(signal) == len(expected)
|
||||
x = np.concatenate([signal, padding])
|
||||
y = np.concatenate([padding, expected])
|
||||
|
||||
A = []
|
||||
b = []
|
||||
# construct Ah=b over-constrained equation system,
|
||||
# used for least-squares estimation of the filter.
|
||||
for i in range(length - order):
|
||||
offset = order + i
|
||||
row = signal[offset-order:offset+lookahead]
|
||||
A.append(np.array(row, ndmin=2))
|
||||
b.append(expected[i])
|
||||
|
||||
A = np.concatenate(A, axis=0)
|
||||
b = np.array(b)
|
||||
h = lstsq(A, b)[0]
|
||||
h = h[::-1].real
|
||||
return h
|
||||
|
||||
|
||||
prefix = [1]*400 + [0]*50
|
||||
equalizer_length = 500
|
||||
silence_length = 100
|
||||
N = order + lookahead # filter length
|
||||
Rxx = np.zeros(N)
|
||||
Rxy = np.zeros(N)
|
||||
for i in range(N):
|
||||
Rxx[i] = np.dot(x[i:], x[:len(x)-i])
|
||||
Rxy[i] = np.dot(y[i:], x[:len(x)-i])
|
||||
return levinson.solver(t=Rxx, y=Rxy)
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
from . import common
|
||||
|
||||
import binascii
|
||||
import functools
|
||||
import itertools
|
||||
import binascii
|
||||
import struct
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from . import common
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_checksum_func = lambda x: binascii.crc32(bytes(x)) & 0xFFFFFFFF
|
||||
# (so the result will be unsigned on Python 2/3)
|
||||
|
||||
def _checksum_func(x):
|
||||
''' The result will be unsigned on Python 2/3. '''
|
||||
return binascii.crc32(bytes(x)) & 0xFFFFFFFF
|
||||
|
||||
|
||||
class Checksum(object):
|
||||
class Checksum:
|
||||
fmt = '>L' # unsigned longs (32-bit)
|
||||
size = struct.calcsize(fmt)
|
||||
|
||||
@@ -24,12 +27,13 @@ class Checksum(object):
|
||||
payload = data[self.size:]
|
||||
expected = _checksum_func(payload)
|
||||
if received != expected:
|
||||
log.warning('Invalid checksum: %04x != %04x', received, expected)
|
||||
log.warning('Invalid checksum: %08x != %08x', received, expected)
|
||||
raise ValueError('Invalid checksum')
|
||||
log.debug('Good checksum: %08x', received)
|
||||
return payload
|
||||
|
||||
|
||||
class Framer(object):
|
||||
class Framer:
|
||||
block_size = 250
|
||||
prefix_fmt = '>B'
|
||||
prefix_len = struct.calcsize(prefix_fmt)
|
||||
@@ -83,7 +87,7 @@ def chain_wrapper(func):
|
||||
return wrapped
|
||||
|
||||
|
||||
class BitPacker(object):
|
||||
class BitPacker:
|
||||
byte_size = 8
|
||||
|
||||
def __init__(self):
|
||||
@@ -113,8 +117,7 @@ def _to_bytes(bits):
|
||||
yield [converter.to_byte[chunk]]
|
||||
|
||||
|
||||
@chain_wrapper
|
||||
def decode(bits, framer=None):
|
||||
def decode_frames(bits, framer=None):
|
||||
framer = framer or Framer()
|
||||
for frame in framer.decode(_to_bytes(bits)):
|
||||
yield frame
|
||||
yield bytes(frame)
|
||||
|
||||
30
amodem/levinson.py
Normal file
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
|
||||
@@ -1,6 +1,8 @@
|
||||
import numpy as np
|
||||
import logging
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import send as _send
|
||||
from . import recv as _recv
|
||||
from . import framing, common, stream, detect, sampling
|
||||
@@ -8,12 +10,12 @@ from . import framing, common, stream, detect, sampling
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send(config, src, dst):
|
||||
sender = _send.Sender(dst, config=config)
|
||||
def send(config, src, dst, gain=1.0, extra_silence=0.0):
|
||||
sender = _send.Sender(dst, config=config, gain=gain)
|
||||
Fs = config.Fs
|
||||
|
||||
# pre-padding audio with silence (priming the audio sending queue)
|
||||
sender.write(np.zeros(int(Fs * config.silence_start)))
|
||||
sender.write(np.zeros(int(Fs * (config.silence_start + extra_silence))))
|
||||
|
||||
sender.start()
|
||||
|
||||
@@ -57,10 +59,11 @@ def recv(config, src, dst, dump_audio=None, pylab=None):
|
||||
gain = 1.0 / amplitude
|
||||
log.debug('Gain correction: %.3f', gain)
|
||||
|
||||
sampler = sampling.Sampler(signal, sampling.Interpolator(), freq=freq)
|
||||
sampler = sampling.Sampler(signal, sampling.defaultInterpolator,
|
||||
freq=freq)
|
||||
receiver.run(sampler, gain=1.0/amplitude, output=dst)
|
||||
return True
|
||||
except BaseException:
|
||||
except BaseException: # pylint: disable=broad-except
|
||||
log.exception('Decoding failed')
|
||||
return False
|
||||
finally:
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import numpy as np
|
||||
import logging
|
||||
import itertools
|
||||
import functools
|
||||
import itertools
|
||||
import logging
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
import numpy as np
|
||||
|
||||
from . import dsp
|
||||
from . import common
|
||||
from . import framing
|
||||
from . import equalizer
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Receiver(object):
|
||||
|
||||
class Receiver:
|
||||
|
||||
def __init__(self, config, pylab=None):
|
||||
self.stats = {}
|
||||
@@ -28,6 +29,7 @@ class Receiver(object):
|
||||
self.equalizer = equalizer.Equalizer(config)
|
||||
self.carrier_index = config.carrier_index
|
||||
self.output_size = 0 # number of bytes written to output stream
|
||||
self.freq_err_gain = 0.01 * self.Tsym # integration feedback gain
|
||||
|
||||
def _prefix(self, symbols, gain=1.0):
|
||||
S = common.take(symbols, len(equalizer.prefix))
|
||||
@@ -41,15 +43,17 @@ class Receiver(object):
|
||||
self.plt.subplot(1, 2, 2)
|
||||
self.plt.plot(np.abs(S))
|
||||
self.plt.plot(equalizer.prefix)
|
||||
if any(bits != equalizer.prefix):
|
||||
raise ValueError('Incorrect prefix')
|
||||
errors = (bits != equalizer.prefix)
|
||||
if any(errors):
|
||||
msg = 'Incorrect prefix: {0} errors'.format(sum(errors))
|
||||
raise ValueError(msg)
|
||||
log.debug('Prefix OK')
|
||||
|
||||
def _train(self, sampler, order, lookahead):
|
||||
Nfreq = len(self.frequencies)
|
||||
equalizer_length = equalizer.equalizer_length
|
||||
train_symbols = self.equalizer.train_symbols(equalizer_length)
|
||||
train_signal = self.equalizer.modulator(train_symbols) * Nfreq
|
||||
train_signal = (self.equalizer.modulator(train_symbols) *
|
||||
len(self.frequencies))
|
||||
|
||||
prefix = postfix = equalizer.silence_length * self.Nsym
|
||||
signal_length = equalizer_length * self.Nsym + prefix + postfix
|
||||
@@ -58,7 +62,7 @@ class Receiver(object):
|
||||
|
||||
coeffs = equalizer.train(
|
||||
signal=signal[prefix:-postfix],
|
||||
expected=train_signal,
|
||||
expected=np.concatenate([train_signal, np.zeros(lookahead)]),
|
||||
order=order, lookahead=lookahead
|
||||
)
|
||||
|
||||
@@ -66,6 +70,8 @@ class Receiver(object):
|
||||
self.plt.plot(np.arange(order+lookahead), coeffs)
|
||||
|
||||
equalization_filter = dsp.FIR(h=coeffs)
|
||||
log.debug('Training completed')
|
||||
# Pre-load equalization filter with the signal (+lookahead)
|
||||
equalized = list(equalization_filter(signal))
|
||||
equalized = equalized[prefix+lookahead:-postfix+lookahead]
|
||||
self._verify_training(equalized, train_symbols)
|
||||
@@ -79,10 +85,9 @@ class Receiver(object):
|
||||
error_rate = errors.sum() / errors.size
|
||||
|
||||
errors = np.array(symbols - train_symbols)
|
||||
rms = lambda x: (np.mean(np.abs(x) ** 2, axis=0) ** 0.5)
|
||||
|
||||
noise_rms = rms(errors)
|
||||
signal_rms = rms(train_symbols)
|
||||
noise_rms = dsp.rms(errors)
|
||||
signal_rms = dsp.rms(train_symbols)
|
||||
SNRs = 20.0 * np.log10(signal_rms / noise_rms)
|
||||
|
||||
self.plt.figure()
|
||||
@@ -91,6 +96,7 @@ class Receiver(object):
|
||||
self._constellation(symbols[:, i], train_symbols[:, i],
|
||||
'$F_c = {0} Hz$'.format(freq), index=i)
|
||||
assert error_rate == 0, error_rate
|
||||
log.debug('Training verified')
|
||||
|
||||
def _bitstream(self, symbols, error_handler):
|
||||
streams = []
|
||||
@@ -105,7 +111,7 @@ class Receiver(object):
|
||||
bits = self.modem.decode(S, freq_handler) # list of bit tuples
|
||||
streams.append(bits) # bit stream per frequency
|
||||
|
||||
return common.izip(streams), symbol_list
|
||||
return zip(*streams), symbol_list
|
||||
|
||||
def _demodulate(self, sampler, symbols):
|
||||
symbol_list = []
|
||||
@@ -135,10 +141,10 @@ class Receiver(object):
|
||||
|
||||
def _update_sampler(self, errors, sampler):
|
||||
err = np.array([e for v in errors.values() for e in v])
|
||||
err = np.mean(np.angle(err))/(2*np.pi) if len(err) else 0
|
||||
err = np.mean(np.angle(err))/(2*np.pi) if err.size else 0
|
||||
errors.clear()
|
||||
|
||||
sampler.freq -= 0.01 * err * self.Tsym
|
||||
sampler.freq -= self.freq_err_gain * err
|
||||
sampler.offset -= err
|
||||
|
||||
def _report_progress(self, noise, sampler):
|
||||
@@ -152,20 +158,19 @@ class Receiver(object):
|
||||
)
|
||||
|
||||
def run(self, sampler, gain, output):
|
||||
log.debug('Receiving')
|
||||
symbols = dsp.Demux(sampler, omegas=self.omegas, Nsym=self.Nsym)
|
||||
self._prefix(symbols, gain=gain)
|
||||
|
||||
filt = self._train(sampler, order=20, lookahead=20)
|
||||
filt = self._train(sampler, order=10, lookahead=10)
|
||||
sampler.equalizer = lambda x: list(filt(x))
|
||||
|
||||
bitstream = self._demodulate(sampler, symbols)
|
||||
bitstream = itertools.chain.from_iterable(bitstream)
|
||||
|
||||
data = framing.decode(bitstream)
|
||||
for chunk in common.iterate(data=data, size=256,
|
||||
truncate=False, func=bytearray):
|
||||
output.write(chunk)
|
||||
self.output_size += len(chunk)
|
||||
for frame in framing.decode_frames(bitstream):
|
||||
output.write(frame)
|
||||
self.output_size += len(frame)
|
||||
|
||||
def report(self):
|
||||
if self.stats:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
import numpy as np
|
||||
import itertools
|
||||
|
||||
from amodem import common
|
||||
import numpy as np
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class Interpolator(object):
|
||||
class Interpolator:
|
||||
|
||||
def __init__(self, resolution=1024, width=128):
|
||||
|
||||
@@ -14,7 +14,7 @@ class Interpolator(object):
|
||||
|
||||
N = resolution * width
|
||||
u = np.arange(-N, N, dtype=float)
|
||||
window = (1 + np.cos(0.5 * np.pi * u / N)) / 2.0 # (Hann window)
|
||||
window = np.cos(0.5 * np.pi * u / N) ** 2.0 # raised cosine
|
||||
|
||||
h = np.sinc(u / resolution) * window
|
||||
self.filt = []
|
||||
@@ -30,7 +30,10 @@ class Interpolator(object):
|
||||
assert len(self.filt) == resolution
|
||||
|
||||
|
||||
class Sampler(object):
|
||||
defaultInterpolator = Interpolator()
|
||||
|
||||
|
||||
class Sampler:
|
||||
def __init__(self, src, interp=None, freq=1.0):
|
||||
self.freq = freq
|
||||
self.equalizer = lambda x: x # LTI equalization filter
|
||||
@@ -57,26 +60,26 @@ class Sampler(object):
|
||||
def _take(self, size):
|
||||
frame = np.zeros(size)
|
||||
count = 0
|
||||
try:
|
||||
for frame_index in range(size):
|
||||
offset = self.offset
|
||||
# offset = k + (j / self.resolution)
|
||||
k = int(offset) # integer part
|
||||
j = int((offset - k) * self.resolution) # fractional part
|
||||
coeffs = self.filt[j] # choose correct filter phase
|
||||
end = k + self.width
|
||||
# process input until all buffer is full with samples
|
||||
for frame_index in range(size):
|
||||
offset = self.offset
|
||||
# offset = k + (j / self.resolution)
|
||||
k = int(offset) # integer part
|
||||
j = int((offset - k) * self.resolution) # fractional part
|
||||
coeffs = self.filt[j] # choose correct filter phase
|
||||
end = k + self.width
|
||||
# process input until all buffer is full with samples
|
||||
try:
|
||||
while self.index < end:
|
||||
self.buff[:-1] = self.buff[1:]
|
||||
self.buff[-1] = next(self.src) # throws StopIteration
|
||||
self.index += 1
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
self.offset += self.freq
|
||||
# apply interpolation filter
|
||||
frame[frame_index] = np.dot(coeffs, self.buff)
|
||||
count = frame_index + 1
|
||||
except StopIteration:
|
||||
pass
|
||||
self.offset += self.freq
|
||||
# apply interpolation filter
|
||||
frame[frame_index] = np.dot(coeffs, self.buff)
|
||||
count = frame_index + 1
|
||||
|
||||
return self.equalizer(frame[:count])
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import numpy as np
|
||||
import logging
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
import numpy as np
|
||||
|
||||
from . import common
|
||||
from . import equalizer
|
||||
from . import dsp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Sender(object):
|
||||
def __init__(self, fd, config):
|
||||
|
||||
class Sender:
|
||||
def __init__(self, fd, config, gain=1.0):
|
||||
self.gain = gain
|
||||
self.offset = 0
|
||||
self.fd = fd
|
||||
self.modem = dsp.MODEM(config.symbols)
|
||||
@@ -22,7 +24,7 @@ class Sender(object):
|
||||
self.equalizer = equalizer.Equalizer(config)
|
||||
|
||||
def write(self, sym):
|
||||
sym = np.array(sym)
|
||||
sym = np.array(sym) * self.gain
|
||||
data = common.dumps(sym)
|
||||
self.fd.write(data)
|
||||
self.offset += len(sym)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
|
||||
|
||||
class Reader(object):
|
||||
class Reader:
|
||||
|
||||
wait = 0.2
|
||||
timeout = 2.0
|
||||
@@ -24,8 +24,7 @@ class Reader(object):
|
||||
self.total += len(data)
|
||||
block.extend(data)
|
||||
return block
|
||||
else:
|
||||
raise StopIteration()
|
||||
raise StopIteration()
|
||||
|
||||
finish_time = time.time() + self.timeout
|
||||
while time.time() <= finish_time:
|
||||
@@ -45,7 +44,7 @@ class Reader(object):
|
||||
__next__ = next
|
||||
|
||||
|
||||
class Dumper(object):
|
||||
class Dumper:
|
||||
def __init__(self, src, dst):
|
||||
self.src = src
|
||||
self.dst = dst
|
||||
|
||||
40
amodem/tests/test_alsa.py
Normal file
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'])]
|
||||
@@ -1,7 +1,7 @@
|
||||
import mock
|
||||
import time
|
||||
import pytest
|
||||
from amodem import async
|
||||
from amodem import async_reader
|
||||
import logging
|
||||
|
||||
logging.basicConfig(format='%(message)s')
|
||||
@@ -13,7 +13,7 @@ def test_async_reader():
|
||||
return b'\x00' * n
|
||||
s = mock.Mock()
|
||||
s.read = _read
|
||||
r = async.AsyncReader(s, 1)
|
||||
r = async_reader.AsyncReader(s, 1)
|
||||
|
||||
n = 5
|
||||
assert r.read(n) == b'\x00' * n
|
||||
@@ -25,6 +25,6 @@ def test_async_reader():
|
||||
def test_async_reader_error():
|
||||
s = mock.Mock()
|
||||
s.read.side_effect = IOError()
|
||||
r = async.AsyncReader(s, 1)
|
||||
r = async_reader.AsyncReader(s, 1)
|
||||
with pytest.raises(IOError):
|
||||
r.read(3)
|
||||
@@ -15,7 +15,8 @@ def test():
|
||||
lib.Pa_OpenStream.return_value = 0
|
||||
cdll.return_value = lib
|
||||
interface = audio.Interface(config=config.fastest(), debug=True)
|
||||
with interface.load(name='portaudio'):
|
||||
assert interface.load(name='portaudio') is interface
|
||||
with interface:
|
||||
s = interface.player()
|
||||
assert s.params.device == 1
|
||||
s.stream = 1 # simulate non-zero output stream handle
|
||||
@@ -1,7 +1,6 @@
|
||||
from amodem import calib
|
||||
from amodem import common
|
||||
from amodem import config
|
||||
config = config.fastest()
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
@@ -10,22 +9,16 @@ import random
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
config = config.fastest()
|
||||
|
||||
class ProcessMock(object):
|
||||
|
||||
class ProcessMock:
|
||||
def __init__(self):
|
||||
self.buf = BytesIO()
|
||||
self.stdin = self
|
||||
self.stdout = self
|
||||
self.bytes_per_sample = 2
|
||||
|
||||
def launch(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
__call__ = launch
|
||||
|
||||
def kill(self):
|
||||
pass
|
||||
|
||||
def write(self, data):
|
||||
assert self.buf.tell() < 10e6
|
||||
self.buf.write(data)
|
||||
@@ -46,8 +39,8 @@ def test_too_strong():
|
||||
calib.send(config, p, gain=1.001, limit=32)
|
||||
p.buf.seek(0)
|
||||
for r in calib.detector(config, src=p):
|
||||
assert not r.success
|
||||
assert r.msg == 'too strong signal'
|
||||
assert not r['success']
|
||||
assert r['msg'] == 'too strong signal'
|
||||
|
||||
|
||||
def test_too_weak():
|
||||
@@ -55,8 +48,8 @@ def test_too_weak():
|
||||
calib.send(config, p, gain=0.01, limit=32)
|
||||
p.buf.seek(0)
|
||||
for r in calib.detector(config, src=p):
|
||||
assert not r.success
|
||||
assert r.msg == 'too weak signal'
|
||||
assert not r['success']
|
||||
assert r['msg'] == 'too weak signal'
|
||||
|
||||
|
||||
def test_too_noisy():
|
||||
@@ -64,8 +57,8 @@ def test_too_noisy():
|
||||
signal = np.array([r.choice([-1, 1]) for i in range(int(config.Fs))])
|
||||
src = BytesIO(common.dumps(signal * 0.5))
|
||||
for r in calib.detector(config, src=src):
|
||||
assert not r.success
|
||||
assert r.msg == 'too noisy signal'
|
||||
assert not r['success']
|
||||
assert r['msg'] == 'too noisy signal'
|
||||
|
||||
|
||||
def test_errors():
|
||||
@@ -101,9 +94,9 @@ def test_drift(freq_err):
|
||||
src = BytesIO(common.dumps(signal))
|
||||
iters = 0
|
||||
for r in calib.detector(config, src, frame_length=frame_length):
|
||||
assert r.success is True
|
||||
assert abs(r.rms - rms) < 1e-3
|
||||
assert abs(r.total - rms) < 1e-3
|
||||
assert r['success'] is True
|
||||
assert abs(r['rms'] - rms) < 1e-3
|
||||
assert abs(r['total'] - rms) < 1e-3
|
||||
iters += 1
|
||||
|
||||
assert iters > 0
|
||||
@@ -153,3 +146,15 @@ def test_recv_binary_search():
|
||||
fmt = 'ctl {0:.0f}%'
|
||||
expected = [mock.call(shell=True, args=fmt.format(100 * g)) for g in gains]
|
||||
assert check_call.mock_calls == expected
|
||||
|
||||
|
||||
def test_recv_freq_change():
|
||||
p = ProcessMock()
|
||||
calib.send(config, p, gain=0.5, limit=2)
|
||||
offset = p.buf.tell() // 16
|
||||
p.buf.seek(offset)
|
||||
messages = [state['msg'] for state in calib.recv_iter(config, p)]
|
||||
assert messages == [
|
||||
'good signal', 'good signal', 'good signal',
|
||||
'frequency change',
|
||||
'good signal', 'good signal', 'good signal']
|
||||
@@ -1,4 +1,5 @@
|
||||
from amodem import common
|
||||
from amodem import config
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -47,15 +48,9 @@ def test_dumps_loads():
|
||||
assert all(x == y)
|
||||
|
||||
|
||||
def test_izip():
|
||||
x = range(10)
|
||||
y = range(-10, 0)
|
||||
assert list(common.izip([x, y])) == list(zip(x, y))
|
||||
|
||||
|
||||
def test_holder():
|
||||
d = {'x': 1, 'y': 2.3}
|
||||
a = common.AttributeHolder(d)
|
||||
assert a.x == d['x']
|
||||
assert a.y == d['y']
|
||||
assert repr(a) == 'AttributeHolder(x=1, y=2.3)'
|
||||
def test_configs():
|
||||
default = config.Configuration()
|
||||
fastest = config.fastest()
|
||||
slowest = config.slowest()
|
||||
assert slowest.modem_bps <= default.modem_bps
|
||||
assert fastest.modem_bps >= default.modem_bps
|
||||
12
amodem/tests/test_configs.py
Normal file
12
amodem/tests/test_configs.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from amodem import config
|
||||
|
||||
|
||||
def test_bitrates():
|
||||
for rate, cfg in sorted(config.bitrates.items()):
|
||||
assert rate * 1000 == cfg.modem_bps
|
||||
|
||||
|
||||
def test_slowest():
|
||||
c = config.slowest()
|
||||
assert c.Npoints == 2
|
||||
assert list(c.symbols) == [-1j, 1j]
|
||||
@@ -19,7 +19,7 @@ def test_detect():
|
||||
detector = detect.Detector(config, pylab=common.Dummy())
|
||||
samples, amp, freq_err = detector.run(x)
|
||||
assert abs(1 - amp) < 1e-12
|
||||
assert abs(freq_err) < 1e-16
|
||||
assert abs(freq_err) < 1e-12
|
||||
|
||||
x = np.cos(2 * np.pi * (2*config.Fc) * t)
|
||||
with pytest.raises(ValueError):
|
||||
@@ -56,6 +56,6 @@ def test_find_start():
|
||||
for offset in range(32):
|
||||
bufs = [prefix, [0] * offset, carrier, postfix]
|
||||
buf = np.concatenate(bufs)
|
||||
start = detector.find_start(buf, length)
|
||||
start = detector.find_start(buf)
|
||||
expected = offset + len(prefix)
|
||||
assert expected == start
|
||||
@@ -1,14 +1,14 @@
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
|
||||
from amodem import dsp
|
||||
from amodem import sampling
|
||||
from amodem import config
|
||||
config = config.fastest()
|
||||
import utils
|
||||
|
||||
import numpy as np
|
||||
import random
|
||||
import itertools
|
||||
|
||||
config = config.fastest()
|
||||
|
||||
|
||||
def test_linreg():
|
||||
x = np.array([1, 3, 2, 8, 4, 6, 9, 7, 0, 5])
|
||||
@@ -21,11 +21,11 @@ def test_linreg():
|
||||
|
||||
def test_filter():
|
||||
x = range(10)
|
||||
y = dsp.lfilter(b=[1], a=[1], x=x)
|
||||
y = utils.lfilter(b=[1], a=[1], x=x)
|
||||
assert (np.array(x) == y).all()
|
||||
|
||||
x = [1] + [0] * 10
|
||||
y = dsp.lfilter(b=[0.5], a=[1, -0.5], x=x)
|
||||
y = utils.lfilter(b=[0.5], a=[1, -0.5], x=x)
|
||||
assert list(y) == [0.5 ** (i+1) for i in range(len(x))]
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ def test_qam():
|
||||
decoded = list(q.decode(S))
|
||||
assert decoded == bits
|
||||
|
||||
noise = lambda A: A*(r.uniform(-1, 1) + 1j*r.uniform(-1, 1))
|
||||
def noise(A):
|
||||
return A*(r.uniform(-1, 1) + 1j*r.uniform(-1, 1))
|
||||
|
||||
noised_symbols = [(s + noise(1e-3)) for s in S]
|
||||
decoded = list(q.decode(noised_symbols))
|
||||
assert decoded == bits
|
||||
@@ -70,3 +72,22 @@ def test_overflow():
|
||||
for i in range(10000):
|
||||
s = 10*(r.normal() + 1j * r.normal())
|
||||
quantize(q, s)
|
||||
|
||||
|
||||
def test_prbs():
|
||||
r = list(itertools.islice(dsp.prbs(reg=1, poly=0x7, bits=2), 4))
|
||||
assert r == [1, 2, 3, 1]
|
||||
|
||||
r = list(itertools.islice(dsp.prbs(reg=1, poly=0x7, bits=1), 4))
|
||||
assert r == [1, 0, 1, 1]
|
||||
|
||||
r = list(itertools.islice(dsp.prbs(reg=1, poly=0xd, bits=3), 8))
|
||||
assert r == [1, 2, 4, 5, 7, 3, 6, 1]
|
||||
|
||||
r = list(itertools.islice(dsp.prbs(reg=1, poly=0xd, bits=2), 8))
|
||||
assert r == [1, 2, 0, 1, 3, 3, 2, 1]
|
||||
|
||||
period = 2 ** 16 - 1
|
||||
r = list(itertools.islice(dsp.prbs(reg=1, poly=0x1100b, bits=16), period))
|
||||
r.sort()
|
||||
assert r == list(range(1, 2 ** 16))
|
||||
@@ -1,15 +1,17 @@
|
||||
from numpy.linalg import norm
|
||||
from numpy.random import RandomState
|
||||
import numpy as np
|
||||
|
||||
from amodem import dsp
|
||||
import utils
|
||||
from amodem import equalizer
|
||||
from amodem import dsp
|
||||
from amodem import config
|
||||
config = config.fastest()
|
||||
|
||||
|
||||
def assert_approx(x, y, e=1e-12):
|
||||
assert norm(x - y) < e * norm(x)
|
||||
x = x.flatten()
|
||||
y = y.flatten()
|
||||
assert dsp.norm(x - y) < e * dsp.norm(x)
|
||||
|
||||
|
||||
def test_training():
|
||||
@@ -24,14 +26,14 @@ def test_commutation():
|
||||
x = np.random.RandomState(seed=0).normal(size=1000)
|
||||
b = [1, 1j, -1, -1j]
|
||||
a = [1, 0.1]
|
||||
y = dsp.lfilter(x=x, b=b, a=a)
|
||||
y1 = dsp.lfilter(x=dsp.lfilter(x=x, b=b, a=[1]), b=[1], a=a)
|
||||
y2 = dsp.lfilter(x=dsp.lfilter(x=x, b=[1], a=a), b=b, a=[1])
|
||||
y = utils.lfilter(x=x, b=b, a=a)
|
||||
y1 = utils.lfilter(x=utils.lfilter(x=x, b=b, a=[1]), b=[1], a=a)
|
||||
y2 = utils.lfilter(x=utils.lfilter(x=x, b=[1], a=a), b=b, a=[1])
|
||||
assert_approx(y, y1)
|
||||
assert_approx(y, y2)
|
||||
|
||||
z = dsp.lfilter(x=y, b=a, a=[1])
|
||||
z_ = dsp.lfilter(x=x, b=b, a=[1])
|
||||
z = utils.lfilter(x=y, b=a, a=[1])
|
||||
z_ = utils.lfilter(x=x, b=b, a=[1])
|
||||
assert_approx(z, z_)
|
||||
|
||||
|
||||
@@ -46,19 +48,20 @@ def test_modem():
|
||||
|
||||
|
||||
def test_signal():
|
||||
length = 100
|
||||
length = 120
|
||||
x = np.sign(RandomState(0).normal(size=length))
|
||||
x[-20:] = 0 # make sure the signal has bounded support
|
||||
den = np.array([1, -0.6, 0.1])
|
||||
num = np.array([0.5])
|
||||
y = dsp.lfilter(x=x, b=num, a=den)
|
||||
y = utils.lfilter(x=x, b=num, a=den)
|
||||
|
||||
lookahead = 2
|
||||
h = equalizer.train(
|
||||
signal=y, expected=x, order=len(den), lookahead=lookahead)
|
||||
assert norm(h[:lookahead]) < 1e-12
|
||||
assert dsp.norm(h[:lookahead]) < 1e-12
|
||||
|
||||
h = h[lookahead:]
|
||||
assert_approx(h, den / num)
|
||||
|
||||
x_ = dsp.lfilter(x=y, b=h, a=[1])
|
||||
x_ = utils.lfilter(x=y, b=h, a=[1])
|
||||
assert_approx(x_, x)
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
def concat(iterable):
|
||||
return bytearray(itertools.chain.from_iterable(iterable))
|
||||
|
||||
|
||||
r = random.Random(0)
|
||||
blob = bytearray(r.randrange(0, 256) for i in range(64 * 1024))
|
||||
|
||||
@@ -31,22 +32,22 @@ def test_framer(data):
|
||||
|
||||
def test_main(data):
|
||||
encoded = framing.encode(data)
|
||||
decoded = framing.decode(encoded)
|
||||
assert bytearray(decoded) == data
|
||||
decoded = framing.decode_frames(encoded)
|
||||
assert concat(decoded) == data
|
||||
|
||||
|
||||
def test_fail():
|
||||
encoded = list(framing.encode(''))
|
||||
encoded[-1] = not encoded[-1]
|
||||
with pytest.raises(ValueError):
|
||||
list(framing.decode(encoded))
|
||||
concat(framing.decode_frames(encoded))
|
||||
|
||||
|
||||
def test_missing():
|
||||
f = framing.Framer()
|
||||
with pytest.raises(ValueError):
|
||||
list(f.decode(b''))
|
||||
concat(f.decode(b''))
|
||||
with pytest.raises(ValueError):
|
||||
list(f.decode(b'\x01'))
|
||||
concat(f.decode(b'\x01'))
|
||||
with pytest.raises(ValueError):
|
||||
list(f.decode(b'\xff'))
|
||||
concat(f.decode(b'\xff'))
|
||||
@@ -20,6 +20,6 @@ def test_resample():
|
||||
|
||||
|
||||
def test_coeffs():
|
||||
I = sampling.Interpolator(width=4, resolution=16)
|
||||
err = I.filt[0] - [0, 0, 0, 1, 0, 0, 0, 0]
|
||||
interp = sampling.Interpolator(width=4, resolution=16)
|
||||
err = interp.filt[0] - [0, 0, 0, 1, 0, 0, 0, 0]
|
||||
assert np.max(np.abs(err)) < 1e-10
|
||||
@@ -1,5 +1,6 @@
|
||||
from amodem import stream
|
||||
import subprocess as sp
|
||||
import sys
|
||||
|
||||
script = br"""
|
||||
import sys
|
||||
@@ -14,7 +15,7 @@ while True:
|
||||
|
||||
|
||||
def test_read():
|
||||
p = sp.Popen(args=['python', '-'], stdin=sp.PIPE, stdout=sp.PIPE)
|
||||
p = sp.Popen(args=[sys.executable, '-'], stdin=sp.PIPE, stdout=sp.PIPE)
|
||||
p.stdin.write(script)
|
||||
p.stdin.close()
|
||||
f = stream.Reader(p.stdout)
|
||||
@@ -29,7 +30,6 @@ def test_read():
|
||||
j += 1
|
||||
|
||||
try:
|
||||
for buf in f:
|
||||
pass
|
||||
next(f)
|
||||
except IOError as e:
|
||||
assert e.args == ('timeout',)
|
||||
@@ -1,35 +1,25 @@
|
||||
from amodem import main
|
||||
from amodem import common
|
||||
from amodem import sampling
|
||||
from amodem import config
|
||||
import utils
|
||||
|
||||
import numpy as np
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
import numpy as np
|
||||
|
||||
from amodem import main
|
||||
from amodem import common
|
||||
from amodem import dsp
|
||||
from amodem import sampling
|
||||
from amodem import config
|
||||
from amodem import async
|
||||
config = config.fastest()
|
||||
|
||||
import pytest
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
logging.basicConfig(level=logging.DEBUG, # useful for debugging
|
||||
format='%(asctime)s %(levelname)-12s %(message)s')
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class Args(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return None
|
||||
|
||||
|
||||
def run(size, chan=None, df=0, success=True, reader=None):
|
||||
def run(size, chan=None, df=0, success=True, cfg=None):
|
||||
if cfg is None:
|
||||
cfg = config.fastest()
|
||||
tx_data = os.urandom(size)
|
||||
tx_audio = BytesIO()
|
||||
main.send(config=config, src=BytesIO(tx_data), dst=tx_audio)
|
||||
main.send(config=cfg, src=BytesIO(tx_data), dst=tx_audio, gain=0.5)
|
||||
|
||||
data = tx_audio.getvalue()
|
||||
data = common.loads(data)
|
||||
@@ -45,11 +35,9 @@ def run(size, chan=None, df=0, success=True, reader=None):
|
||||
rx_data = BytesIO()
|
||||
dump = BytesIO()
|
||||
|
||||
if reader:
|
||||
rx_audio = reader(rx_audio)
|
||||
try:
|
||||
result = main.recv(config=config, src=rx_audio, dst=rx_data,
|
||||
dump_audio=dump)
|
||||
result = main.recv(config=cfg, src=rx_audio, dst=rx_data,
|
||||
dump_audio=dump, pylab=None)
|
||||
finally:
|
||||
rx_audio.close()
|
||||
|
||||
@@ -61,13 +49,27 @@ def run(size, chan=None, df=0, success=True, reader=None):
|
||||
assert rx_data == tx_data
|
||||
|
||||
|
||||
@pytest.fixture(params=[0, 1, 3, 10, 42, 123])
|
||||
@pytest.fixture(params=[0, 1, 3, 10, 16, 17, 42, 123])
|
||||
def small_size(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def test_small(small_size):
|
||||
run(small_size, chan=lambda x: x)
|
||||
@pytest.fixture(params=list(config.bitrates.values()))
|
||||
def all_configs(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def test_small(small_size, all_configs):
|
||||
run(small_size, chan=lambda x: x, cfg=all_configs)
|
||||
|
||||
|
||||
def test_flip():
|
||||
run(16, chan=lambda x: -x)
|
||||
|
||||
|
||||
def test_large_drift():
|
||||
run(1, df=+0.01)
|
||||
run(1, df=-0.01)
|
||||
|
||||
|
||||
def test_error():
|
||||
@@ -76,7 +78,7 @@ def test_error():
|
||||
|
||||
|
||||
@pytest.fixture(params=[sign * mag for sign in (+1, -1)
|
||||
for mag in (0.1, 1, 10, 100, 1e3, 2e3)])
|
||||
for mag in (0.1, 1, 10, 100, 1e3, 10e3)])
|
||||
def freq_err(request):
|
||||
return request.param * 1e-6
|
||||
|
||||
@@ -86,11 +88,11 @@ def test_timing(freq_err):
|
||||
|
||||
|
||||
def test_lowpass():
|
||||
run(1024, chan=lambda x: dsp.lfilter(b=[0.9], a=[1.0, -0.1], x=x))
|
||||
run(1024, chan=lambda x: utils.lfilter(b=[0.9], a=[1.0, -0.1], x=x))
|
||||
|
||||
|
||||
def test_highpass():
|
||||
run(1024, chan=lambda x: dsp.lfilter(b=[0.9], a=[1.0, 0.1], x=x))
|
||||
run(1024, chan=lambda x: utils.lfilter(b=[0.9], a=[1.0, 0.1], x=x))
|
||||
|
||||
|
||||
def test_attenuation():
|
||||
@@ -109,3 +111,12 @@ def test_medium_noise():
|
||||
|
||||
def test_large():
|
||||
run(54321, chan=lambda x: x)
|
||||
|
||||
|
||||
@pytest.fixture(params=sorted(config.bitrates.keys()))
|
||||
def rate(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def test_rate(rate):
|
||||
run(1, cfg=config.bitrates[rate])
|
||||
27
amodem/tests/utils.py
Normal file
27
amodem/tests/utils.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
class IIR:
|
||||
def __init__(self, b, a):
|
||||
self.b = np.array(b) / a[0]
|
||||
self.a = np.array(a[1:]) / a[0]
|
||||
self.x_state = [0] * len(self.b)
|
||||
self.y_state = [0] * (len(self.a) + 1)
|
||||
|
||||
def __call__(self, x):
|
||||
x_, y_ = self.x_state, self.y_state
|
||||
for v in x:
|
||||
x_ = [v] + x_[:-1]
|
||||
y_ = y_[:-1]
|
||||
num = np.dot(x_, self.b)
|
||||
den = np.dot(y_, self.a)
|
||||
y = num - den
|
||||
y_ = [y] + y_
|
||||
yield y
|
||||
self.x_state, self.y_state = x_, y_
|
||||
|
||||
|
||||
def lfilter(b, a, x):
|
||||
f = IIR(b=b, a=a)
|
||||
y = list(f(x))
|
||||
return np.array(y)
|
||||
@@ -1,3 +0,0 @@
|
||||
numpy
|
||||
six
|
||||
argcomplete
|
||||
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,16 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""Script that exposes pylab's spectogram plotting
|
||||
capabilities to the command line. It implements this
|
||||
for amodem.config Configurations.
|
||||
|
||||
"""
|
||||
|
||||
import pylab
|
||||
import numpy as np
|
||||
from amodem import common
|
||||
from amodem.config import Configuration
|
||||
import sys
|
||||
|
||||
|
||||
def spectrogram(t, x, Fs, NFFT=256):
|
||||
@@ -11,10 +21,8 @@ def spectrogram(t, x, Fs, NFFT=256):
|
||||
pylab.specgram(x, NFFT=NFFT, Fs=Fs, noverlap=NFFT/2,
|
||||
cmap=pylab.cm.gist_heat)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from amodem import common
|
||||
from amodem.config import Configuration
|
||||
|
||||
def main():
|
||||
config = Configuration()
|
||||
|
||||
for fname in sys.argv[1:]:
|
||||
@@ -25,3 +33,7 @@ if __name__ == '__main__':
|
||||
spectrogram(t, x, config.Fs)
|
||||
|
||||
pylab.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
15
scripts/profile.sh
Executable file
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
|
||||
@@ -1,18 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""Script that records audio through an interface
|
||||
and stores it into an amodem.config Configuration.
|
||||
|
||||
"""
|
||||
import argparse
|
||||
from amodem import audio
|
||||
from amodem.config import Configuration
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('-l', '--audio-library', default='libportaudio.so')
|
||||
p.add_argument('filename')
|
||||
args = p.parse_args()
|
||||
|
||||
def run(args):
|
||||
config = Configuration()
|
||||
with open(args.filename, 'wb') as dst:
|
||||
print dst
|
||||
interface = audio.Interface(config=config)
|
||||
with interface.load(args.audio_library):
|
||||
src = interface.recorder()
|
||||
@@ -21,8 +20,16 @@ def main():
|
||||
dst.write(src.read(size))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('-l', '--audio-library', default='libportaudio.so')
|
||||
p.add_argument('filename')
|
||||
|
||||
try:
|
||||
main()
|
||||
run(args=p.parse_args())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
2
scripts/record.sh
Executable file
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]))
|
||||
22
scripts/resample.py
Executable file
22
scripts/resample.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""Script that exposes the amodem.resample() function
|
||||
to the command line, taking parameters via standard
|
||||
inputs and returning results via standard outputs.
|
||||
"""
|
||||
|
||||
from amodem.sampling import resample
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('df', type=float)
|
||||
args = p.parse_args()
|
||||
|
||||
resample(src=sys.stdin, dst=sys.stdout, df=args.df)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
44
setup.py
44
setup.py
@@ -2,7 +2,6 @@
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
|
||||
def finalize_options(self):
|
||||
@@ -12,35 +11,34 @@ class PyTest(TestCommand):
|
||||
def run_tests(self):
|
||||
import sys
|
||||
import pytest
|
||||
sys.exit(pytest.main(['tests']))
|
||||
sys.exit(pytest.main(['.']))
|
||||
|
||||
setup(
|
||||
name="amodem",
|
||||
version="1.8",
|
||||
description="Audio Modem Communication Library",
|
||||
author="Roman Zeyde",
|
||||
author_email="roman.zeyde@gmail.com",
|
||||
license="MIT",
|
||||
url="http://github.com/romanz/amodem",
|
||||
name='amodem',
|
||||
version='1.15.2',
|
||||
description='Audio Modem Communication Library',
|
||||
author='Roman Zeyde',
|
||||
author_email='dev@romanzey.de',
|
||||
license='MIT',
|
||||
url='http://github.com/romanz/amodem',
|
||||
packages=['amodem'],
|
||||
tests_require=['pytest'],
|
||||
cmdclass={'test': PyTest},
|
||||
install_requires=['numpy', 'six'],
|
||||
platforms=['POSIX'],
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Information Technology",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3.2",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: System :: Networking",
|
||||
"Topic :: Communications",
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Information Technology',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: Communications',
|
||||
],
|
||||
scripts=['amodem-cli'],
|
||||
entry_points={'console_scripts': ['amodem = amodem.__main__:_main']},
|
||||
)
|
||||
|
||||
12
tox.ini
12
tox.ini
@@ -1,15 +1,15 @@
|
||||
[tox]
|
||||
envlist = py27,py34
|
||||
envlist = py3
|
||||
[testenv]
|
||||
deps=
|
||||
pytest
|
||||
mock
|
||||
pep8
|
||||
pycodestyle
|
||||
coverage
|
||||
pylint
|
||||
six
|
||||
six
|
||||
commands=
|
||||
pep8 amodem/ scripts/ tests/ amodem-cli
|
||||
pylint --extension-pkg-whitelist=numpy --report=no amodem
|
||||
coverage run --source amodem/ -m py.test tests/
|
||||
pycodestyle amodem/ scripts/
|
||||
pylint --extension-pkg-whitelist=numpy --reports=no amodem --rcfile .pylintrc
|
||||
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v
|
||||
coverage report
|
||||
|
||||
Reference in New Issue
Block a user