mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2727ff014a | ||
|
|
fe1714a0bc | ||
|
|
cb8ce9e8ec | ||
|
|
b4dc0922eb | ||
|
|
36f52f9346 | ||
|
|
babd4a5438 | ||
|
|
904966365f | ||
|
|
aa0dd2c2c8 | ||
|
|
1da258ebf8 | ||
|
|
5401206178 | ||
|
|
9b6160ec43 | ||
|
|
04b0142955 | ||
|
|
9dbfcaa870 | ||
|
|
af59eb5bdf | ||
|
|
e4267f236b | ||
|
|
cfc6de9eb0 | ||
|
|
2d890339e2 | ||
|
|
2ee79870c5 | ||
|
|
2bb4956439 | ||
|
|
fd8dc1d8b7 | ||
|
|
5bf2d0f566 | ||
|
|
b91f51df12 | ||
|
|
fcd58d404d | ||
|
|
4ca1cdf23a | ||
|
|
8e8d43b041 | ||
|
|
c2a4bfbd5e | ||
|
|
ae7d742ee4 | ||
|
|
c2c1b89a0e | ||
|
|
750eb5428f | ||
|
|
15f330330c | ||
|
|
96a1abb714 | ||
|
|
a83888ff02 | ||
|
|
a866301774 | ||
|
|
3dcd9f4ccc | ||
|
|
3b1d193b0b | ||
|
|
318a0644de | ||
|
|
807cbc31a2 | ||
|
|
004ad2403f | ||
|
|
a1ad9ff32c | ||
|
|
f086bbfdeb | ||
|
|
bd329c19d0 | ||
|
|
7f9e84dd02 | ||
|
|
93f0396bc5 | ||
|
|
75dd7d28c9 | ||
|
|
b3510c18b3 | ||
|
|
a30ee7f92c | ||
|
|
3901b32cc5 | ||
|
|
1728bba109 | ||
|
|
28bac11e9a | ||
|
|
3ae72091cb | ||
|
|
72ee5147f4 | ||
|
|
bb6e262b57 | ||
|
|
dfd19df01f | ||
|
|
43ed34ede5 | ||
|
|
ffdfce78fc | ||
|
|
5f664e5944 | ||
|
|
3e7b61205c | ||
|
|
cfca2d6cb5 | ||
|
|
4cfe1711b9 | ||
|
|
4b6a7fcf1c | ||
|
|
477013fcdd | ||
|
|
c38208e10b | ||
|
|
c37cf741bc | ||
|
|
2c408907c4 | ||
|
|
ad5e688547 | ||
|
|
11279e26a6 | ||
|
|
1e09e4961e | ||
|
|
2430588077 | ||
|
|
d5b18f922c | ||
|
|
e7bcf5cbe0 | ||
|
|
1d326304e1 | ||
|
|
1f0363941d | ||
|
|
5988586c08 | ||
|
|
e2ed9915ee | ||
|
|
bc33bc1428 | ||
|
|
f3409b8638 | ||
|
|
4d75dba0bc | ||
|
|
fbd34844cf | ||
|
|
0ae80e6d8b | ||
|
|
8557271da7 | ||
|
|
29ec1c4864 | ||
|
|
13bedf50a4 | ||
|
|
a73fddf988 | ||
|
|
3c6ec642eb | ||
|
|
9696e8796a | ||
|
|
2a4297c5fc | ||
|
|
ca93de06af | ||
|
|
f88820e9c3 | ||
|
|
2f90ac7e46 | ||
|
|
0d29eecaa2 | ||
|
|
67b69a62ec | ||
|
|
f532895dd4 | ||
|
|
ec5b5fa4c0 | ||
|
|
b9dc85e857 | ||
|
|
a1f58436d2 | ||
|
|
8378a273c3 | ||
|
|
f4d8c8a06e | ||
|
|
353f8b8211 | ||
|
|
ca14f0862b | ||
|
|
6bf0d4eeda | ||
|
|
3985aa4f34 | ||
|
|
ea5e577953 | ||
|
|
b23a38295b | ||
|
|
75b990473a | ||
|
|
f4f742a7a4 | ||
|
|
da5e971d94 | ||
|
|
c84e081b1c | ||
|
|
6b1e39f48f | ||
|
|
ceb826728a | ||
|
|
c8f5924c12 | ||
|
|
9d754b04cf | ||
|
|
59435e44a5 | ||
|
|
86848fec1a | ||
|
|
77078d6150 | ||
|
|
1da1e22553 | ||
|
|
6f90289d6b | ||
|
|
02b28fc87c | ||
|
|
7c334db8c4 | ||
|
|
61b0299bbb | ||
|
|
3a59a54107 | ||
|
|
6b483335e9 | ||
|
|
4248a0f08a | ||
|
|
1196c2c25e |
@@ -1,14 +1,18 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
install:
|
||||
- pip install .
|
||||
- pip install coveralls
|
||||
- pip install coveralls pep8 mock
|
||||
|
||||
script:
|
||||
- pep8 amodem/ scripts/ tests/ amodem-cli
|
||||
- cd tests
|
||||
- coverage run --source=amodem -m py.test
|
||||
|
||||
|
||||
145
README.md
145
README.md
@@ -2,6 +2,9 @@
|
||||
|
||||
[](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/)
|
||||
|
||||
# Description
|
||||
|
||||
@@ -9,11 +12,11 @@ This program can be used to transmit a specified file between 2 computers, using
|
||||
a simple audio cable (for better SNR and higher speeds) or a simple headset,
|
||||
allowing true air-gapped communication (via a speaker and a microphone).
|
||||
|
||||
The sender modulates an input binary data file into an 32kHz audio file,
|
||||
which is played to the sound card, using `aplay` Linux utility.
|
||||
The sender modulates an input binary data file into an 32kHz audio,
|
||||
which is played to the sound card.
|
||||
|
||||
The receiver side uses `arecord` Linux utility to record the transmitted audio
|
||||
to an audio file, which is demodulated concurrently into an output binary data file.
|
||||
The receiver side records the transmitted audio,
|
||||
which is demodulated concurrently into an output binary data file.
|
||||
|
||||
The process requires a single manual calibration step: the transmitter has to
|
||||
find maximal output volume for its sound card, which will not saturate the
|
||||
@@ -22,24 +25,45 @@ receiving microphone.
|
||||
The modem is using OFDM over an audio cable with the following parameters:
|
||||
|
||||
- Sampling rate: 32 kHz
|
||||
- BAUD rate: 1 kHz
|
||||
- Symbol modulation: 64-QAM
|
||||
- Carriers: (1,2,3,4,5,6,7,8) kHz
|
||||
- Baud rate: 1 kHz
|
||||
- Symbol modulation: BPSK, 4-PSK, 16-QAM ,64-QAM
|
||||
- Carriers: 2-11 kHz
|
||||
|
||||
This way, modem achieves 48kpbs bitrate = 6.0 kB/s.
|
||||
This way, modem may achieve 60kbps bitrate = 7.5 kB/s.
|
||||
|
||||
A simple CRC-32 checksum is used for data integrity verification on each 1KB data frame.
|
||||
A simple CRC-32 checksum is used for data integrity verification
|
||||
on each 250 byte data frame.
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
Run the following command (will also download and install `numpy` and `bitarray` packages):
|
||||
Make sure that `numpy` and `PortAudio v19` packages are installed (on Debian):
|
||||
|
||||
$ sudo pip install amodem
|
||||
$ sudo apt-get install python-numpy portaudio19-dev
|
||||
|
||||
For graphs and visualization (optional), install:
|
||||
Clone and install latest version:
|
||||
|
||||
$ git clone https://github.com/romanz/amodem.git
|
||||
$ pip install --user -e amodem
|
||||
|
||||
For graphs and visualization (optional), install `matplotlib` Python package.
|
||||
|
||||
For validation, run:
|
||||
|
||||
$ export BITRATE=16 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
$ amodem-cli -h
|
||||
usage: amodem-cli [-h] {send,recv} ...
|
||||
|
||||
Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz
|
||||
|
||||
positional arguments:
|
||||
{send,recv}
|
||||
send modulate binary data into audio signal.
|
||||
recv demodulate audio signal into binary data.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
$ sudo pip install matplotlib
|
||||
|
||||
# Calibration
|
||||
|
||||
@@ -48,39 +72,98 @@ following scripts:
|
||||
|
||||
- On the sender's side:
|
||||
```
|
||||
~/sender $ amodem send --calibrate
|
||||
~/sender $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
~/sender $ amodem-cli send --calibrate
|
||||
```
|
||||
|
||||
- On the receiver's side:
|
||||
```
|
||||
~/receiver $ amodem recv --calibrate
|
||||
~/receiver $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR).
|
||||
~/receiver $ amodem-cli recv --calibrate
|
||||
```
|
||||
|
||||
Increase the sender computer's output audio level, until the
|
||||
received **amplitude** and **peak** values are not higher than 0.5,
|
||||
while the **coherence** is 1.0 (to avoid saturation).
|
||||
If BITRATE is not set, the MODEM will use 1 kbps settings (single frequency with BPSK modulation).
|
||||
|
||||
# Testing
|
||||
Change the sender computer's output audio level, until
|
||||
all frequencies are received well:
|
||||
```
|
||||
1000 Hz: good signal
|
||||
2000 Hz: good signal
|
||||
3000 Hz: good signal
|
||||
4000 Hz: good signal
|
||||
5000 Hz: good signal
|
||||
6000 Hz: good signal
|
||||
7000 Hz: good signal
|
||||
8000 Hz: good signal
|
||||
```
|
||||
|
||||
- Prepare the sender (generate random binary data file to be sent):
|
||||
If the signal is "too weak", increase the sender's output audio level.
|
||||
|
||||
If the signal is "too strong", decrease the sender's output audio level.
|
||||
|
||||
If the signal is "too noisy", the SNR is probably too low: decrease the
|
||||
background noise or increase the signal (without causing saturation).
|
||||
|
||||
# Usage
|
||||
|
||||
- Prepare the sender (generate a random binary data file to be sent):
|
||||
|
||||
```
|
||||
~/sender $ dd if=/dev/urandom of=data.tx bs=125kB count=1 status=none
|
||||
~/sender $ dd if=/dev/urandom of=data.tx bs=16kB count=1 status=none
|
||||
~/sender $ sha256sum data.tx
|
||||
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.tx
|
||||
```
|
||||
|
||||
- Start the receiver:
|
||||
- Start the receiver (will wait for the sender to start):
|
||||
```
|
||||
~/receiver $ amodem recv >data.rx
|
||||
~/receiver $ amodem-cli recv -vv -i data.rx
|
||||
```
|
||||
|
||||
- Start the sender:
|
||||
- Start the sender (will modulate the data and start the transmission):
|
||||
```
|
||||
~/sender $ amodem send <data.tx
|
||||
~/sender $ amodem-cli send -vv -o data.tx
|
||||
```
|
||||
|
||||
- After the receiver has finished, verify that the file's hash is the same:
|
||||
- A similar log should be emitted by the sender:
|
||||
```
|
||||
2014-10-23 09:46:36,116 DEBUG MODEM settings: {'F0': 1000.0, 'Nfreq': 8, 'Fs': 32000.0, 'Npoints': 64, 'Tsym': 0.001} amodem:126
|
||||
2014-10-23 09:46:36,116 DEBUG Running: ['aplay', '-', '-q', '-f', 'S16_LE', '-c', '1', '-r', '32000'] wave.py:20
|
||||
2014-10-23 09:46:36,665 INFO Sending 2.150 seconds of training audio send.py:69
|
||||
2014-10-23 09:46:36,665 INFO Starting modulation: <48.000 kbps, 64-QAM, 8 carriers> send.py:74
|
||||
2014-10-23 09:46:37,735 DEBUG Sent 6.0 kB send.py:56
|
||||
2014-10-23 09:46:38,794 DEBUG Sent 12.0 kB send.py:56
|
||||
2014-10-23 09:46:39,440 INFO Sent 16.384 kB @ 2.754 seconds send.py:79
|
||||
```
|
||||
|
||||
- A similar log should be emitted by the receiver:
|
||||
```
|
||||
2014-10-23 09:46:36,116 DEBUG MODEM settings: {'F0': 1000.0, 'Nfreq': 8, 'Fs': 32000.0, 'Npoints': 64, 'Tsym': 0.001} amodem:126
|
||||
2014-10-23 09:46:36,238 DEBUG Running: ['arecord', '-', '-q', '-f', 'S16_LE', '-c', '1', '-r', '32000'] wave.py:20
|
||||
2014-10-23 09:46:36,408 DEBUG Skipping 0.128 seconds recv.py:275
|
||||
2014-10-23 09:46:36,409 INFO Waiting for carrier tone: 1.0 kHz recv.py:282
|
||||
2014-10-23 09:46:37,657 INFO Carrier detected at ~886.0 ms @ 1.0 kHz: coherence=99.996%, amplitude=0.475 recv.py:40
|
||||
2014-10-23 09:46:37,657 DEBUG Buffered 1000 ms of audio recv.py:64
|
||||
2014-10-23 09:46:37,660 DEBUG Carrier starts at 9.531 ms recv.py:73
|
||||
2014-10-23 09:46:38,119 DEBUG Prefix OK recv.py:108
|
||||
2014-10-23 09:46:38,153 DEBUG Current phase on carrier: -0.497 recv.py:121
|
||||
2014-10-23 09:46:38,153 DEBUG Frequency error: 0.02 ppm recv.py:123
|
||||
2014-10-23 09:46:38,682 DEBUG 1.0 kHz: SNR = 34.20 dB recv.py:165
|
||||
2014-10-23 09:46:38,715 DEBUG 2.0 kHz: SNR = 35.05 dB recv.py:165
|
||||
2014-10-23 09:46:38,766 DEBUG 3.0 kHz: SNR = 35.52 dB recv.py:165
|
||||
2014-10-23 09:46:38,803 DEBUG 4.0 kHz: SNR = 35.65 dB recv.py:165
|
||||
2014-10-23 09:46:38,837 DEBUG 5.0 kHz: SNR = 35.03 dB recv.py:165
|
||||
2014-10-23 09:46:38,869 DEBUG 6.0 kHz: SNR = 35.05 dB recv.py:165
|
||||
2014-10-23 09:46:38,907 DEBUG 7.0 kHz: SNR = 34.80 dB recv.py:165
|
||||
2014-10-23 09:46:38,943 DEBUG 8.0 kHz: SNR = 33.74 dB recv.py:165
|
||||
2014-10-23 09:46:38,977 INFO Starting demodulation: <48.000 kbps, 64-QAM, 8 carriers> recv.py:197
|
||||
2014-10-23 09:46:39,619 DEBUG Got 6.0 kB, realtime: 64.18%, drift: +0.02 ppm recv.py:215
|
||||
2014-10-23 09:46:40,538 DEBUG Got 12.0 kB, realtime: 78.03%, drift: +0.02 ppm recv.py:215
|
||||
2014-10-23 09:46:41,306 DEBUG EOF frame detected framing.py:60
|
||||
2014-10-23 09:46:41,306 DEBUG Demodulated 16.520 kB @ 2.329 seconds (84.6% realtime) recv.py:244
|
||||
2014-10-23 09:46:41,306 INFO Received 16.384 kB @ 2.329 seconds = 7.034 kB/s recv.py:247
|
||||
```
|
||||
|
||||
- After the receiver has finished, verify the received file's hash:
|
||||
```
|
||||
~/receiver $ sha256sum data.rx
|
||||
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.rx
|
||||
@@ -90,5 +173,13 @@ while the **coherence** is 1.0 (to avoid saturation).
|
||||
Make sure that `matplotlib` package is installed, and run (at the receiver side):
|
||||
|
||||
```
|
||||
~/receiver $ amodem recv --plot >data.rx
|
||||
~/receiver $ amodem-cli recv --plot -o data.rx
|
||||
```
|
||||
|
||||
|
||||
# Donations
|
||||
|
||||
Want to donate? Feel free.
|
||||
Send to [1C1snTrkHAHM5XnnfuAtiTBaA11HBxjJyv](https://blockchain.info/address/1C1snTrkHAHM5XnnfuAtiTBaA11HBxjJyv).
|
||||
|
||||
Thanks :)
|
||||
|
||||
191
amodem-cli
Executable file
191
amodem-cli
Executable file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zlib
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
if sys.version_info.major == 2:
|
||||
_stdin = sys.stdin
|
||||
_stdout = sys.stdout
|
||||
else:
|
||||
_stdin = sys.stdin.buffer
|
||||
_stdout = sys.stdout.buffer
|
||||
|
||||
try:
|
||||
import argcomplete
|
||||
except ImportError:
|
||||
argcomplete = None
|
||||
|
||||
log = logging.getLogger('__name__')
|
||||
|
||||
from amodem import recv, send, calib, audio
|
||||
from amodem.config import bitrates
|
||||
|
||||
bitrate = os.environ.get('BITRATE', 1)
|
||||
config = bitrates.get(int(bitrate))
|
||||
|
||||
|
||||
class Compressor(object):
|
||||
def __init__(self, stream):
|
||||
self.obj = zlib.compressobj()
|
||||
self.stream = stream
|
||||
|
||||
def read(self, size):
|
||||
while True:
|
||||
data = self.stream.read(size)
|
||||
if data:
|
||||
result = self.obj.compress(data)
|
||||
if not result: # compression is too good :)
|
||||
continue # try again (since falsy data = EOF)
|
||||
elif self.obj:
|
||||
result = self.obj.flush()
|
||||
self.obj = None
|
||||
else:
|
||||
result = '' # EOF marker
|
||||
return result
|
||||
|
||||
|
||||
class Decompressor(object):
|
||||
def __init__(self, stream):
|
||||
self.obj = zlib.decompressobj()
|
||||
self.stream = stream
|
||||
|
||||
def write(self, data):
|
||||
self.stream.write(self.obj.decompress(bytes(data)))
|
||||
|
||||
def flush(self):
|
||||
self.stream.write(self.obj.flush())
|
||||
|
||||
|
||||
def FileType(mode, audio_interface=None):
|
||||
def opener(fname):
|
||||
assert 'r' in mode or 'w' in mode
|
||||
if audio_interface is None and fname is None:
|
||||
fname = '-'
|
||||
|
||||
if fname is None:
|
||||
assert audio_interface is not None
|
||||
if 'r' in mode:
|
||||
return audio_interface.recorder()
|
||||
if 'w' in mode:
|
||||
return audio_interface.player()
|
||||
|
||||
if fname == '-':
|
||||
if 'r' in mode:
|
||||
return _stdin
|
||||
if 'w' in mode:
|
||||
return _stdout
|
||||
|
||||
return open(fname, mode)
|
||||
|
||||
return opener
|
||||
|
||||
|
||||
def main():
|
||||
fmt = ('Audio OFDM MODEM: {0:.1f} kb/s ({1:d}-QAM x {2:d} carriers) '
|
||||
'Fs={3:.1f} kHz')
|
||||
description = fmt.format(config.modem_bps / 1e3, len(config.symbols),
|
||||
config.Nfreq, config.Fs / 1e3)
|
||||
interface = audio.Interface('libportaudio.so', config=config)
|
||||
|
||||
p = argparse.ArgumentParser(description=description)
|
||||
subparsers = p.add_subparsers()
|
||||
|
||||
def wrap(cls, stream, enable):
|
||||
return cls(stream) if enable else stream
|
||||
|
||||
# Modulator
|
||||
sender = subparsers.add_parser(
|
||||
'send', help='modulate binary data into audio signal.')
|
||||
sender.add_argument(
|
||||
'-i', '--input', help='input file (use "-" for stdin).')
|
||||
sender.add_argument(
|
||||
'-o', '--output', help='output file (use "-" for stdout).'
|
||||
' if not specified, `aplay` tool will be used.')
|
||||
sender.add_argument(
|
||||
'-c', '--calibrate', default=False, action='store_true')
|
||||
|
||||
sender.set_defaults(
|
||||
main=lambda config, args: send.main(
|
||||
config, src=wrap(Compressor, args.src, args.zip), dst=args.dst
|
||||
),
|
||||
calib=lambda config, args: calib.send(
|
||||
config, dst=args.dst
|
||||
),
|
||||
input_type=FileType('rb'),
|
||||
output_type=FileType('wb', interface)
|
||||
)
|
||||
|
||||
# Demodulator
|
||||
receiver = subparsers.add_parser(
|
||||
'recv', help='demodulate audio signal into binary data.')
|
||||
receiver.add_argument(
|
||||
'-i', '--input', help='input file (use "-" for stdin).'
|
||||
' if not specified, `arecord` tool will be used.')
|
||||
receiver.add_argument(
|
||||
'-o', '--output', help='output file (use "-" for stdout).')
|
||||
receiver.add_argument(
|
||||
'-c', '--calibrate', default=False, action='store_true')
|
||||
receiver.add_argument(
|
||||
'-d', '--dump', type=FileType('wb'),
|
||||
help='Filename to save recorded audio')
|
||||
receiver.add_argument(
|
||||
'--plot', action='store_true', default=False,
|
||||
help='plot results using pylab module')
|
||||
receiver.set_defaults(
|
||||
main=lambda config, args: recv.main(
|
||||
config, src=args.src, dst=wrap(Decompressor, args.dst, args.zip),
|
||||
pylab=args.pylab, dump_audio=args.dump
|
||||
),
|
||||
calib=lambda config, args: calib.recv(
|
||||
config, src=args.src, verbose=args.verbose
|
||||
),
|
||||
input_type=FileType('rb', interface),
|
||||
output_type=FileType('wb')
|
||||
)
|
||||
|
||||
for sub in subparsers.choices.values():
|
||||
sub.add_argument('-z', '--zip', default=False, action='store_true')
|
||||
g = sub.add_mutually_exclusive_group()
|
||||
g.add_argument('-v', '--verbose', default=0, action='count')
|
||||
g.add_argument('-q', '--quiet', default=False, action='store_true')
|
||||
|
||||
if argcomplete:
|
||||
argcomplete.autocomplete(p)
|
||||
|
||||
with interface:
|
||||
args = p.parse_args()
|
||||
if args.verbose == 0:
|
||||
level, format = 'INFO', '%(message)s'
|
||||
elif args.verbose == 1:
|
||||
level, format = 'DEBUG', '%(message)s'
|
||||
elif args.verbose >= 2:
|
||||
level, format = ('DEBUG', '%(asctime)s %(levelname)-10s '
|
||||
'%(message)-100s '
|
||||
'%(filename)s:%(lineno)d')
|
||||
if args.quiet:
|
||||
level, format = 'WARNING', '%(message)s'
|
||||
logging.basicConfig(level=level, format=format)
|
||||
|
||||
# Parsing and execution
|
||||
log.debug(description)
|
||||
|
||||
args.pylab = None
|
||||
if getattr(args, 'plot', False):
|
||||
import pylab
|
||||
args.pylab = pylab
|
||||
|
||||
args.src = args.input_type(args.input)
|
||||
args.dst = args.output_type(args.output)
|
||||
if args.calibrate:
|
||||
args.calib(config=config, args=args)
|
||||
else:
|
||||
return args.main(config=config, args=args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,2 @@
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
111
amodem/audio.py
Normal file
111
amodem/audio.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import ctypes
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Interface(object):
|
||||
def __init__(self, name, config, debug=False):
|
||||
self.debug = bool(debug)
|
||||
self.lib = ctypes.CDLL(name)
|
||||
self.config = config
|
||||
self.streams = []
|
||||
assert self._error_string(0) == 'Success'
|
||||
|
||||
def _error_string(self, code):
|
||||
return self.call('GetErrorText', code, restype=ctypes.c_char_p)
|
||||
|
||||
def call(self, name, *args, **kwargs):
|
||||
func_name = 'Pa_{0}'.format(name)
|
||||
if self.debug:
|
||||
log.debug('API: %s%s', name, args)
|
||||
func = getattr(self.lib, func_name)
|
||||
func.restype = kwargs.get('restype', self._error_check)
|
||||
return func(*args)
|
||||
|
||||
def _error_check(self, res):
|
||||
if res != 0:
|
||||
raise Exception(res, self._error_string(res))
|
||||
|
||||
def __enter__(self):
|
||||
self.call('Initialize')
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
for s in self.streams:
|
||||
s.close()
|
||||
self.call('Terminate')
|
||||
|
||||
def recorder(self):
|
||||
return Stream(lib=self, config=self.config, read=True)
|
||||
|
||||
def player(self):
|
||||
return Stream(lib=self, config=self.config, write=True)
|
||||
|
||||
|
||||
class Stream(object):
|
||||
|
||||
class Parameters(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('device', ctypes.c_int),
|
||||
('channelCount', ctypes.c_int),
|
||||
('sampleFormat', ctypes.c_ulong),
|
||||
('suggestedLatency', ctypes.c_double),
|
||||
('hostApiSpecificStreamInfo', ctypes.POINTER(None))
|
||||
]
|
||||
|
||||
def __init__(self, lib, config, read=False, write=False):
|
||||
self.lib = lib
|
||||
self.stream = ctypes.POINTER(ctypes.c_void_p)()
|
||||
self.user_data = ctypes.c_void_p(None)
|
||||
self.stream_callback = ctypes.c_void_p(None)
|
||||
self.bytes_per_sample = config.sample_size
|
||||
assert config.bits_per_sample == 16 # just to make sure :)
|
||||
|
||||
read = bool(read)
|
||||
write = bool(write)
|
||||
assert read != write
|
||||
|
||||
direction = 'Input' if read else 'Output'
|
||||
api_name = 'GetDefault{0}Device'.format(direction)
|
||||
index = lib.call(api_name, restype=ctypes.c_int)
|
||||
self.params = Stream.Parameters(
|
||||
device=index, # choose default device
|
||||
channelCount=1, # mono audio
|
||||
sampleFormat=0x00000008, # 16-bit samples (paInt16)
|
||||
suggestedLatency=0.1, # 100ms should be good enough
|
||||
hostApiSpecificStreamInfo=None)
|
||||
|
||||
self.lib.call(
|
||||
'OpenStream',
|
||||
ctypes.byref(self.stream),
|
||||
ctypes.byref(self.params) if read else None,
|
||||
ctypes.byref(self.params) if write else None,
|
||||
ctypes.c_double(config.Fs),
|
||||
ctypes.c_ulong(config.samples_per_buffer),
|
||||
ctypes.c_ulong(0), # no flags (paNoFlag)
|
||||
self.stream_callback,
|
||||
self.user_data)
|
||||
|
||||
self.lib.streams.append(self)
|
||||
self.lib.call('StartStream', self.stream)
|
||||
|
||||
def close(self):
|
||||
if self.stream:
|
||||
self.lib.call('StopStream', self.stream)
|
||||
self.lib.call('CloseStream', self.stream)
|
||||
self.stream = None
|
||||
|
||||
def read(self, size):
|
||||
assert size % self.bytes_per_sample == 0
|
||||
buf = ctypes.create_string_buffer(size)
|
||||
frames = ctypes.c_ulong(size // self.bytes_per_sample)
|
||||
self.lib.call('ReadStream', self.stream, buf, frames)
|
||||
return buf.raw
|
||||
|
||||
def write(self, data):
|
||||
data = bytes(data)
|
||||
assert len(data) % self.bytes_per_sample == 0
|
||||
buf = ctypes.c_char_p(data)
|
||||
frames = ctypes.c_ulong(len(data) // self.bytes_per_sample)
|
||||
self.lib.call('WriteStream', self.stream, buf, frames)
|
||||
112
amodem/calib.py
112
amodem/calib.py
@@ -1,66 +1,88 @@
|
||||
import numpy as np
|
||||
import itertools
|
||||
import logging
|
||||
import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import common
|
||||
from . import config
|
||||
from . import wave
|
||||
|
||||
CALIBRATION_SYMBOLS = int(1.0 * config.Fs)
|
||||
from . import dsp
|
||||
from . import sampling
|
||||
|
||||
ALLOWED_EXCEPTIONS = (IOError, KeyboardInterrupt)
|
||||
|
||||
def send(wave_play=wave.play):
|
||||
t = np.arange(0, CALIBRATION_SYMBOLS) * config.Ts
|
||||
signal = [np.sin(2 * np.pi * f * t) for f in config.frequencies]
|
||||
signal = common.dumps(np.concatenate(signal))
|
||||
|
||||
p = wave_play(stdin=wave.sp.PIPE)
|
||||
fd = p.stdin
|
||||
def send(config, dst):
|
||||
calibration_symbols = int(1.0 * config.Fs)
|
||||
t = np.arange(0, calibration_symbols) * config.Ts
|
||||
signals = [np.sin(2 * np.pi * f * t) for f in config.frequencies]
|
||||
signals = [common.dumps(s) for s in signals]
|
||||
|
||||
try:
|
||||
while True:
|
||||
fd.write(signal)
|
||||
for signal in itertools.cycle(signals):
|
||||
dst.write(signal)
|
||||
except ALLOWED_EXCEPTIONS:
|
||||
pass
|
||||
finally:
|
||||
p.kill()
|
||||
|
||||
|
||||
FRAME_LENGTH = 100 * config.Nsym
|
||||
def frame_iter(config, src, frame_length):
|
||||
frame_size = frame_length * config.Nsym * config.sample_size
|
||||
omegas = 2 * np.pi * config.frequencies / config.Fs
|
||||
|
||||
def recorder(process):
|
||||
frame_size = int(wave.bytes_per_sample * FRAME_LENGTH)
|
||||
fd = process.stdout
|
||||
try:
|
||||
while True:
|
||||
data = fd.read(frame_size)
|
||||
if len(data) < frame_size:
|
||||
return
|
||||
data = common.loads(data)
|
||||
data = data - np.mean(data)
|
||||
yield data
|
||||
except ALLOWED_EXCEPTIONS:
|
||||
pass
|
||||
finally:
|
||||
process.kill()
|
||||
while True:
|
||||
data = src.read(frame_size)
|
||||
if len(data) < frame_size:
|
||||
return
|
||||
data = common.loads(data)
|
||||
frame = data - np.mean(data)
|
||||
|
||||
def recv(wave_record=wave.record, log=sys.stdout.write):
|
||||
t = np.arange(0, FRAME_LENGTH) * config.Ts
|
||||
carriers = [np.exp(2j * np.pi * f * t) for f in config.frequencies]
|
||||
carriers = np.array(carriers) / (0.5 * len(t))
|
||||
sampler = sampling.Sampler(frame)
|
||||
symbols = dsp.Demux(sampler, omegas, config.Nsym)
|
||||
|
||||
symbols = np.array(list(symbols))
|
||||
coeffs = np.mean(np.abs(symbols) ** 2, axis=0) ** 0.5
|
||||
|
||||
for frame in recorder(wave_record(stdout=wave.sp.PIPE)):
|
||||
peak = np.max(np.abs(frame))
|
||||
coeffs = np.dot(carriers, frame)
|
||||
max_index = np.argmax(np.abs(coeffs))
|
||||
max_coeff = coeffs[max_index]
|
||||
total = np.sqrt(np.dot(frame, frame) / (0.5 * len(frame)))
|
||||
yield coeffs, peak, total
|
||||
|
||||
freq = config.frequencies[max_index]
|
||||
rms = abs(max_coeff)
|
||||
total = np.sqrt(np.dot(frame, frame) / (0.5 * len(t)))
|
||||
coherency = rms / total
|
||||
log(fmt.format(freq / 1e3, 100 * coherency, rms, total, peak))
|
||||
|
||||
fmt = '{:4.0f} kHz @ {:6.2f}% : RMS = {:.4f}, Total = {:.4f}, Peak = {:.4f}\n'
|
||||
def detector(config, src, frame_length=200):
|
||||
|
||||
states = [True]
|
||||
errors = ['weak', 'strong', 'noisy']
|
||||
try:
|
||||
for coeffs, peak, total in frame_iter(config, src, frame_length):
|
||||
max_index = np.argmax(coeffs)
|
||||
freq = config.frequencies[max_index]
|
||||
rms = abs(coeffs[max_index])
|
||||
coherency = rms / total
|
||||
flags = [rms > 0.1, peak < 1.0, coherency > 0.99]
|
||||
|
||||
states.append(all(flags))
|
||||
states = states[-2:]
|
||||
|
||||
message = 'good signal'
|
||||
error = not any(states)
|
||||
if error:
|
||||
message = 'too {0} signal'.format(errors[flags.index(False)])
|
||||
|
||||
yield common.AttributeHolder(dict(
|
||||
freq=freq, rms=rms, peak=peak, coherency=coherency,
|
||||
total=total, error=error, message=message
|
||||
))
|
||||
except ALLOWED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
def recv(config, src, verbose=False):
|
||||
fmt = '{0.freq:6.0f} Hz: {0.message:s}'
|
||||
if verbose:
|
||||
fields = ['peak', 'total', 'rms', 'coherency']
|
||||
fmt += ''.join(', {0}={{0.{0}:.4f}}'.format(f) for f in fields)
|
||||
|
||||
for result in detector(config=config, src=src):
|
||||
msg = fmt.format(result)
|
||||
if not result.error:
|
||||
log.info(msg)
|
||||
else:
|
||||
log.error(msg)
|
||||
|
||||
@@ -5,17 +5,6 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
scaling = 32000.0 # out of 2**15
|
||||
SATURATION_THRESHOLD = (2**15 - 1) / scaling
|
||||
|
||||
|
||||
class SaturationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def check_saturation(x):
|
||||
peak = np.max(np.abs(x))
|
||||
if peak >= SATURATION_THRESHOLD:
|
||||
raise SaturationError(peak)
|
||||
|
||||
|
||||
def load(fileobj):
|
||||
@@ -28,14 +17,12 @@ def loads(data):
|
||||
return x
|
||||
|
||||
|
||||
def dumps(sym, n=1):
|
||||
def dumps(sym):
|
||||
sym = sym.real * scaling
|
||||
sym = sym.astype('int16')
|
||||
data = sym.tostring()
|
||||
return data * n
|
||||
return sym.astype('int16').tostring()
|
||||
|
||||
|
||||
def iterate(data, size, func=None, truncate=True, enumerate=False):
|
||||
def iterate(data, size, func=None, truncate=True, index=False):
|
||||
offset = 0
|
||||
data = iter(data)
|
||||
|
||||
@@ -48,7 +35,7 @@ def iterate(data, size, func=None, truncate=True, enumerate=False):
|
||||
done = True
|
||||
|
||||
result = func(buf) if func else np.array(buf)
|
||||
yield (offset, result) if enumerate else result
|
||||
yield (offset, result) if index else result
|
||||
offset += size
|
||||
|
||||
|
||||
@@ -85,3 +72,14 @@ class Dummy(object):
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
|
||||
class AttributeHolder(object):
|
||||
|
||||
def __init__(self, d):
|
||||
self.__dict__.update(d)
|
||||
|
||||
def __repr__(self):
|
||||
items = sorted(self.__dict__.items())
|
||||
args = ', '.join('{0}={1}'.format(k, v) for k, v in items)
|
||||
return '{0}({1})'.format(self.__class__.__name__, args)
|
||||
|
||||
@@ -1,42 +1,66 @@
|
||||
Fs = 32000.0 # sampling frequency [Hz]
|
||||
Tsym = 0.001 # symbol duration [seconds]
|
||||
Nfreq = 8 # number of frequencies used
|
||||
Npoints = 64
|
||||
F0 = 1e3
|
||||
|
||||
# Update default configuration from environment variables
|
||||
settings = {k: v for k, v in locals().items() if not k.startswith('_')}
|
||||
|
||||
import os
|
||||
for k in settings.keys():
|
||||
v = settings[k]
|
||||
settings[k] = type(v)(os.environ.get(k, v))
|
||||
locals().update(settings)
|
||||
|
||||
import numpy as np
|
||||
|
||||
Ts = 1.0 / Fs
|
||||
Fsym = 1 / Tsym
|
||||
frequencies = F0 + np.arange(Nfreq) * Fsym
|
||||
carrier_index = 0
|
||||
Fc = frequencies[carrier_index]
|
||||
Tc = 1.0 / Fc
|
||||
|
||||
# Hexagonal symbol constellation (optimal "sphere packing")
|
||||
I = np.arange(-Npoints, Npoints+1)
|
||||
imag_factor = np.exp(1j * np.pi / 3.0)
|
||||
offset = 0.5
|
||||
symbols = [(x + y*imag_factor + offset) for x in I for y in I]
|
||||
symbols.sort(key=lambda z: (z*z.conjugate()).real)
|
||||
symbols = np.array(symbols[:Npoints])
|
||||
symbols = symbols / np.max(np.abs(symbols))
|
||||
class Configuration(object):
|
||||
Fs = 32000.0 # sampling frequency [Hz]
|
||||
Tsym = 0.001 # symbol duration [seconds]
|
||||
Nfreq = 8 # number of frequencies used
|
||||
Npoints = 64
|
||||
F0 = 1e3
|
||||
|
||||
Nsym = int(Tsym / Ts)
|
||||
baud = int(1/Tsym)
|
||||
# audio config
|
||||
bits_per_sample = 16
|
||||
sample_size = bits_per_sample // 8
|
||||
samples_per_buffer = 4096
|
||||
|
||||
bits_per_symbol = np.log2(Npoints)
|
||||
bits_per_baud = bits_per_symbol * Nfreq
|
||||
modem_bps = baud * bits_per_baud
|
||||
carriers = np.array([
|
||||
np.exp(2j * np.pi * f * np.arange(0, Nsym) * Ts) for f in frequencies
|
||||
])
|
||||
# sender config
|
||||
silence_start = 1.0
|
||||
silence_stop = 1.0
|
||||
|
||||
# receiver config
|
||||
skip_start = 0.1
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
self.Ts = 1.0 / self.Fs
|
||||
self.Fsym = 1 / self.Tsym
|
||||
self.frequencies = self.F0 + np.arange(self.Nfreq) * self.Fsym
|
||||
self.carrier_index = 0
|
||||
self.Fc = self.frequencies[self.carrier_index]
|
||||
|
||||
self.Nsym = int(self.Tsym / self.Ts)
|
||||
self.baud = int(1.0 / self.Tsym)
|
||||
|
||||
bits_per_symbol = np.log2(self.Npoints)
|
||||
assert int(bits_per_symbol) == bits_per_symbol
|
||||
self.bits_per_baud = bits_per_symbol * self.Nfreq
|
||||
self.modem_bps = self.baud * self.bits_per_baud
|
||||
self.carriers = np.array([
|
||||
np.exp(2j * np.pi * f * np.arange(0, self.Nsym) * self.Ts)
|
||||
for f in self.frequencies
|
||||
])
|
||||
|
||||
# QAM constellation
|
||||
Nx = 2 ** int(np.ceil(bits_per_symbol / 2))
|
||||
Ny = self.Npoints // Nx
|
||||
symbols = [complex(x, y) for x in range(Nx) for y in range(Ny)]
|
||||
symbols = np.array(symbols)
|
||||
symbols = symbols - symbols[-1]/2
|
||||
self.symbols = symbols / np.max(np.abs(symbols))
|
||||
|
||||
# MODEM configurations for various bitrates [kbps]
|
||||
bitrates = {
|
||||
1: Configuration(F0=8e3, Npoints=2, Nfreq=1),
|
||||
2: Configuration(F0=8e3, Npoints=4, Nfreq=1),
|
||||
4: Configuration(F0=8e3, Npoints=16, Nfreq=1),
|
||||
8: Configuration(F0=8e3, Npoints=16, Nfreq=2),
|
||||
16: Configuration(F0=6e3, Npoints=16, Nfreq=4),
|
||||
32: Configuration(F0=3e3, Npoints=16, Nfreq=8),
|
||||
48: Configuration(F0=3e3, Npoints=64, Nfreq=8),
|
||||
54: Configuration(F0=3e3, Npoints=64, Nfreq=9),
|
||||
60: Configuration(F0=2e3, Npoints=64, Nfreq=10),
|
||||
}
|
||||
|
||||
fastest = lambda: bitrates[max(bitrates)]
|
||||
slowest = lambda: bitrates[min(bitrates)]
|
||||
|
||||
116
amodem/detect.py
Normal file
116
amodem/detect.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import numpy as np
|
||||
import logging
|
||||
import itertools
|
||||
import collections
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import dsp
|
||||
from . import equalizer
|
||||
from . import common
|
||||
|
||||
|
||||
class Detector(object):
|
||||
|
||||
COHERENCE_THRESHOLD = 0.9
|
||||
|
||||
CARRIER_DURATION = sum(equalizer.prefix)
|
||||
CARRIER_THRESHOLD = int(0.9 * CARRIER_DURATION)
|
||||
SEARCH_WINDOW = int(0.1 * CARRIER_DURATION)
|
||||
|
||||
TIMEOUT = 10.0 # [seconds]
|
||||
|
||||
def __init__(self, config, pylab):
|
||||
self.freq = config.Fc
|
||||
self.omega = 2 * np.pi * self.freq / config.Fs
|
||||
self.Nsym = config.Nsym
|
||||
self.Tsym = config.Tsym
|
||||
self.maxlen = config.baud # 1 second of symbols
|
||||
self.max_offset = self.TIMEOUT * config.Fs
|
||||
self.plt = pylab
|
||||
|
||||
def _wait(self, samples):
|
||||
counter = 0
|
||||
bufs = collections.deque([], maxlen=self.maxlen)
|
||||
for offset, buf in common.iterate(samples, self.Nsym, index=True):
|
||||
if offset > self.max_offset:
|
||||
raise ValueError('Timeout waiting for carrier')
|
||||
bufs.append(buf)
|
||||
|
||||
coeff = dsp.coherence(buf, self.omega)
|
||||
if abs(coeff) > self.COHERENCE_THRESHOLD:
|
||||
counter += 1
|
||||
else:
|
||||
counter = 0
|
||||
|
||||
if counter == self.CARRIER_THRESHOLD:
|
||||
return offset, bufs
|
||||
|
||||
raise ValueError('No carrier detected')
|
||||
|
||||
def run(self, samples):
|
||||
offset, bufs = self._wait(samples)
|
||||
|
||||
length = (self.CARRIER_THRESHOLD - 1) * self.Nsym
|
||||
begin = offset - length
|
||||
|
||||
x = np.concatenate(tuple(bufs)[-self.CARRIER_THRESHOLD:-1])
|
||||
Hc = dsp.exp_iwt(-self.omega, len(x))
|
||||
amplitude = np.abs(np.dot(Hc, x) / (0.5 * len(x)))
|
||||
start_time = begin * self.Tsym / self.Nsym
|
||||
log.info('Carrier detected at ~%.1f ms @ %.1f kHz:'
|
||||
' coherence=%.3f%%, amplitude=%.3f',
|
||||
start_time * 1e3, self.freq / 1e3,
|
||||
np.abs(dsp.coherence(x, self.omega)) * 100, amplitude)
|
||||
|
||||
log.debug('Buffered %d ms of audio', len(bufs))
|
||||
|
||||
bufs = list(bufs)[-self.CARRIER_THRESHOLD-self.SEARCH_WINDOW:]
|
||||
n = self.SEARCH_WINDOW + self.CARRIER_DURATION - self.CARRIER_THRESHOLD
|
||||
trailing = list(itertools.islice(samples, n * self.Nsym))
|
||||
bufs.append(np.array(trailing))
|
||||
|
||||
buf = np.concatenate(bufs)
|
||||
offset = self.find_start(buf, duration=self.CARRIER_DURATION)
|
||||
start_time += (offset / self.Nsym - self.SEARCH_WINDOW) * self.Tsym
|
||||
log.debug('Carrier starts at %.3f ms', start_time * 1e3)
|
||||
|
||||
buf = buf[offset:]
|
||||
|
||||
prefix_length = self.CARRIER_DURATION * self.Nsym
|
||||
amplitude, freq_err = self.estimate(buf[:prefix_length])
|
||||
return itertools.chain(buf, samples), amplitude, freq_err
|
||||
|
||||
def find_start(self, buf, duration):
|
||||
filt = dsp.FIR(dsp.exp_iwt(self.omega, self.Nsym))
|
||||
p = np.abs(list(filt(buf))) ** 2
|
||||
p = np.cumsum(p)[self.Nsym-1:]
|
||||
p = np.concatenate([[0], p])
|
||||
length = (duration - 1) * self.Nsym
|
||||
correlations = np.abs(p[length:] - p[:-length])
|
||||
offset = np.argmax(correlations)
|
||||
return offset
|
||||
|
||||
def estimate(self, buf, skip=5):
|
||||
filt = dsp.exp_iwt(-self.omega, self.Nsym) / (0.5 * self.Nsym)
|
||||
frames = common.iterate(buf, self.Nsym)
|
||||
symbols = [np.dot(filt, frame) for frame in frames]
|
||||
symbols = np.array(symbols[skip:-skip])
|
||||
|
||||
amplitude = np.mean(np.abs(symbols))
|
||||
log.debug('Carrier symbols amplitude : %.3f', amplitude)
|
||||
|
||||
phase = np.unwrap(np.angle(symbols)) / (2 * np.pi)
|
||||
indices = np.arange(len(phase))
|
||||
a, b = dsp.linear_regression(indices, phase)
|
||||
self.plt.figure()
|
||||
self.plt.plot(indices, phase, ':')
|
||||
self.plt.plot(indices, a * indices + b)
|
||||
|
||||
freq_err = a / (self.Tsym * self.freq)
|
||||
last_phase = a * indices[-1] + b
|
||||
log.debug('Current phase on carrier: %.3f', last_phase)
|
||||
log.debug('Frequency error: %.2f ppm', freq_err * 1e6)
|
||||
self.plt.title('Frequency drift: {0:.3f} ppm'.format(freq_err * 1e6))
|
||||
|
||||
return amplitude, freq_err
|
||||
@@ -1,13 +1,23 @@
|
||||
import numpy as np
|
||||
from numpy import linalg
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import config
|
||||
from . import common
|
||||
|
||||
|
||||
class FIR(object):
|
||||
def __init__(self, h):
|
||||
self.h = np.array(h)
|
||||
self.x_state = [0] * len(self.h)
|
||||
|
||||
def __call__(self, x):
|
||||
x_ = self.x_state
|
||||
h = self.h
|
||||
for v in x:
|
||||
x_ = [v] + x_[:-1]
|
||||
yield np.dot(x_, h)
|
||||
self.x_state = x_
|
||||
|
||||
|
||||
class IIR(object):
|
||||
def __init__(self, b, a):
|
||||
self.b = np.array(b) / a[0]
|
||||
@@ -28,45 +38,16 @@ class IIR(object):
|
||||
self.x_state, self.y_state = x_, y_
|
||||
|
||||
|
||||
class FIR(object):
|
||||
def __init__(self, h):
|
||||
self.h = np.array(h)
|
||||
self.x_state = [0] * len(self.h)
|
||||
|
||||
def __call__(self, x):
|
||||
x_ = self.x_state
|
||||
h = self.h
|
||||
for v in x:
|
||||
x_ = [v] + x_[:-1]
|
||||
yield np.dot(x_, h)
|
||||
self.x_state = x_
|
||||
|
||||
|
||||
def lfilter(b, a, x):
|
||||
f = IIR(b=b, a=a)
|
||||
y = list(f(x))
|
||||
return np.array(y)
|
||||
|
||||
|
||||
def estimate(x, y, order, lookahead=0):
|
||||
offset = order - 1
|
||||
assert offset >= lookahead
|
||||
b = y[offset-lookahead:len(x)-lookahead]
|
||||
|
||||
A = [] # columns of x
|
||||
N = len(x) - order + 1
|
||||
for i in range(order):
|
||||
A.append(x[i:N+i])
|
||||
|
||||
# switch to rows for least-squares
|
||||
h = linalg.lstsq(np.array(A).T, b)[0]
|
||||
return h[::-1]
|
||||
|
||||
|
||||
class Demux(object):
|
||||
def __init__(self, sampler, freqs):
|
||||
Nsym = config.Nsym
|
||||
self.filters = [exp_iwt(-f, Nsym) / (0.5*Nsym) for f in freqs]
|
||||
def __init__(self, sampler, omegas, Nsym):
|
||||
self.Nsym = Nsym
|
||||
self.filters = [exp_iwt(-w, Nsym) / (0.5*self.Nsym) for w in omegas]
|
||||
self.filters = np.array(self.filters)
|
||||
self.sampler = sampler
|
||||
|
||||
@@ -74,8 +55,8 @@ class Demux(object):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
frame = self.sampler.take(size=config.Nsym)
|
||||
if len(frame) == config.Nsym:
|
||||
frame = self.sampler.take(size=self.Nsym)
|
||||
if len(frame) == self.Nsym:
|
||||
return np.dot(self.filters, frame)
|
||||
else:
|
||||
raise StopIteration
|
||||
@@ -83,18 +64,17 @@ class Demux(object):
|
||||
__next__ = next
|
||||
|
||||
|
||||
def exp_iwt(freq, n):
|
||||
iwt = 2j * np.pi * freq * np.arange(n) * config.Ts
|
||||
return np.exp(iwt)
|
||||
def exp_iwt(omega, n):
|
||||
return np.exp(1j * omega * np.arange(n))
|
||||
|
||||
|
||||
def norm(x):
|
||||
return np.sqrt(np.dot(x.conj(), x).real)
|
||||
|
||||
|
||||
def coherence(x, freq):
|
||||
def coherence(x, omega):
|
||||
n = len(x)
|
||||
Hc = exp_iwt(-freq, n) / np.sqrt(0.5*n)
|
||||
Hc = exp_iwt(-omega, n) / np.sqrt(0.5*n)
|
||||
norm_x = norm(x)
|
||||
if norm_x:
|
||||
return np.dot(Hc, x) / norm_x
|
||||
@@ -132,7 +112,7 @@ class MODEM(object):
|
||||
self.symbols = symbols
|
||||
self.bits_per_symbol = bits_per_symbol
|
||||
|
||||
bits_map = {symbol: bits for bits, symbol in self.encode_map.items()}
|
||||
bits_map = dict(item[::-1] for item in self.encode_map.items())
|
||||
self.decode_list = [(s, bits_map[s]) for s in self.symbols]
|
||||
|
||||
def encode(self, bits):
|
||||
@@ -151,9 +131,3 @@ class MODEM(object):
|
||||
if error_handler:
|
||||
error_handler(received=received, decoded=decoded)
|
||||
yield bits
|
||||
|
||||
def __repr__(self):
|
||||
return '<{:.3f} kbps, {:d}-QAM, {:d} carriers>'.format(
|
||||
config.modem_bps / 1e3, len(self.symbols), len(config.carriers))
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
@@ -2,7 +2,6 @@ import numpy as np
|
||||
from numpy.linalg import lstsq
|
||||
|
||||
from amodem import dsp
|
||||
from amodem import config
|
||||
from amodem import sampling
|
||||
|
||||
import itertools
|
||||
@@ -11,62 +10,38 @@ import random
|
||||
_constellation = [1, 1j, -1, -1j]
|
||||
|
||||
|
||||
def train_symbols(length, seed=0, Nfreq=config.Nfreq):
|
||||
r = random.Random(seed)
|
||||
choose = lambda: [r.choice(_constellation) for j in range(Nfreq)]
|
||||
return np.array([choose() for i in range(length)])
|
||||
class Equalizer(object):
|
||||
|
||||
def __init__(self, config):
|
||||
self.carriers = config.carriers
|
||||
self.omegas = 2 * np.pi * np.array(config.frequencies) / config.Fs
|
||||
self.Nfreq = config.Nfreq
|
||||
self.Nsym = config.Nsym
|
||||
|
||||
def train_symbols(self, length, seed=0):
|
||||
r = random.Random(seed)
|
||||
choose = lambda: [r.choice(_constellation) for j in range(self.Nfreq)]
|
||||
return np.array([choose() for _ in range(length)])
|
||||
|
||||
def modulator(self, symbols):
|
||||
gain = 1.0 / len(self.carriers)
|
||||
result = []
|
||||
for s in symbols:
|
||||
result.append(np.dot(s, self.carriers))
|
||||
result = np.concatenate(result).real * gain
|
||||
assert np.max(np.abs(result)) <= 1
|
||||
return result
|
||||
|
||||
def demodulator(self, signal, size):
|
||||
signal = itertools.chain(signal, itertools.repeat(0))
|
||||
symbols = dsp.Demux(sampler=sampling.Sampler(signal),
|
||||
omegas=self.omegas, Nsym=self.Nsym)
|
||||
return np.array(list(itertools.islice(symbols, size)))
|
||||
|
||||
|
||||
def modulator(symbols):
|
||||
carriers = config.carriers
|
||||
gain = 1.0 / len(carriers)
|
||||
result = []
|
||||
for s in symbols:
|
||||
result.append(np.dot(s, carriers))
|
||||
result = np.concatenate(result).real * gain
|
||||
assert np.max(np.abs(result)) <= 1
|
||||
return result
|
||||
|
||||
|
||||
def demodulator(signal, size):
|
||||
signal = itertools.chain(signal, itertools.repeat(0))
|
||||
symbols = dsp.Demux(sampling.Sampler(signal), config.frequencies)
|
||||
return np.array(list(itertools.islice(symbols, size)))
|
||||
|
||||
|
||||
def equalize_symbols(signal, symbols, order, lookahead=0):
|
||||
Nsym = config.Nsym
|
||||
Nfreq = config.Nfreq
|
||||
carriers = config.carriers
|
||||
|
||||
assert symbols.shape[1] == Nfreq
|
||||
length = symbols.shape[0]
|
||||
|
||||
matched = np.array(carriers) / (0.5*Nsym)
|
||||
matched = matched[:, ::-1].transpose().conj()
|
||||
signal = np.concatenate([signal, np.zeros(lookahead)])
|
||||
y = dsp.lfilter(x=signal, b=matched, a=[1])
|
||||
|
||||
A = []
|
||||
b = []
|
||||
|
||||
for j in range(Nfreq):
|
||||
for i in range(length):
|
||||
offset = (i+1)*Nsym
|
||||
row = y[offset-order:offset+lookahead, j]
|
||||
A.append(row)
|
||||
b.append(symbols[i, j])
|
||||
|
||||
A = np.array(A)
|
||||
b = np.array(b)
|
||||
h, residuals, rank, sv = lstsq(A, b)
|
||||
h = h[::-1].real
|
||||
|
||||
return h
|
||||
|
||||
|
||||
def equalize_signal(signal, expected, order, lookahead=0):
|
||||
signal = np.concatenate([np.zeros(order-1), signal, np.zeros(lookahead)])
|
||||
def train(signal, expected, order, lookahead=0):
|
||||
signal = [np.zeros(order-1), signal, np.zeros(lookahead)]
|
||||
signal = np.concatenate(signal)
|
||||
length = len(expected)
|
||||
|
||||
A = []
|
||||
@@ -80,6 +55,11 @@ def equalize_signal(signal, expected, order, lookahead=0):
|
||||
|
||||
A = np.concatenate(A, axis=0)
|
||||
b = np.array(b)
|
||||
h, residuals, rank, sv = lstsq(A, b)
|
||||
h = lstsq(A, b)[0]
|
||||
h = h[::-1].real
|
||||
return h
|
||||
|
||||
|
||||
prefix = [1]*400 + [0]*50
|
||||
equalizer_length = 500
|
||||
silence_length = 100
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
''' Reed-Solomon CODEC. '''
|
||||
from . import common
|
||||
import bitarray
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
@@ -9,23 +7,22 @@ import struct
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_crc32 = lambda x, mask: binascii.crc32(x) & mask
|
||||
_checksum_func = lambda x: binascii.crc32(bytes(x)) & 0xFFFFFFFF
|
||||
# (so the result will be unsigned on Python 2/3)
|
||||
|
||||
|
||||
class Checksum(object):
|
||||
fmt = '>L' # unsigned longs (32-bit)
|
||||
size = struct.calcsize(fmt)
|
||||
func = functools.partial(_crc32, mask=0xFFFFFFFF)
|
||||
|
||||
def encode(self, payload):
|
||||
checksum = self.func(payload)
|
||||
checksum = _checksum_func(payload)
|
||||
return struct.pack(self.fmt, checksum) + payload
|
||||
|
||||
def decode(self, data):
|
||||
received, = struct.unpack(self.fmt, data[:self.size])
|
||||
received, = struct.unpack(self.fmt, bytes(data[:self.size]))
|
||||
payload = data[self.size:]
|
||||
expected = self.func(payload)
|
||||
expected = _checksum_func(payload)
|
||||
if received != expected:
|
||||
log.warning('Invalid checksum: %04x != %04x', received, expected)
|
||||
raise ValueError('Invalid checksum')
|
||||
@@ -33,8 +30,8 @@ class Checksum(object):
|
||||
|
||||
|
||||
class Framer(object):
|
||||
block_size = 1024
|
||||
prefix_fmt = '>L'
|
||||
block_size = 250
|
||||
prefix_fmt = '>B'
|
||||
prefix_len = struct.calcsize(prefix_fmt)
|
||||
checksum = Checksum()
|
||||
|
||||
@@ -42,7 +39,7 @@ class Framer(object):
|
||||
|
||||
def _pack(self, block):
|
||||
frame = self.checksum.encode(block)
|
||||
return struct.pack(self.prefix_fmt, len(frame)) + frame
|
||||
return bytearray(struct.pack(self.prefix_fmt, len(frame)) + frame)
|
||||
|
||||
def encode(self, data):
|
||||
for block in common.iterate(data=data, size=self.block_size,
|
||||
@@ -53,8 +50,8 @@ class Framer(object):
|
||||
def decode(self, data):
|
||||
data = iter(data)
|
||||
while True:
|
||||
length, = self._take_fmt(data, self.prefix_fmt)
|
||||
frame = self._take_len(data, length)
|
||||
length, = _take_fmt(data, self.prefix_fmt)
|
||||
frame = _take_len(data, length)
|
||||
block = self.checksum.decode(frame)
|
||||
if block == self.EOF:
|
||||
log.debug('EOF frame detected')
|
||||
@@ -62,18 +59,20 @@ class Framer(object):
|
||||
|
||||
yield block
|
||||
|
||||
def _take_fmt(self, data, fmt):
|
||||
length = struct.calcsize(fmt)
|
||||
chunk = bytearray(itertools.islice(data, length))
|
||||
if len(chunk) < length:
|
||||
raise ValueError('missing prefix data')
|
||||
return struct.unpack(fmt, chunk)
|
||||
|
||||
def _take_len(self, data, length):
|
||||
chunk = bytearray(itertools.islice(data, length))
|
||||
if len(chunk) < length:
|
||||
raise ValueError('missing payload data')
|
||||
return chunk
|
||||
def _take_fmt(data, fmt):
|
||||
length = struct.calcsize(fmt)
|
||||
chunk = bytearray(itertools.islice(data, length))
|
||||
if len(chunk) < length:
|
||||
raise ValueError('missing prefix data')
|
||||
return struct.unpack(fmt, bytes(chunk))
|
||||
|
||||
|
||||
def _take_len(data, length):
|
||||
chunk = bytearray(itertools.islice(data, length))
|
||||
if len(chunk) < length:
|
||||
raise ValueError('missing payload data')
|
||||
return chunk
|
||||
|
||||
|
||||
def chain_wrapper(func):
|
||||
@@ -84,20 +83,34 @@ def chain_wrapper(func):
|
||||
return wrapped
|
||||
|
||||
|
||||
class BitPacker(object):
|
||||
byte_size = 8
|
||||
|
||||
def __init__(self):
|
||||
bits_list = []
|
||||
for index in range(2 ** self.byte_size):
|
||||
bits = [index & (2 ** k) for k in range(self.byte_size)]
|
||||
bits_list.append(tuple((1 if b else 0) for b in bits))
|
||||
|
||||
self.to_bits = dict((i, bits) for i, bits in enumerate(bits_list))
|
||||
self.to_byte = dict((bits, i) for i, bits in enumerate(bits_list))
|
||||
|
||||
|
||||
@chain_wrapper
|
||||
def encode(data, framer=None):
|
||||
converter = BitPacker()
|
||||
framer = framer or Framer()
|
||||
for frame in framer.encode(data):
|
||||
bits = bitarray.bitarray(endian='little')
|
||||
bits.frombytes(bytes(frame))
|
||||
yield bits
|
||||
for byte in frame:
|
||||
yield converter.to_bits[byte]
|
||||
|
||||
|
||||
@chain_wrapper
|
||||
def _to_bytes(bits, block_size=1):
|
||||
for chunk in common.iterate(data=bits, size=8*block_size,
|
||||
func=lambda x: x, truncate=True):
|
||||
yield bitarray.bitarray(chunk, endian='little').tobytes()
|
||||
def _to_bytes(bits):
|
||||
converter = BitPacker()
|
||||
for chunk in common.iterate(data=bits, size=8,
|
||||
func=tuple, truncate=True):
|
||||
yield [converter.to_byte[chunk]]
|
||||
|
||||
|
||||
@chain_wrapper
|
||||
|
||||
277
amodem/recv.py
277
amodem/recv.py
@@ -2,7 +2,6 @@ import numpy as np
|
||||
import logging
|
||||
import itertools
|
||||
import functools
|
||||
import collections
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -10,89 +9,31 @@ log = logging.getLogger(__name__)
|
||||
from . import stream
|
||||
from . import dsp
|
||||
from . import sampling
|
||||
from . import train
|
||||
from . import common
|
||||
from . import config
|
||||
from . import framing
|
||||
from . import equalizer
|
||||
|
||||
modem = dsp.MODEM(config.symbols)
|
||||
|
||||
# Plots' size (WIDTH x HEIGHT)
|
||||
HEIGHT = np.floor(np.sqrt(config.Nfreq))
|
||||
WIDTH = np.ceil(config.Nfreq / float(HEIGHT))
|
||||
|
||||
COHERENCE_THRESHOLD = 0.99
|
||||
|
||||
CARRIER_DURATION = sum(train.prefix)
|
||||
CARRIER_THRESHOLD = int(0.99 * CARRIER_DURATION)
|
||||
SEARCH_WINDOW = 10 # symbols
|
||||
|
||||
|
||||
def report_carrier(bufs, begin):
|
||||
x = np.concatenate(tuple(bufs)[-CARRIER_THRESHOLD:-1])
|
||||
Hc = dsp.exp_iwt(-config.Fc, len(x))
|
||||
Zc = np.dot(Hc, x) / (0.5*len(x))
|
||||
amp = abs(Zc)
|
||||
log.info('Carrier detected at ~%.1f ms @ %.1f kHz:'
|
||||
' coherence=%.3f%%, amplitude=%.3f',
|
||||
begin * config.Tsym * 1e3 / config.Nsym, config.Fc / 1e3,
|
||||
np.abs(dsp.coherence(x, config.Fc)) * 100, amp)
|
||||
return amp
|
||||
|
||||
|
||||
def detect(samples, freq):
|
||||
counter = 0
|
||||
bufs = collections.deque([], maxlen=config.baud) # 1 second of symbols
|
||||
for offset, buf in common.iterate(samples, config.Nsym, enumerate=True):
|
||||
bufs.append(buf)
|
||||
|
||||
coeff = dsp.coherence(buf, config.Fc)
|
||||
if abs(coeff) > COHERENCE_THRESHOLD:
|
||||
counter += 1
|
||||
else:
|
||||
counter = 0
|
||||
|
||||
if counter == CARRIER_THRESHOLD:
|
||||
length = (CARRIER_THRESHOLD - 1) * config.Nsym
|
||||
begin = offset - length
|
||||
amplitude = report_carrier(bufs, begin=begin)
|
||||
break
|
||||
else:
|
||||
raise ValueError('No carrier detected')
|
||||
|
||||
log.debug('Buffered %d ms of audio', len(bufs))
|
||||
|
||||
bufs = list(bufs)[-CARRIER_THRESHOLD-SEARCH_WINDOW:]
|
||||
trailing = list(itertools.islice(samples, SEARCH_WINDOW*config.Nsym))
|
||||
bufs.append(np.array(trailing))
|
||||
|
||||
buf = np.concatenate(bufs)
|
||||
offset = find_start(buf, CARRIER_DURATION*config.Nsym)
|
||||
log.debug('Carrier starts at %.3f ms',
|
||||
offset * config.Tsym * 1e3 / config.Nsym)
|
||||
|
||||
return itertools.chain(buf[offset:], samples), amplitude
|
||||
|
||||
|
||||
def find_start(buf, length):
|
||||
N = len(buf)
|
||||
carrier = dsp.exp_iwt(config.Fc, N)
|
||||
z = np.cumsum(buf * carrier)
|
||||
z = np.concatenate([[0], z])
|
||||
correlations = np.abs(z[length:] - z[:-length])
|
||||
return np.argmax(correlations)
|
||||
from . import detect
|
||||
|
||||
|
||||
class Receiver(object):
|
||||
|
||||
def __init__(self, plt=None):
|
||||
def __init__(self, config, pylab=None):
|
||||
self.stats = {}
|
||||
self.plt = plt or common.Dummy()
|
||||
self.plt = pylab
|
||||
self.modem = dsp.MODEM(config.symbols)
|
||||
self.frequencies = np.array(config.frequencies)
|
||||
self.omegas = 2 * np.pi * self.frequencies / config.Fs
|
||||
self.Nsym = config.Nsym
|
||||
self.Tsym = config.Tsym
|
||||
self.iters_per_update = 100 # [ms]
|
||||
self.modem_bitrate = config.modem_bps
|
||||
self.equalizer = equalizer.Equalizer(config)
|
||||
self.carrier_index = config.carrier_index
|
||||
self.output_size = 0 # number of bytes written to output stream
|
||||
|
||||
def _prefix(self, sampler, freq, gain=1.0, skip=5):
|
||||
symbols = dsp.Demux(sampler, [freq])
|
||||
S = common.take(symbols, len(train.prefix)).squeeze() * gain
|
||||
def _prefix(self, symbols, gain=1.0):
|
||||
S = common.take(symbols, len(equalizer.prefix))
|
||||
S = S[:, self.carrier_index] * gain
|
||||
sliced = np.round(np.abs(S))
|
||||
self.plt.figure()
|
||||
self.plt.subplot(121)
|
||||
@@ -101,54 +42,40 @@ class Receiver(object):
|
||||
bits = np.array(sliced, dtype=int)
|
||||
self.plt.subplot(122)
|
||||
self.plt.plot(np.abs(S))
|
||||
self.plt.plot(train.prefix)
|
||||
if any(bits != train.prefix):
|
||||
self.plt.plot(equalizer.prefix)
|
||||
if any(bits != equalizer.prefix):
|
||||
raise ValueError('Incorrect prefix')
|
||||
|
||||
log.debug('Prefix OK')
|
||||
|
||||
nonzeros = np.array(train.prefix, dtype=bool)
|
||||
pilot_tone = S[nonzeros]
|
||||
phase = np.unwrap(np.angle(pilot_tone)) / (2 * np.pi)
|
||||
indices = np.arange(len(phase))
|
||||
a, b = dsp.linear_regression(indices[skip:-skip], phase[skip:-skip])
|
||||
self.plt.figure()
|
||||
self.plt.plot(indices, phase, ':')
|
||||
self.plt.plot(indices, a * indices + b)
|
||||
|
||||
freq_err = a / (config.Tsym * config.Fc)
|
||||
last_phase = a * indices[-1] + b
|
||||
log.debug('Current phase on carrier: %.3f', last_phase)
|
||||
|
||||
log.debug('Frequency error: %.2f ppm', freq_err * 1e6)
|
||||
self.plt.title('Frequency drift: {:.3f} ppm'.format(freq_err * 1e6))
|
||||
return freq_err
|
||||
|
||||
def _train(self, sampler, order, lookahead):
|
||||
gain = config.Nfreq
|
||||
train_symbols = equalizer.train_symbols(train.equalizer_length)
|
||||
train_signal = equalizer.modulator(train_symbols) * gain
|
||||
Nfreq = len(self.frequencies)
|
||||
equalizer_length = equalizer.equalizer_length
|
||||
train_symbols = self.equalizer.train_symbols(equalizer_length)
|
||||
train_signal = self.equalizer.modulator(train_symbols) * Nfreq
|
||||
|
||||
prefix = postfix = train.silence_length * config.Nsym
|
||||
signal_length = train.equalizer_length * config.Nsym + prefix + postfix
|
||||
prefix = postfix = equalizer.silence_length * self.Nsym
|
||||
signal_length = equalizer_length * self.Nsym + prefix + postfix
|
||||
|
||||
signal = sampler.take(signal_length + lookahead)
|
||||
|
||||
coeffs = equalizer.equalize_signal(
|
||||
coeffs = equalizer.train(
|
||||
signal=signal[prefix:-postfix],
|
||||
expected=train_signal,
|
||||
order=order, lookahead=lookahead
|
||||
)
|
||||
|
||||
log.debug(
|
||||
'Equalization filter: [%s]',
|
||||
', '.join('{:.2f}'.format(c) for c in coeffs)
|
||||
)
|
||||
self.plt.figure()
|
||||
self.plt.plot(np.arange(order+lookahead), coeffs)
|
||||
|
||||
equalization_filter = dsp.FIR(h=coeffs)
|
||||
equalized = list(equalization_filter(signal))
|
||||
equalized = equalized[prefix+lookahead:-postfix+lookahead]
|
||||
self._verify_training(equalized, train_symbols)
|
||||
return equalization_filter
|
||||
|
||||
symbols = equalizer.demodulator(equalized, train.equalizer_length)
|
||||
def _verify_training(self, equalized, train_symbols):
|
||||
equalizer_length = equalizer.equalizer_length
|
||||
symbols = self.equalizer.demodulator(equalized, equalizer_length)
|
||||
sliced = np.array(symbols).round()
|
||||
errors = np.array(sliced - train_symbols, dtype=np.bool)
|
||||
error_rate = errors.sum() / errors.size
|
||||
@@ -161,17 +88,13 @@ class Receiver(object):
|
||||
SNRs = 20.0 * np.log10(signal_rms / noise_rms)
|
||||
|
||||
self.plt.figure()
|
||||
for i, freq, snr in zip(range(config.Nfreq), config.frequencies, SNRs):
|
||||
for (i, freq), snr in zip(enumerate(self.frequencies), SNRs):
|
||||
log.debug('%5.1f kHz: SNR = %5.2f dB', freq / 1e3, snr)
|
||||
self.plt.subplot(HEIGHT, WIDTH, i+1)
|
||||
self._constellation(symbols[:, i], train_symbols[:, i],
|
||||
'$F_c = {} Hz$'.format(freq))
|
||||
|
||||
'$F_c = {0} Hz$'.format(freq), index=i)
|
||||
assert error_rate == 0, error_rate
|
||||
|
||||
return equalization_filter
|
||||
|
||||
def _demodulate(self, sampler, freqs):
|
||||
def _demodulate(self, sampler, symbols):
|
||||
streams = []
|
||||
symbol_list = []
|
||||
errors = {}
|
||||
@@ -179,82 +102,86 @@ class Receiver(object):
|
||||
def error_handler(received, decoded, freq):
|
||||
errors.setdefault(freq, []).append(received / decoded)
|
||||
|
||||
symbols = dsp.Demux(sampler, freqs)
|
||||
generators = common.split(symbols, n=len(freqs))
|
||||
for freq, S in zip(freqs, generators):
|
||||
generators = common.split(symbols, n=len(self.omegas))
|
||||
for freq, S in zip(self.frequencies, generators):
|
||||
equalized = []
|
||||
S = common.icapture(S, result=equalized)
|
||||
symbol_list.append(equalized)
|
||||
|
||||
freq_handler = functools.partial(error_handler, freq=freq)
|
||||
bits = modem.decode(S, freq_handler) # list of bit tuples
|
||||
bits = self.modem.decode(S, freq_handler) # list of bit tuples
|
||||
streams.append(bits) # stream per frequency
|
||||
|
||||
self.stats['symbol_list'] = symbol_list
|
||||
self.stats['rx_bits'] = 0
|
||||
self.stats['rx_start'] = time.time()
|
||||
|
||||
log.info('Starting demodulation: %s', modem)
|
||||
for i, block in enumerate(common.izip(streams)): # block per frequency
|
||||
log.info('Starting demodulation')
|
||||
for i, block in enumerate(common.izip(streams), 1):
|
||||
for bits in block:
|
||||
self.stats['rx_bits'] = self.stats['rx_bits'] + len(bits)
|
||||
yield bits
|
||||
|
||||
if i > 0 and i % config.baud == 0:
|
||||
err = np.array([e for v in errors.values() for e in v])
|
||||
err = np.mean(np.angle(err))/(2*np.pi) if len(err) else 0
|
||||
errors.clear()
|
||||
if i % self.iters_per_update == 0:
|
||||
self._update_sampler(i, errors, sampler)
|
||||
|
||||
duration = time.time() - self.stats['rx_start']
|
||||
sampler.freq -= 0.01 * err / config.Fc
|
||||
sampler.offset -= err
|
||||
log.debug(
|
||||
'Got %8.1f kB, realtime: %6.2f%%, drift: %+5.2f ppm',
|
||||
self.stats['rx_bits'] / 8e3,
|
||||
duration * 100.0 / (i*config.Tsym),
|
||||
(1.0 - sampler.freq) * 1e6
|
||||
)
|
||||
def _update_sampler(self, iter_index, errors, sampler):
|
||||
err = np.array([e for v in errors.values() for e in v])
|
||||
err = np.mean(np.angle(err))/(2*np.pi) if len(err) else 0
|
||||
errors.clear()
|
||||
|
||||
def start(self, signal, freqs, gain=1.0):
|
||||
sampler = sampling.Sampler(signal, sampling.Interpolator())
|
||||
duration = time.time() - self.stats['rx_start']
|
||||
sampler.freq -= 0.01 * err * self.Tsym
|
||||
sampler.offset -= err
|
||||
log.debug(
|
||||
'Got %10.3f kB, realtime: %6.2f%%, drift: %+5.2f ppm',
|
||||
self.stats['rx_bits'] / 8e3,
|
||||
duration * 100.0 / (iter_index * self.Tsym),
|
||||
(1.0 - sampler.freq) * 1e6
|
||||
)
|
||||
|
||||
freq_err = self._prefix(sampler, freq=freqs[0], gain=gain)
|
||||
sampler.freq -= freq_err
|
||||
def run(self, sampler, gain, output):
|
||||
symbols = dsp.Demux(sampler, omegas=self.omegas, Nsym=self.Nsym)
|
||||
self._prefix(symbols, gain=gain)
|
||||
|
||||
filt = self._train(sampler, order=11, lookahead=5)
|
||||
filt = self._train(sampler, order=20, lookahead=20)
|
||||
sampler.equalizer = lambda x: list(filt(x))
|
||||
|
||||
bitstream = self._demodulate(sampler, freqs)
|
||||
self.bitstream = itertools.chain.from_iterable(bitstream)
|
||||
bitstream = self._demodulate(sampler, symbols)
|
||||
bitstream = itertools.chain.from_iterable(bitstream)
|
||||
|
||||
def run(self, output):
|
||||
data = framing.decode(self.bitstream)
|
||||
self.size = 0
|
||||
data = framing.decode(bitstream)
|
||||
for chunk in common.iterate(data=data, size=256,
|
||||
truncate=False, func=bytearray):
|
||||
output.write(chunk)
|
||||
self.size += len(chunk)
|
||||
self.output_size += len(chunk)
|
||||
|
||||
def report(self):
|
||||
if self.stats:
|
||||
duration = time.time() - self.stats['rx_start']
|
||||
audio_time = self.stats['rx_bits'] / float(config.modem_bps)
|
||||
audio_time = self.stats['rx_bits'] / float(self.modem_bitrate)
|
||||
log.debug('Demodulated %.3f kB @ %.3f seconds (%.1f%% realtime)',
|
||||
self.stats['rx_bits'] / 8e3, duration,
|
||||
100 * duration / audio_time if audio_time else 0)
|
||||
|
||||
log.info('Received %.3f kB @ %.3f seconds = %.3f kB/s',
|
||||
self.size * 1e-3, duration, self.size * 1e-3 / duration)
|
||||
self.output_size * 1e-3, duration,
|
||||
self.output_size * 1e-3 / duration)
|
||||
|
||||
self.plt.figure()
|
||||
symbol_list = np.array(self.stats['symbol_list'])
|
||||
for i, freq in enumerate(config.frequencies):
|
||||
self.plt.subplot(HEIGHT, WIDTH, i+1)
|
||||
self._constellation(symbol_list[i], config.symbols,
|
||||
'$F_c = {} Hz$'.format(freq))
|
||||
for i, freq in enumerate(self.frequencies):
|
||||
self._constellation(symbol_list[i], self.modem.symbols,
|
||||
'$F_c = {0} Hz$'.format(freq), index=i)
|
||||
self.plt.show()
|
||||
|
||||
def _constellation(self, y, symbols, title):
|
||||
def _constellation(self, y, symbols, title, index=None):
|
||||
if index is not None:
|
||||
Nfreq = len(self.frequencies)
|
||||
height = np.floor(np.sqrt(Nfreq))
|
||||
width = np.ceil(Nfreq / float(height))
|
||||
self.plt.subplot(height, width, index + 1)
|
||||
|
||||
theta = np.linspace(0, 2*np.pi, 1000)
|
||||
y = np.array(y)
|
||||
self.plt.plot(y.real, y.imag, '.')
|
||||
@@ -267,25 +194,45 @@ class Receiver(object):
|
||||
self.plt.title(title)
|
||||
|
||||
|
||||
def main(args):
|
||||
reader = stream.Reader(args.input, data_type=common.loads)
|
||||
class Dumper(object):
|
||||
def __init__(self, src, dst):
|
||||
self.src = src
|
||||
self.dst = dst
|
||||
|
||||
def read(self, size):
|
||||
data = self.src.read(size)
|
||||
self.dst.write(data)
|
||||
return data
|
||||
|
||||
|
||||
def main(config, src, dst, dump_audio=None, pylab=None):
|
||||
if dump_audio:
|
||||
src = Dumper(src, dump_audio)
|
||||
reader = stream.Reader(src, data_type=common.loads)
|
||||
signal = itertools.chain.from_iterable(reader)
|
||||
|
||||
skipped = common.take(signal, args.skip)
|
||||
log.debug('Skipping %.3f seconds', len(skipped) / float(config.baud))
|
||||
log.debug('Skipping %.3f seconds', config.skip_start)
|
||||
common.take(signal, int(config.skip_start * config.Fs))
|
||||
|
||||
reader.check = common.check_saturation
|
||||
|
||||
receiver = Receiver(plt=args.plot)
|
||||
success = False
|
||||
pylab = pylab or common.Dummy()
|
||||
detector = detect.Detector(config=config, pylab=pylab)
|
||||
receiver = Receiver(config=config, pylab=pylab)
|
||||
try:
|
||||
log.info('Waiting for carrier tone: %.1f kHz', config.Fc / 1e3)
|
||||
signal, amplitude = detect(signal, config.Fc)
|
||||
receiver.start(signal, config.frequencies, gain=1.0/amplitude)
|
||||
receiver.run(args.output)
|
||||
success = True
|
||||
signal, amplitude, freq_error = detector.run(signal)
|
||||
|
||||
freq = 1 / (1.0 + freq_error) # receiver's compensated frequency
|
||||
log.debug('Frequency correction: %.3f ppm', (freq - 1) * 1e6)
|
||||
|
||||
gain = 1.0 / amplitude
|
||||
log.debug('Gain correction: %.3f', gain)
|
||||
|
||||
sampler = sampling.Sampler(signal, sampling.Interpolator(), freq=freq)
|
||||
receiver.run(sampler, gain=1.0/amplitude, output=dst)
|
||||
return True
|
||||
except Exception:
|
||||
log.exception('Decoding failed')
|
||||
|
||||
receiver.report()
|
||||
return success
|
||||
return False
|
||||
finally:
|
||||
dst.flush()
|
||||
receiver.report()
|
||||
|
||||
@@ -1,51 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
import numpy as np
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from amodem import common
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Interpolator(object):
|
||||
def __init__(self, resolution=10000, width=128):
|
||||
|
||||
def __init__(self, resolution=1024, width=128):
|
||||
|
||||
self.width = width
|
||||
self.resolution = resolution
|
||||
|
||||
N = resolution * width
|
||||
u = np.arange(-N, N, dtype=float)
|
||||
window = (1 + np.cos(0.5 * np.pi * u / N)) / 2.0
|
||||
window = (1 + np.cos(0.5 * np.pi * u / N)) / 2.0 # (Hann window)
|
||||
|
||||
h = np.sinc(u / resolution) * window
|
||||
self.filt = []
|
||||
for index in range(resolution): # split into multiphase filters
|
||||
filt = h[index::resolution]
|
||||
filt = filt[::-1]
|
||||
filt = filt[::-1] # flip (due to convolution)
|
||||
self.filt.append(filt)
|
||||
lengths = map(len, self.filt)
|
||||
self.coeff_len = 2*width
|
||||
assert set(lengths) == set([self.coeff_len])
|
||||
|
||||
lengths = [len(f) for f in self.filt]
|
||||
self.coeff_len = 2 * width
|
||||
|
||||
assert set(lengths) == set([self.coeff_len]) # verify same lengths
|
||||
assert len(self.filt) == resolution
|
||||
|
||||
|
||||
class Sampler(object):
|
||||
def __init__(self, src, interp=None):
|
||||
self.freq = 1.0
|
||||
self.equalizer = lambda x: x
|
||||
def __init__(self, src, interp=None, freq=1.0):
|
||||
self.freq = freq
|
||||
self.equalizer = lambda x: x # LTI equalization filter
|
||||
if interp is not None:
|
||||
self.interp = interp
|
||||
self.resolution = self.interp.resolution
|
||||
self.filt = self.interp.filt
|
||||
self.width = self.interp.width
|
||||
|
||||
# TODO: explain indices arithmetic
|
||||
# polyphase filters are centered at (width + 1) index
|
||||
padding = [0.0] * self.interp.width
|
||||
# pad with zeroes to "simulate" regular sampling
|
||||
self.src = itertools.chain(padding, src)
|
||||
self.offset = self.interp.width + 1
|
||||
# samples' buffer to be used by interpolation
|
||||
self.buff = np.zeros(self.interp.coeff_len)
|
||||
self.index = 0
|
||||
self.take = self._take
|
||||
else:
|
||||
# skip interpolation
|
||||
# skip interpolation (for testing)
|
||||
src = iter(src)
|
||||
self.take = lambda size: common.take(src, size)
|
||||
|
||||
@@ -58,14 +63,16 @@ class Sampler(object):
|
||||
# offset = k + (j / self.resolution)
|
||||
k = int(offset) # integer part
|
||||
j = int((offset - k) * self.resolution) # fractional part
|
||||
coeffs = self.filt[j]
|
||||
coeffs = self.filt[j] # choose correct filter phase
|
||||
end = k + self.width
|
||||
# process input until all buffer is full with samples
|
||||
while self.index < end:
|
||||
self.buff[:-1] = self.buff[1:]
|
||||
self.buff[-1] = next(self.src) # throws StopIteration
|
||||
self.index += 1
|
||||
|
||||
self.offset += self.freq
|
||||
# apply interpolation filter
|
||||
frame[frame_index] = np.dot(coeffs, self.buff)
|
||||
count = frame_index + 1
|
||||
except StopIteration:
|
||||
@@ -75,7 +82,6 @@ class Sampler(object):
|
||||
|
||||
|
||||
def resample(src, dst, df=0.0):
|
||||
from . import common
|
||||
x = common.load(src)
|
||||
sampler = Sampler(x, Interpolator())
|
||||
sampler.freq += df
|
||||
|
||||
@@ -4,79 +4,74 @@ import itertools
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import train
|
||||
from . import wave
|
||||
|
||||
from . import common
|
||||
from . import config
|
||||
from . import stream
|
||||
from . import framing
|
||||
from . import equalizer
|
||||
from . import dsp
|
||||
|
||||
modem = dsp.MODEM(config.symbols)
|
||||
|
||||
|
||||
class Writer(object):
|
||||
def __init__(self, fd):
|
||||
class Sender(object):
|
||||
def __init__(self, fd, config):
|
||||
self.offset = 0
|
||||
self.fd = fd
|
||||
self.modem = dsp.MODEM(config.symbols)
|
||||
self.carriers = config.carriers / config.Nfreq
|
||||
self.pilot = config.carriers[config.carrier_index]
|
||||
self.silence = np.zeros(equalizer.silence_length * config.Nsym)
|
||||
self.iters_per_report = config.baud # report once per second
|
||||
self.padding = [0] * config.bits_per_baud
|
||||
self.equalizer = equalizer.Equalizer(config)
|
||||
|
||||
def write(self, sym, n=1):
|
||||
def write(self, sym):
|
||||
sym = np.array(sym)
|
||||
data = common.dumps(sym, n)
|
||||
data = common.dumps(sym)
|
||||
self.fd.write(data)
|
||||
self.offset += len(data)
|
||||
self.offset += len(sym)
|
||||
|
||||
def start(self):
|
||||
carrier = config.carriers[config.carrier_index]
|
||||
for value in train.prefix:
|
||||
self.write(carrier * value)
|
||||
for value in equalizer.prefix:
|
||||
self.write(self.pilot * value)
|
||||
|
||||
silence = np.zeros(train.silence_length * config.Nsym)
|
||||
symbols = equalizer.train_symbols(train.equalizer_length)
|
||||
signal = equalizer.modulator(symbols)
|
||||
self.write(silence)
|
||||
symbols = self.equalizer.train_symbols(equalizer.equalizer_length)
|
||||
signal = self.equalizer.modulator(symbols)
|
||||
self.write(self.silence)
|
||||
self.write(signal)
|
||||
self.write(silence)
|
||||
self.write(self.silence)
|
||||
|
||||
def modulate(self, bits):
|
||||
padding = [0] * config.bits_per_baud
|
||||
bits = itertools.chain(bits, padding)
|
||||
symbols_iter = modem.encode(bits)
|
||||
carriers = config.carriers / config.Nfreq
|
||||
for i, symbols in common.iterate(symbols_iter,
|
||||
size=config.Nfreq, enumerate=True):
|
||||
symbols = np.array(list(symbols))
|
||||
self.write(np.dot(symbols, carriers))
|
||||
|
||||
data_duration = (i / config.Nfreq + 1) * config.Tsym
|
||||
if data_duration % 1 == 0:
|
||||
bits_size = data_duration * config.modem_bps
|
||||
log.debug('Sent %8.1f kB', bits_size / 8e3)
|
||||
bits = itertools.chain(bits, self.padding)
|
||||
Nfreq = len(self.carriers)
|
||||
symbols_iter = common.iterate(self.modem.encode(bits), size=Nfreq)
|
||||
for i, symbols in enumerate(symbols_iter, 1):
|
||||
self.write(np.dot(symbols, self.carriers))
|
||||
if i % self.iters_per_report == 0:
|
||||
total_bits = i * Nfreq * self.modem.bits_per_symbol
|
||||
log.debug('Sent %10.3f kB', total_bits / 8e3)
|
||||
|
||||
|
||||
def main(args):
|
||||
writer = Writer(args.output)
|
||||
def main(config, src, dst):
|
||||
sender = Sender(dst, config=config)
|
||||
Fs = config.Fs
|
||||
|
||||
# pre-padding audio with silence
|
||||
writer.write(np.zeros(int(config.Fs * args.silence_start)))
|
||||
sender.write(np.zeros(int(Fs * config.silence_start)))
|
||||
|
||||
writer.start()
|
||||
sender.start()
|
||||
|
||||
training_size = writer.offset
|
||||
training_duration = training_size / wave.bytes_per_second
|
||||
log.info('Sending %.3f seconds of training audio', training_duration)
|
||||
training_duration = sender.offset
|
||||
log.info('Sending %.3f seconds of training audio', training_duration / Fs)
|
||||
|
||||
reader = stream.Reader(args.input, bufsize=(64 << 10), eof=True)
|
||||
reader = stream.Reader(src, eof=True)
|
||||
data = itertools.chain.from_iterable(reader)
|
||||
bits = framing.encode(data)
|
||||
log.info('Starting modulation: %s', modem)
|
||||
writer.modulate(bits=bits)
|
||||
log.info('Starting modulation')
|
||||
sender.modulate(bits=bits)
|
||||
|
||||
data_size = writer.offset - training_size
|
||||
data_duration = sender.offset - training_duration
|
||||
log.info('Sent %.3f kB @ %.3f seconds',
|
||||
reader.total / 1e3, data_size / wave.bytes_per_second)
|
||||
reader.total / 1e3, data_duration / Fs)
|
||||
|
||||
# post-padding audio with silence
|
||||
writer.write(np.zeros(int(config.Fs * args.silence_stop)))
|
||||
sender.write(np.zeros(int(Fs * config.silence_stop)))
|
||||
return True
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import time
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Reader(object):
|
||||
|
||||
def __init__(self, fd, data_type=None, bufsize=4096,
|
||||
eof=False, timeout=2.0, wait=0.2):
|
||||
wait = 0.2
|
||||
timeout = 2.0
|
||||
bufsize = 4096
|
||||
|
||||
def __init__(self, fd, data_type=None, eof=False):
|
||||
self.fd = fd
|
||||
self.data_type = data_type if (data_type is not None) else lambda x: x
|
||||
self.bufsize = bufsize
|
||||
self.eof = eof
|
||||
self.timeout = timeout
|
||||
self.wait = wait
|
||||
self.total = 0
|
||||
self.check = None
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
@@ -40,10 +36,7 @@ class Reader(object):
|
||||
block.extend(data)
|
||||
|
||||
if len(block) == self.bufsize:
|
||||
values = self.data_type(block)
|
||||
if self.check:
|
||||
self.check(values)
|
||||
return values
|
||||
return self.data_type(block)
|
||||
|
||||
time.sleep(self.wait)
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
prefix = [1]*400 + [0]*50
|
||||
equalizer_length = 500
|
||||
silence_length = 100
|
||||
@@ -1,27 +0,0 @@
|
||||
import subprocess as sp
|
||||
import logging
|
||||
import functools
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import config
|
||||
Fs = int(config.Fs) # sampling rate
|
||||
|
||||
bits_per_sample = 16
|
||||
bytes_per_sample = bits_per_sample / 8.0
|
||||
bytes_per_second = bytes_per_sample * Fs
|
||||
|
||||
audio_format = 'S{}_LE'.format(bits_per_sample) # PCM signed little endian
|
||||
|
||||
|
||||
def launch(tool, fname=None, **kwargs):
|
||||
fname = fname or '-'
|
||||
args = [tool, fname, '-q', '-f', audio_format, '-c', '1', '-r', str(Fs)]
|
||||
log.debug('Running: %r', args)
|
||||
p = sp.Popen(args=args, **kwargs)
|
||||
return p
|
||||
|
||||
|
||||
# Use ALSA tools for audio playing/recording
|
||||
play = functools.partial(launch, tool='aplay')
|
||||
record = functools.partial(launch, tool='arecord')
|
||||
@@ -1,3 +1,2 @@
|
||||
numpy
|
||||
bitarray
|
||||
reedsolo
|
||||
argcomplete
|
||||
|
||||
163
scripts/amodem
163
scripts/amodem
@@ -1,163 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
if sys.version_info.major == 2:
|
||||
_stdin = sys.stdin
|
||||
_stdout = sys.stdout
|
||||
else:
|
||||
_stdin = sys.stdin.buffer
|
||||
_stdout = sys.stdout.buffer
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('__name__')
|
||||
|
||||
from amodem import config
|
||||
from amodem import recv
|
||||
from amodem import send
|
||||
from amodem import wave
|
||||
from amodem import calib
|
||||
|
||||
null = open('/dev/null', 'wb')
|
||||
|
||||
|
||||
def FileType(mode, process=None):
|
||||
def opener(fname):
|
||||
assert 'r' in mode or 'w' in mode
|
||||
if process is None and fname is None:
|
||||
fname = '-'
|
||||
|
||||
if fname is None:
|
||||
if 'r' in mode:
|
||||
return process(stdout=wave.sp.PIPE, stderr=null).stdout
|
||||
if 'w' in mode:
|
||||
return process(stdin=wave.sp.PIPE, stderr=null).stdin
|
||||
|
||||
if fname == '-':
|
||||
if 'r' in mode:
|
||||
return _stdin
|
||||
if 'w' in mode:
|
||||
return _stdout
|
||||
|
||||
return open(fname, mode)
|
||||
|
||||
return opener
|
||||
|
||||
|
||||
def main():
|
||||
fmt = ('Audio OFDM MODEM: {:.1f} kb/s ({:d}-QAM x {:d} carriers) '
|
||||
'Fs={:.1f} kHz')
|
||||
description = fmt.format(config.modem_bps / 1e3, len(config.symbols),
|
||||
config.Nfreq, config.Fs / 1e3)
|
||||
p = argparse.ArgumentParser(description=description)
|
||||
g = p.add_mutually_exclusive_group()
|
||||
g.add_argument('-v', '--verbose', default=0, action='count')
|
||||
g.add_argument('-q', '--quiet', default=False, action='store_true')
|
||||
subparsers = p.add_subparsers()
|
||||
|
||||
# Modulator
|
||||
sender = subparsers.add_parser(
|
||||
'send', help='modulate binary data into audio signal.')
|
||||
sender.add_argument(
|
||||
'-i', '--input', help='input file (use "-" for stdin).')
|
||||
sender.add_argument(
|
||||
'-o', '--output', help='output file (use "-" for stdout).'
|
||||
' if not specified, `aplay` tool will be used.')
|
||||
sender.add_argument(
|
||||
'-c', '--calibrate', default=False, action='store_true')
|
||||
sender.add_argument(
|
||||
'-w', '--wave', default=False, action='store_true')
|
||||
sender.add_argument(
|
||||
'--silence-start', type=float, default=1.0,
|
||||
help='seconds of silence before transmission starts')
|
||||
sender.add_argument(
|
||||
'--silence-stop', type=float, default=1.0,
|
||||
help='seconds of silence after transmission stops')
|
||||
|
||||
sender.set_defaults(
|
||||
main=run_send,
|
||||
input_type=FileType('rb'),
|
||||
output_type=FileType('wb', wave.play)
|
||||
)
|
||||
|
||||
# Demodulator
|
||||
receiver = subparsers.add_parser(
|
||||
'recv', help='demodulate audio signal into binary data.')
|
||||
receiver.add_argument(
|
||||
'-i', '--input', help='input file (use "-" for stdin).'
|
||||
' if not specified, `arecord` tool will be used.')
|
||||
receiver.add_argument(
|
||||
'-o', '--output', help='output file (use "-" for stdout).')
|
||||
receiver.add_argument(
|
||||
'-c', '--calibrate', default=False, action='store_true')
|
||||
receiver.add_argument(
|
||||
'-w', '--wave', default=False, action='store_true')
|
||||
receiver.add_argument(
|
||||
'--skip', type=int, default=128,
|
||||
help='skip initial N samples, due to spurious spikes')
|
||||
receiver.add_argument(
|
||||
'--plot', action='store_true', default=False,
|
||||
help='plot results using pylab module')
|
||||
receiver.set_defaults(
|
||||
main=run_recv,
|
||||
input_type=FileType('rb', wave.record),
|
||||
output_type=FileType('wb')
|
||||
)
|
||||
|
||||
args = p.parse_args()
|
||||
if args.verbose == 0:
|
||||
level, format = 'INFO', '%(message)s'
|
||||
elif args.verbose == 1:
|
||||
level, format = 'DEBUG', '%(message)s'
|
||||
elif args.verbose >= 2:
|
||||
level, format = ('DEBUG', '%(asctime)s %(levelname)-10s '
|
||||
'%(message)-100s '
|
||||
'%(filename)s:%(lineno)d')
|
||||
if args.quiet:
|
||||
level, format = 'WARNING', '%(message)s'
|
||||
logging.basicConfig(level=level, format=format)
|
||||
|
||||
# Parsing and execution
|
||||
log.debug('MODEM settings: %r', config.settings)
|
||||
if getattr(args, 'plot', False):
|
||||
import pylab
|
||||
args.plot = pylab
|
||||
args.main(args)
|
||||
|
||||
|
||||
def join_process(process):
|
||||
exitcode = 0
|
||||
try:
|
||||
exitcode = process.wait()
|
||||
except KeyboardInterrupt:
|
||||
process.kill()
|
||||
exitcode = process.wait()
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
def run_modem(args, func):
|
||||
args.input = args.input_type(args.input)
|
||||
args.output = args.output_type(args.output)
|
||||
func(args)
|
||||
|
||||
|
||||
def run_send(args):
|
||||
if args.calibrate:
|
||||
calib.send()
|
||||
elif args.wave:
|
||||
join_process(wave.play(fname=args.input))
|
||||
else:
|
||||
run_modem(args, send.main)
|
||||
|
||||
|
||||
def run_recv(args):
|
||||
if args.calibrate:
|
||||
calib.recv()
|
||||
elif args.wave:
|
||||
join_process(wave.record(fname=args.output))
|
||||
else:
|
||||
run_modem(args, recv.main)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -14,13 +14,14 @@ def spectrogram(t, x, Fs, NFFT=256):
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from amodem import common
|
||||
from amodem.config import Fs, Ts
|
||||
from amodem.config import Configuration
|
||||
config = Configuration()
|
||||
|
||||
for fname in sys.argv[1:]:
|
||||
x = common.load(open(fname, 'rb'))
|
||||
t = np.arange(len(x)) * Ts
|
||||
t = np.arange(len(x)) * config.Ts
|
||||
pylab.figure()
|
||||
pylab.title(fname)
|
||||
spectrogram(t, x, Fs)
|
||||
spectrogram(t, x, config.Fs)
|
||||
|
||||
pylab.show()
|
||||
21
setup.py
21
setup.py
@@ -1,14 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from setuptools import setup
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
pwd = os.path.dirname(__file__)
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
@@ -19,12 +10,13 @@ class PyTest(TestCommand):
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
import sys
|
||||
import pytest
|
||||
sys.exit(pytest.main(['tests']))
|
||||
|
||||
setup(
|
||||
name="amodem",
|
||||
version="1.2",
|
||||
version="1.6",
|
||||
description="Audio Modem Communication Library",
|
||||
author="Roman Zeyde",
|
||||
author_email="roman.zeyde@gmail.com",
|
||||
@@ -33,7 +25,7 @@ setup(
|
||||
packages=['amodem'],
|
||||
tests_require=['pytest'],
|
||||
cmdclass={'test': PyTest},
|
||||
install_requires=['numpy', 'bitarray', 'reedsolo'],
|
||||
install_requires=['numpy'],
|
||||
platforms=['POSIX'],
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
@@ -41,11 +33,14 @@ setup(
|
||||
"Intended Audience :: Information Technology",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3.2",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: System :: Networking",
|
||||
"Topic :: Communications",
|
||||
],
|
||||
scripts=['scripts/amodem'],
|
||||
scripts=['amodem-cli'],
|
||||
)
|
||||
|
||||
35
tests/test_audio.py
Normal file
35
tests/test_audio.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from amodem import audio, config
|
||||
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
|
||||
def test():
|
||||
length = 1024
|
||||
data = b'\x12\x34' * length
|
||||
with mock.patch('ctypes.CDLL') as cdll:
|
||||
lib = mock.Mock()
|
||||
lib.Pa_GetErrorText = lambda code: 'Error' if code else 'Success'
|
||||
lib.Pa_GetDefaultOutputDevice.return_value = 1
|
||||
lib.Pa_GetDefaultInputDevice.return_value = 2
|
||||
lib.Pa_OpenStream.return_value = 0
|
||||
cdll.return_value = lib
|
||||
interface = audio.Interface(
|
||||
name='portaudio', config=config.fastest(), debug=True
|
||||
)
|
||||
with interface:
|
||||
s = interface.player()
|
||||
assert s.params.device == 1
|
||||
s.stream = 1 # simulate non-zero output stream handle
|
||||
s.write(data=data)
|
||||
s.close()
|
||||
|
||||
with interface:
|
||||
s = interface.recorder()
|
||||
assert s.params.device == 2
|
||||
s.stream = 2 # simulate non-zero input stream handle
|
||||
s.read(len(data))
|
||||
s.close()
|
||||
|
||||
with pytest.raises(Exception):
|
||||
interface._error_check(1)
|
||||
@@ -1,17 +1,26 @@
|
||||
from amodem import calib
|
||||
from amodem import common
|
||||
from amodem import config
|
||||
config = config.fastest()
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
class ProcessMock(object):
|
||||
def __init__(self):
|
||||
self.buf = BytesIO()
|
||||
self.stdin = self
|
||||
self.stdout = self
|
||||
self.bytes_per_sample = 2
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
def launch(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
__call__ = launch
|
||||
|
||||
def kill(self):
|
||||
pass
|
||||
|
||||
@@ -26,22 +35,46 @@ class ProcessMock(object):
|
||||
|
||||
def test_success():
|
||||
p = ProcessMock()
|
||||
calib.send(p)
|
||||
calib.send(config, p)
|
||||
p.buf.seek(0)
|
||||
calib.recv(p)
|
||||
calib.recv(config, p)
|
||||
|
||||
|
||||
def test_errors():
|
||||
p = ProcessMock()
|
||||
|
||||
def _write(data):
|
||||
raise IOError()
|
||||
p.write = _write
|
||||
calib.send(p)
|
||||
class WriteError(ProcessMock):
|
||||
def write(self, data):
|
||||
raise IOError()
|
||||
p = WriteError()
|
||||
calib.send(config, p)
|
||||
assert p.buf.tell() == 0
|
||||
|
||||
def _read(data):
|
||||
raise KeyboardInterrupt()
|
||||
p.read = _read
|
||||
calib.recv(p)
|
||||
class ReadError(ProcessMock):
|
||||
def read(self, n):
|
||||
raise KeyboardInterrupt()
|
||||
p = ReadError()
|
||||
calib.recv(config, p, verbose=True)
|
||||
assert p.buf.tell() == 0
|
||||
|
||||
|
||||
@pytest.fixture(params=[0] + [sign * mag for sign in (+1, -1)
|
||||
for mag in (0.1, 1, 10, 100, 1e3, 2e3)])
|
||||
def freq_err(request):
|
||||
return request.param * 1e-6
|
||||
|
||||
|
||||
def test_drift(freq_err):
|
||||
freq = config.Fc * (1 + freq_err / 1e6)
|
||||
t = np.arange(int(1.0 * config.Fs)) * config.Ts
|
||||
frame_length = 100
|
||||
rms = 0.5
|
||||
signal = rms * np.cos(2 * np.pi * freq * t)
|
||||
src = BytesIO(common.dumps(signal))
|
||||
iters = 0
|
||||
for r in calib.detector(config, src, frame_length=frame_length):
|
||||
assert not r.error
|
||||
assert abs(r.rms - rms) < 1e-3
|
||||
assert abs(r.total - rms) < 1e-3
|
||||
iters += 1
|
||||
|
||||
assert iters > 0
|
||||
assert iters == config.baud / frame_length
|
||||
|
||||
@@ -6,7 +6,7 @@ def iterlist(x, *args, **kwargs):
|
||||
x = np.array(x)
|
||||
return list(
|
||||
(i, list(x))
|
||||
for i, x in common.iterate(x, enumerate=True, *args, **kwargs)
|
||||
for i, x in common.iterate(x, index=True, *args, **kwargs)
|
||||
)
|
||||
|
||||
|
||||
@@ -47,15 +47,15 @@ def test_dumps_loads():
|
||||
assert all(x == y)
|
||||
|
||||
|
||||
def test_saturation():
|
||||
x = np.array([1, -1, 1, -1]) * 1e10
|
||||
try:
|
||||
common.check_saturation(x)
|
||||
assert False
|
||||
except common.SaturationError as e:
|
||||
assert e.args == (max(x),)
|
||||
|
||||
def test_izip():
|
||||
x = range(10)
|
||||
y = range(-10, 0)
|
||||
assert list(common.izip([x, y])) == list(zip(x, y))
|
||||
|
||||
|
||||
def test_holder():
|
||||
d = {'x': 1, 'y': 2.3}
|
||||
a = common.AttributeHolder(d)
|
||||
assert a.x == d['x']
|
||||
assert a.y == d['y']
|
||||
assert repr(a) == 'AttributeHolder(x=1, y=2.3)'
|
||||
|
||||
61
tests/test_detect.py
Normal file
61
tests/test_detect.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from amodem import dsp
|
||||
from amodem import recv
|
||||
from amodem import detect
|
||||
from amodem import equalizer
|
||||
from amodem import sampling
|
||||
from amodem import config
|
||||
from amodem import common
|
||||
config = config.fastest()
|
||||
|
||||
|
||||
def test_detect():
|
||||
P = sum(equalizer.prefix)
|
||||
t = np.arange(P * config.Nsym) * config.Ts
|
||||
x = np.cos(2 * np.pi * config.Fc * t)
|
||||
|
||||
detector = detect.Detector(config, pylab=common.Dummy())
|
||||
samples, amp, freq_err = detector.run(x)
|
||||
assert abs(1 - amp) < 1e-12
|
||||
assert abs(freq_err) < 1e-16
|
||||
|
||||
x = np.cos(2 * np.pi * (2*config.Fc) * t)
|
||||
with pytest.raises(ValueError):
|
||||
detector.run(x)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
detector.max_offset = 0
|
||||
detector.run(x)
|
||||
|
||||
|
||||
def test_prefix():
|
||||
omega = 2 * np.pi * config.Fc / config.Fs
|
||||
symbol = np.cos(omega * np.arange(config.Nsym))
|
||||
signal = np.concatenate([c * symbol for c in equalizer.prefix])
|
||||
|
||||
def symbols_stream(signal):
|
||||
sampler = sampling.Sampler(signal)
|
||||
return dsp.Demux(sampler=sampler, omegas=[omega], Nsym=config.Nsym)
|
||||
r = recv.Receiver(config, pylab=common.Dummy())
|
||||
r._prefix(symbols_stream(signal))
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
silence = 0 * signal
|
||||
r._prefix(symbols_stream(silence))
|
||||
|
||||
|
||||
def test_find_start():
|
||||
sym = np.cos(2 * np.pi * config.Fc * np.arange(config.Nsym) * config.Ts)
|
||||
detector = detect.Detector(config, pylab=common.Dummy())
|
||||
|
||||
length = 200
|
||||
prefix = postfix = np.tile(0 * sym, 50)
|
||||
carrier = np.tile(sym, length)
|
||||
for offset in range(32):
|
||||
bufs = [prefix, [0] * offset, carrier, postfix]
|
||||
buf = np.concatenate(bufs)
|
||||
start = detector.find_start(buf, length)
|
||||
expected = offset + len(prefix)
|
||||
assert expected == start
|
||||
@@ -2,8 +2,9 @@ import numpy as np
|
||||
from numpy.linalg import norm
|
||||
|
||||
from amodem import dsp
|
||||
from amodem import config
|
||||
from amodem import sampling
|
||||
from amodem import config
|
||||
config = config.fastest()
|
||||
|
||||
import random
|
||||
import itertools
|
||||
@@ -28,42 +29,13 @@ def test_filter():
|
||||
assert list(y) == [0.5 ** (i+1) for i in range(len(x))]
|
||||
|
||||
|
||||
def test_estimate():
|
||||
r = np.random.RandomState(seed=0)
|
||||
x = r.uniform(-1, 1, [1000])
|
||||
x[:10] = 0
|
||||
x[len(x)-10:] = 0
|
||||
|
||||
c = 1.23
|
||||
y = c * x
|
||||
c_, = dsp.estimate(x=x, y=y, order=1)
|
||||
assert abs(c - c_) < 1e-12
|
||||
|
||||
h = [1, 1]
|
||||
y = dsp.lfilter(b=h, a=[1], x=x)
|
||||
h_ = dsp.estimate(x=x, y=y, order=len(h))
|
||||
assert norm(h - h_) < 1e-12
|
||||
|
||||
h = [0.1, 0.6, 0.9, 0.7, -0.2]
|
||||
L = len(h) // 2
|
||||
|
||||
y = dsp.lfilter(b=h, a=[1], x=x)
|
||||
h_ = dsp.estimate(
|
||||
x=x[:len(x)-L], y=y[L:],
|
||||
order=len(h), lookahead=L
|
||||
)
|
||||
assert norm(h - h_) < 1e-12
|
||||
|
||||
y_ = dsp.lfilter(b=h_, a=[1], x=x)
|
||||
assert norm(y - y_) < 1e-12
|
||||
|
||||
|
||||
def test_demux():
|
||||
freqs = [1e3, 2e3]
|
||||
carriers = [dsp.exp_iwt(f, config.Nsym) for f in freqs]
|
||||
freqs = np.array([1e3, 2e3])
|
||||
omegas = 2 * np.pi * freqs / config.Fs
|
||||
carriers = [dsp.exp_iwt(2*np.pi*f/config.Fs, config.Nsym) for f in freqs]
|
||||
syms = [3, 2j]
|
||||
sig = np.dot(syms, carriers)
|
||||
res = dsp.Demux(sampling.Sampler(sig.real), freqs)
|
||||
res = dsp.Demux(sampling.Sampler(sig.real), omegas, config.Nsym)
|
||||
res = np.array(list(res))
|
||||
assert np.max(np.abs(res - syms)) < 1e-12
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ from numpy.random import RandomState
|
||||
import numpy as np
|
||||
|
||||
from amodem import dsp
|
||||
from amodem import config
|
||||
from amodem import equalizer
|
||||
from amodem import config
|
||||
config = config.fastest()
|
||||
|
||||
|
||||
def assert_approx(x, y, e=1e-12):
|
||||
@@ -13,8 +14,9 @@ def assert_approx(x, y, e=1e-12):
|
||||
|
||||
def test_training():
|
||||
L = 1000
|
||||
t1 = equalizer.train_symbols(L)
|
||||
t2 = equalizer.train_symbols(L)
|
||||
e = equalizer.Equalizer(config)
|
||||
t1 = e.train_symbols(L)
|
||||
t2 = e.train_symbols(L)
|
||||
assert (t1 == t2).all()
|
||||
|
||||
|
||||
@@ -35,37 +37,14 @@ def test_commutation():
|
||||
|
||||
def test_modem():
|
||||
L = 1000
|
||||
sent = equalizer.train_symbols(L)
|
||||
e = equalizer.Equalizer(config)
|
||||
sent = e.train_symbols(L)
|
||||
gain = config.Nfreq
|
||||
x = equalizer.modulator(sent) * gain
|
||||
received = equalizer.demodulator(x, L)
|
||||
x = e.modulator(sent) * gain
|
||||
received = e.demodulator(x, L)
|
||||
assert_approx(sent, received)
|
||||
|
||||
|
||||
def test_symbols():
|
||||
length = 100
|
||||
gain = float(config.Nfreq)
|
||||
|
||||
symbols = equalizer.train_symbols(length=length)
|
||||
x = equalizer.modulator(symbols) * gain
|
||||
assert_approx(equalizer.demodulator(x, size=length), symbols)
|
||||
|
||||
den = np.array([1, -0.6, 0.1])
|
||||
num = np.array([0.5])
|
||||
y = dsp.lfilter(x=x, b=num, a=den)
|
||||
|
||||
lookahead = 2
|
||||
h = equalizer.equalize_symbols(
|
||||
signal=y, symbols=symbols, order=len(den), lookahead=lookahead
|
||||
)
|
||||
assert norm(h[:lookahead]) < 1e-12
|
||||
assert_approx(h[lookahead:], den / num)
|
||||
|
||||
y = dsp.lfilter(x=y, b=h[lookahead:], a=[1])
|
||||
z = equalizer.demodulator(y, size=length)
|
||||
assert_approx(z, symbols)
|
||||
|
||||
|
||||
def test_signal():
|
||||
length = 100
|
||||
x = np.sign(RandomState(0).normal(size=length))
|
||||
@@ -74,7 +53,7 @@ def test_signal():
|
||||
y = dsp.lfilter(x=x, b=num, a=den)
|
||||
|
||||
lookahead = 2
|
||||
h = equalizer.equalize_signal(
|
||||
h = equalizer.train(
|
||||
signal=y, expected=x, order=len(den), lookahead=lookahead)
|
||||
assert norm(h[:lookahead]) < 1e-12
|
||||
|
||||
|
||||
@@ -34,16 +34,19 @@ def test_main(data):
|
||||
decoded = framing.decode(encoded)
|
||||
assert bytearray(decoded) == data
|
||||
|
||||
|
||||
def test_fail():
|
||||
encoded = list(framing.encode(''))
|
||||
encoded[-1] = not encoded[-1]
|
||||
with pytest.raises(ValueError):
|
||||
list(framing.decode(encoded))
|
||||
|
||||
|
||||
def test_missing():
|
||||
f = framing.Framer()
|
||||
with pytest.raises(ValueError):
|
||||
list(f.decode(b'\x00'))
|
||||
list(f.decode(b''))
|
||||
with pytest.raises(ValueError):
|
||||
list(f.decode(b'\x01\x02\x03\x04'))
|
||||
|
||||
list(f.decode(b'\x01'))
|
||||
with pytest.raises(ValueError):
|
||||
list(f.decode(b'\xff'))
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
from amodem import config
|
||||
from amodem import recv
|
||||
from amodem import train
|
||||
from amodem import sampling
|
||||
|
||||
|
||||
def test_detect():
|
||||
P = sum(train.prefix)
|
||||
t = np.arange(P * config.Nsym) * config.Ts
|
||||
x = np.cos(2 * np.pi * config.Fc * t)
|
||||
samples, amp = recv.detect(x, config.Fc)
|
||||
assert abs(1 - amp) < 1e-12
|
||||
|
||||
x = np.cos(2 * np.pi * (2*config.Fc) * t)
|
||||
try:
|
||||
recv.detect(x, config.Fc)
|
||||
assert False
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_prefix():
|
||||
symbol = np.cos(2 * np.pi * config.Fc * np.arange(config.Nsym) * config.Ts)
|
||||
signal = np.concatenate([c * symbol for c in train.prefix])
|
||||
|
||||
sampler = sampling.Sampler(signal)
|
||||
r = recv.Receiver()
|
||||
freq_err = r._prefix(sampler, freq=config.Fc)
|
||||
assert abs(freq_err) < 1e-16
|
||||
|
||||
try:
|
||||
silence = 0 * signal
|
||||
r._prefix(sampling.Sampler(silence), freq=config.Fc)
|
||||
assert False
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_find_start():
|
||||
sym = np.cos(2 * np.pi * config.Fc * np.arange(config.Nsym) * config.Ts)
|
||||
|
||||
length = 200
|
||||
prefix = postfix = np.tile(0 * sym, 50)
|
||||
carrier = np.tile(sym, length)
|
||||
for offset in range(10):
|
||||
prefix = [0] * offset
|
||||
bufs = [prefix, prefix, carrier, postfix]
|
||||
buf = np.concatenate(bufs)
|
||||
start = recv.find_start(buf, length*config.Nsym)
|
||||
expected = offset + len(prefix)
|
||||
assert expected == start
|
||||
@@ -8,6 +8,8 @@ from amodem import recv
|
||||
from amodem import common
|
||||
from amodem import dsp
|
||||
from amodem import sampling
|
||||
from amodem import config
|
||||
config = config.fastest()
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
@@ -27,8 +29,7 @@ class Args(object):
|
||||
def run(size, chan=None, df=0, success=True):
|
||||
tx_data = os.urandom(size)
|
||||
tx_audio = BytesIO()
|
||||
send.main(Args(silence_start=1, silence_stop=1,
|
||||
input=BytesIO(tx_data), output=tx_audio))
|
||||
send.main(config=config, src=BytesIO(tx_data), dst=tx_audio)
|
||||
|
||||
data = tx_audio.getvalue()
|
||||
data = common.loads(data)
|
||||
@@ -43,8 +44,11 @@ def run(size, chan=None, df=0, success=True):
|
||||
rx_audio = BytesIO(data)
|
||||
|
||||
rx_data = BytesIO()
|
||||
result = recv.main(Args(skip=0, input=rx_audio, output=rx_data))
|
||||
d = BytesIO()
|
||||
result = recv.main(config=config, src=rx_audio, dst=rx_data,
|
||||
dump_audio=d)
|
||||
rx_data = rx_data.getvalue()
|
||||
assert data.startswith(d.getvalue())
|
||||
|
||||
assert result == success
|
||||
if success:
|
||||
@@ -61,17 +65,18 @@ def test_small(small_size):
|
||||
|
||||
|
||||
def test_error():
|
||||
skip = 1 * send.config.Fs # remove trailing silence
|
||||
skip = 32000 # remove trailing silence
|
||||
run(1024, chan=lambda x: x[:-skip], success=False)
|
||||
|
||||
|
||||
@pytest.fixture(params=[s*x for s in (+1, -1) for x in (0.1, 1, 10)])
|
||||
@pytest.fixture(params=[sign * mag for sign in (+1, -1)
|
||||
for mag in (0.1, 1, 10, 100, 1e3, 2e3)])
|
||||
def freq_err(request):
|
||||
return request.param * 1e-6
|
||||
|
||||
|
||||
def test_timing(freq_err):
|
||||
run(1024, df=freq_err)
|
||||
run(8192, df=freq_err)
|
||||
|
||||
|
||||
def test_lowpass():
|
||||
@@ -1,27 +0,0 @@
|
||||
from amodem import wave
|
||||
import subprocess as sp
|
||||
import signal
|
||||
|
||||
|
||||
def test_launch():
|
||||
p = wave.launch(tool='true', fname='fname')
|
||||
assert p.wait() == 0
|
||||
|
||||
def test_exit():
|
||||
p = wave.launch(tool='python', fname='-', stdin=sp.PIPE)
|
||||
s = b'import sys; sys.exit(42)'
|
||||
p.stdin.write(s)
|
||||
p.stdin.close()
|
||||
assert p.wait() == 42
|
||||
|
||||
def test_io():
|
||||
p = wave.launch(tool='python', fname='-', stdin=sp.PIPE, stdout=sp.PIPE)
|
||||
s = b'Hello World!'
|
||||
p.stdin.write(b'print("' + s + b'")\n')
|
||||
p.stdin.close()
|
||||
assert p.stdout.read(len(s)) == s
|
||||
|
||||
def test_kill():
|
||||
p = wave.launch(tool='python', fname='-', stdin=sp.PIPE, stdout=sp.PIPE)
|
||||
p.kill()
|
||||
assert p.wait() == -signal.SIGKILL
|
||||
15
tox.ini
15
tox.ini
@@ -1,5 +1,14 @@
|
||||
[tox]
|
||||
envlist = py27,py33,py34
|
||||
envlist = py27,py34
|
||||
[testenv]
|
||||
deps=pytest
|
||||
commands=py.test tests/
|
||||
deps=
|
||||
pytest
|
||||
mock
|
||||
pep8
|
||||
coverage
|
||||
pylint
|
||||
commands=
|
||||
pep8 amodem/ scripts/ tests/ amodem-cli
|
||||
pylint --disable no-member --report=no amodem
|
||||
coverage run --source amodem/ -m py.test tests/
|
||||
coverage report
|
||||
|
||||
Reference in New Issue
Block a user