diff --git a/.travis.yml b/.travis.yml index 0934039..d83083a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - "2.7" + - "3.3" install: - pip install . diff --git a/amodem/calib.py b/amodem/calib.py index 53446b7..06197f7 100755 --- a/amodem/calib.py +++ b/amodem/calib.py @@ -1,9 +1,10 @@ #!/usr/bin/env python import numpy as np -import common -import config -import sigproc -import wave + +from . import common +from . import config +from . import sigproc +from . import wave Tsample = 1 t = np.arange(int(Tsample * config.Fs)) * config.Ts @@ -22,6 +23,7 @@ def send(): except KeyboardInterrupt: p.kill() + def recv(): p = wave.record('-', stdout=wave.sp.PIPE) try: @@ -36,13 +38,15 @@ def recv(): continue x = x - np.mean(x) - c = np.abs(np.dot(x, sig)) / (np.sqrt(0.5 * len(x)) * sigproc.norm(x)) + normalization_factor = np.sqrt(0.5 * len(x)) * sigproc.norm(x) + coherence = np.abs(np.dot(x, sig)) / normalization_factor z = np.dot(x, sig.conj()) / (0.5 * len(x)) - amp = np.abs(z) + amplitude = np.abs(z) phase = np.angle(z) peak = np.max(np.abs(x)) - print('coherence={:.3f} amp={:.3f} phase={:.1f} peak={:.3f}'.format( - c, amp, phase * 180 / np.pi, peak)) + + fmt = 'coherence={:.3f} amplitude={:.3f} phase={:+.1f} peak={:.3f}' + print(fmt.format(coherence, amplitude, phase * 180 / np.pi, peak)) except KeyboardInterrupt: p.kill() diff --git a/amodem/common.py b/amodem/common.py index da9344c..cf9e078 100644 --- a/amodem/common.py +++ b/amodem/common.py @@ -41,7 +41,7 @@ def load(fileobj): def loads(data): - x = np.fromstring(data, dtype='int16') + x = np.frombuffer(data, dtype='int16') x = x / scaling return x @@ -83,7 +83,7 @@ class Splitter(object): while True: if all(self.read): try: - self.last = self.iterable.next() + self.last = next(self.iterable) except StopIteration: return @@ -108,3 +108,9 @@ def icapture(iterable, result): def take(iterable, n): return np.array(list(itertools.islice(iterable, n))) + + +try: + izip = itertools.izip +except AttributeError: + izip = zip # Python 3 diff --git a/amodem/ecc.py b/amodem/ecc.py index 1da4f32..f69445c 100644 --- a/amodem/ecc.py +++ b/amodem/ecc.py @@ -1,7 +1,7 @@ ''' Reed-Solomon CODEC. ''' from reedsolo import rs_encode_msg, rs_correct_msg -import common +from . import common import logging log = logging.getLogger(__name__) @@ -17,7 +17,8 @@ def end_of_stream(size): def encode(data, nsym=DEFAULT_NSYM): chunk_size = BLOCK_SIZE - nsym - 1 - for _, chunk in common.iterate(data, chunk_size, bytearray, truncate=False): + for _, chunk in common.iterate(data=data, size=chunk_size, + func=bytearray, truncate=False): size = len(chunk) if size < chunk_size: padding = [0] * (chunk_size - size) diff --git a/amodem/loop.py b/amodem/loop.py index a1a409e..beef6cb 100644 --- a/amodem/loop.py +++ b/amodem/loop.py @@ -1,8 +1,9 @@ import numpy as np import itertools -import sampling -import sigproc +from . import sampling +from . import sigproc +from . import common class Filter(object): @@ -33,4 +34,4 @@ class FreqLoop(object): self.gens.append(gen) def __iter__(self): - return itertools.izip(*self.gens) + return common.izip(*self.gens) diff --git a/amodem/recv.py b/amodem/recv.py index 08866fa..d47fcba 100755 --- a/amodem/recv.py +++ b/amodem/recv.py @@ -8,21 +8,24 @@ import time import sys import os +import bitarray + log = logging.getLogger(__name__) -import stream -import sigproc -import loop -import train -import common -import config +from . import stream +from . import sigproc +from . import loop +from . import train +from . import common +from . import config +from . import ecc modem = sigproc.MODEM(config) if os.environ.get('PYLAB') == '1': - import pylab - import show + from . import pylab + from . import show WIDTH = np.floor(np.sqrt(len(modem.freqs))) HEIGHT = np.ceil(len(modem.freqs) / float(WIDTH)) else: @@ -190,7 +193,7 @@ def demodulate(symbols, filters, freqs, sampler): stats['rx_start'] = time.time() log.info('Demodulation started') - for i, block in enumerate(itertools.izip(*streams)): # block per frequency + for i, block in enumerate(common.izip(*streams)): # block per frequency for bits in block: stats['rx_bits'] = stats['rx_bits'] + len(bits) yield bits @@ -223,9 +226,6 @@ def receive(signal, freqs, gain=1.0): def decode(bits_iterator): - import bitarray - import ecc - def blocks(): while True: bits = itertools.islice(bits_iterator, 8 * ecc.BLOCK_SIZE) @@ -238,11 +238,16 @@ def decode(bits_iterator): return ecc.decode(blocks()) +def iread(fd): + reader = stream.Reader(fd, data_type=common.loads) + return itertools.chain.from_iterable(reader) + + def main(args): log.info('Running MODEM @ {:.1f} kbps'.format(modem.modem_bps / 1e3)) - signal = stream.iread(args.input) + signal = iread(args.input) skipped = common.take(signal, args.skip) log.debug('Skipping first %.3f seconds', len(skipped) / float(modem.baud)) @@ -282,8 +287,10 @@ if __name__ == '__main__': p = argparse.ArgumentParser() p.add_argument('--skip', type=int, default=100, help='skip initial N samples, due to spurious spikes') - p.add_argument('-i', '--input', type=argparse.FileType('r'), default=sys.stdin) - p.add_argument('-o', '--output', type=argparse.FileType('w'), default=sys.stdout) + p.add_argument('-i', '--input', type=argparse.FileType('rb'), + default=sys.stdin) + p.add_argument('-o', '--output', type=argparse.FileType('wb'), + default=sys.stdout) args = p.parse_args() try: main(args) diff --git a/amodem/sampling.py b/amodem/sampling.py index 56a68d3..e55221d 100755 --- a/amodem/sampling.py +++ b/amodem/sampling.py @@ -49,6 +49,8 @@ class Sampler(object): def next(self): return self._sample() * self.gain + __next__ = next + def _sample(self): offset = self.offset # offset = k + (j / self.resolution) @@ -58,7 +60,7 @@ class Sampler(object): end = k + self.width while self.index < end: self.buff[:-1] = self.buff[1:] - self.buff[-1] = self.src.next() # throws StopIteration + self.buff[-1] = next(self.src) # throws StopIteration self.index += 1 self.offset += self.freq diff --git a/amodem/send.py b/amodem/send.py index 6322224..fe9e7bf 100755 --- a/amodem/send.py +++ b/amodem/send.py @@ -7,19 +7,22 @@ import time log = logging.getLogger(__name__) -import train -import wave +from . import train +from . import wave -import common -import config -import sigproc +from . import common +from . import config +from . import sigproc +from . import stream +from . import ecc modem = sigproc.MODEM(config) class Symbol(object): - t = np.arange(0, config.Nsym) * config.Ts - carrier = [np.exp(2j * np.pi * F * t) for F in modem.freqs] + def __init__(self): + t = np.arange(0, config.Nsym) * config.Ts + self.carrier = [np.exp(2j * np.pi * F * t) for F in modem.freqs] sym = Symbol() @@ -63,26 +66,7 @@ def modulate(fd, bits): break -class Reader(object): - def __init__(self, fd, size): - self.fd = fd - self.size = size - self.total = 0 - - def next(self): - block = self.fd.read(self.size) - if block: - self.total += len(block) - return block - else: - raise StopIteration() - - def __iter__(self): - return self - - def main(args): - import ecc log.info('Running MODEM @ {:.1f} kbps'.format(modem.modem_bps / 1e3)) # padding audio with silence @@ -95,7 +79,7 @@ def main(args): log.info('%.3f seconds of training audio', training_size / wave.bytes_per_second) - reader = Reader(args.input, 64 << 10) + reader = stream.Reader(args.input, bufsize=(64 << 10), eof=True) data = itertools.chain.from_iterable(reader) encoded = itertools.chain.from_iterable(ecc.encode(data)) modulate(args.output, bits=common.to_bits(encoded)) @@ -115,7 +99,9 @@ if __name__ == '__main__': p = argparse.ArgumentParser() p.add_argument('--silence-start', type=float, default=1.0) p.add_argument('--silence-stop', type=float, default=1.0) - p.add_argument('-i', '--input', type=argparse.FileType('r'), default=sys.stdin) - p.add_argument('-o', '--output', type=argparse.FileType('w'), default=sys.stdout) + p.add_argument('-i', '--input', type=argparse.FileType('rb'), + default=sys.stdin) + p.add_argument('-o', '--output', type=argparse.FileType('wb'), + default=sys.stdout) args = p.parse_args() main(args) diff --git a/amodem/sigproc.py b/amodem/sigproc.py index 270c1fb..91b8f33 100644 --- a/amodem/sigproc.py +++ b/amodem/sigproc.py @@ -1,8 +1,8 @@ import numpy as np from numpy import linalg -import common -from config import Ts, Nsym +from . import common +from .config import Ts, Nsym class Filter(object): diff --git a/amodem/stream.py b/amodem/stream.py index fc59b93..25844ff 100644 --- a/amodem/stream.py +++ b/amodem/stream.py @@ -1,43 +1,57 @@ import time -import itertools +import logging -import common -import wave +log = logging.getLogger(__name__) + + +class Timeout(Exception): + pass class Reader(object): - SAMPLES = 4096 - BUFSIZE = int(SAMPLES * wave.bytes_per_sample) - WAIT = 0.1 - TIMEOUT = 2.0 - - def __init__(self, fd): + def __init__(self, fd, data_type=None, bufsize=4096, + eof=False, timeout=2.0, wait=0.2): 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 + def __next__(self): + return self.next() + def next(self): block = bytearray() - finish_time = time.time() + self.TIMEOUT + if self.eof: + data = self.fd.read(self.bufsize) + if data: + self.total += len(data) + block.extend(data) + return block + else: + raise StopIteration() + + finish_time = time.time() + self.timeout while time.time() <= finish_time: - left = self.BUFSIZE - len(block) + left = self.bufsize - len(block) data = self.fd.read(left) if data: + self.total += len(data) block.extend(data) - if len(block) == self.BUFSIZE: - values = common.loads(str(block)) + if len(block) == self.bufsize: + values = self.data_type(block) if self.check: self.check(values) return values - time.sleep(self.WAIT) + time.sleep(self.wait) - raise IOError('timeout') - - -def iread(fd): - return itertools.chain.from_iterable(Reader(fd)) + raise Timeout(self.timeout) diff --git a/amodem/wave.py b/amodem/wave.py index 57e47ab..269c66f 100755 --- a/amodem/wave.py +++ b/amodem/wave.py @@ -6,7 +6,7 @@ import logging log = logging.getLogger(__name__) -import config +from . import config Fs = int(config.Fs) # sampling rate bits_per_sample = 16 @@ -27,7 +27,7 @@ def record(fname, **kwargs): def launch(*args, **kwargs): - args = map(str, args) + args = list(map(str, args)) log.debug('$ %s', ' '.join(args)) p = sp.Popen(args=args, **kwargs) p.stop = lambda: os.kill(p.pid, signal.SIGKILL) diff --git a/amodem/scripts/auto-calib.sh b/scripts/auto-calib.sh similarity index 56% rename from amodem/scripts/auto-calib.sh rename to scripts/auto-calib.sh index af47cc2..d97687d 100755 --- a/amodem/scripts/auto-calib.sh +++ b/scripts/auto-calib.sh @@ -1,7 +1,7 @@ #!/bin/bash killall -q aplay arecord -./calib.py send & +python -m amodem.calib send & SENDER_PID=$! -./calib.py recv +python -m amodem.calib recv kill -INT $SENDER_PID diff --git a/amodem/scripts/profile.sh b/scripts/profile.sh similarity index 100% rename from amodem/scripts/profile.sh rename to scripts/profile.sh diff --git a/amodem/scripts/test.sh b/scripts/test.sh similarity index 78% rename from amodem/scripts/test.sh rename to scripts/test.sh index f97bb2b..0e9b640 100755 --- a/amodem/scripts/test.sh +++ b/scripts/test.sh @@ -32,20 +32,20 @@ run_src dd if=/dev/urandom of=$TEST_DIR/data.send bs=125kB count=1 status=none SRC_HASH=`run_src sha256sum $TEST_DIR/data.send` # modulate data into audio file -run_src "./send.py <$TEST_DIR/data.send >$TEST_DIR/audio.send" +run_src "python -m amodem.send -i $TEST_DIR/data.send -o $TEST_DIR/audio.send" # stop old recording and start a new one run_src killall -q aplay || true run_dst killall -q arecord || true -run_dst "./wave.py record $TEST_DIR/audio.recv" & +run_dst "python -m amodem.wave record $TEST_DIR/audio.recv" & sleep 1 # let audio.recv be filled # play the modulated data -run_src ./wave.py play $TEST_DIR/audio.send & +run_src "python -m amodem.wave play $TEST_DIR/audio.send" & # start the receiever -run_dst "./recv.py <$TEST_DIR/audio.recv >$TEST_DIR/data.recv" +run_dst "python -m amodem.recv -i $TEST_DIR/audio.recv -o $TEST_DIR/data.recv" # stop recording after playing is over run_src killall -q aplay || true diff --git a/tests/test_common.py b/tests/test_common.py index c5e32aa..7858fec 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -25,13 +25,13 @@ def test_iterate(): def test_split(): L = [(i*2, i*2+1) for i in range(10)] iters = common.split(L, n=2) - assert zip(*iters) == L + assert list(zip(*iters)) == L for i in [0, 1]: iters = common.split(L, n=2) - iters[i].next() + next(iters[i]) try: - iters[i].next() + next(iters[i]) assert False except IndexError as e: assert e.args == (i,) @@ -43,8 +43,8 @@ def test_icapture(): z = [] for i in common.icapture(x, result=y): z.append(i) - assert x == y - assert x == z + assert list(x) == y + assert list(x) == z def test_dumps_loads(): diff --git a/tests/test_full.py b/tests/test_full.py index 564e8af..3c1a533 100644 --- a/tests/test_full.py +++ b/tests/test_full.py @@ -1,5 +1,9 @@ import os -from cStringIO import StringIO +try: + from cStringIO import StringIO as BytesIO +except ImportError: + from io import BytesIO # Python 3 + import numpy as np from amodem import send @@ -10,40 +14,48 @@ import logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)-12s %(message)s') + class Args(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) + def run(size, chan): tx_data = os.urandom(size) - tx_audio = StringIO() - send.main(Args(silence_start=1, silence_stop=1, input=StringIO(tx_data), output=tx_audio)) + tx_audio = BytesIO() + send.main(Args(silence_start=1, silence_stop=1, + input=BytesIO(tx_data), output=tx_audio)) data = tx_audio.getvalue() data = common.loads(data) data = chan(data) data = common.dumps(data * 1j) - rx_audio = StringIO(data) + rx_audio = BytesIO(data) - rx_data = StringIO() + rx_data = BytesIO() recv.main(Args(skip=100, input=rx_audio, output=rx_data)) rx_data = rx_data.getvalue() assert rx_data == tx_data + def test_small(): run(1024, lambda x: x) + def test_large(): run(54321, lambda x: x) + def test_attenuation(): run(5120, lambda x: x * 0.1) + def test_low_noise(): r = np.random.RandomState(seed=0) run(5120, lambda x: x + r.normal(size=len(x), scale=0.0001)) + def test_medium_noise(): r = np.random.RandomState(seed=0) run(5120, lambda x: x + r.normal(size=len(x), scale=0.001)) diff --git a/tests/test_stream.py b/tests/test_stream.py index 5dbbcf4..8805bd1 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -1,14 +1,14 @@ from amodem import stream import subprocess as sp -script = r""" +script = br""" import sys import time import os while True: time.sleep(0.1) - sys.stdout.write(b'\x00' * 6400) + sys.stdout.write('\x00' * 6400) sys.stderr.write('.') """ @@ -19,17 +19,17 @@ def test_read(): p.stdin.close() f = stream.Reader(p.stdout) - result = zip(range(10), f) + result = list(zip(range(10), f)) p.kill() j = 0 for i, buf in result: assert i == j - assert len(buf) == f.SAMPLES + assert len(buf) == f.bufsize j += 1 try: for buf in f: pass - except IOError as e: - assert str(e) == 'timeout' + except stream.Timeout as e: + assert e.args == (f.timeout,)