From ceb826728a312023dc93f87cbb9a4eb271cfe060 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 9 Nov 2014 12:14:54 +0200 Subject: [PATCH] don't use global configuration --- amodem-cli | 20 +-- amodem/audio.py | 30 ++++ amodem/calib.py | 30 ++-- amodem/common.py | 6 +- amodem/dsp.py | 26 ++-- amodem/equalizer.py | 119 ++++++++------- amodem/recv.py | 200 ++++++++++++++------------ amodem/send.py | 78 +++++----- amodem/wave.py | 27 ---- tests/{test_wave.py => test_audio.py} | 12 +- tests/test_calib.py | 14 +- tests/test_dsp.py | 7 +- tests/test_equalizer.py | 26 ++-- tests/test_recv.py | 23 +-- tests/{test_full.py => test_whole.py} | 8 +- 15 files changed, 326 insertions(+), 300 deletions(-) create mode 100644 amodem/audio.py delete mode 100644 amodem/wave.py rename tests/{test_wave.py => test_audio.py} (52%) rename tests/{test_full.py => test_whole.py} (89%) diff --git a/amodem-cli b/amodem-cli index 5ffb541..8a9670c 100755 --- a/amodem-cli +++ b/amodem-cli @@ -19,7 +19,7 @@ log = logging.getLogger('__name__') from amodem import config from amodem import recv from amodem import send -from amodem import wave +from amodem import audio from amodem import calib null = open('/dev/null', 'wb') @@ -32,10 +32,11 @@ def FileType(mode, process=None): fname = '-' if fname is None: + assert process is not None if 'r' in mode: - return process(stdout=wave.sp.PIPE, stderr=null).stdout + return process.launch(stdout=audio.sp.PIPE, stderr=null).stdout if 'w' in mode: - return process(stdin=wave.sp.PIPE, stderr=null).stdin + return process.launch(stdin=audio.sp.PIPE, stderr=null).stdin if fname == '-': if 'r' in mode: @@ -81,7 +82,7 @@ def main(): sender.set_defaults( main=run_send, input_type=FileType('rb'), - output_type=FileType('wb', wave.play) + output_type=FileType('wb', audio.play(Fs=config.Fs)) ) # Demodulator @@ -104,7 +105,7 @@ def main(): help='plot results using pylab module') receiver.set_defaults( main=run_recv, - input_type=FileType('rb', wave.record), + input_type=FileType('rb', audio.record(Fs=config.Fs)), output_type=FileType('wb') ) @@ -127,6 +128,7 @@ def main(): if getattr(args, 'plot', False): import pylab args.plot = pylab + args.config = config args.main(args) @@ -148,18 +150,18 @@ def run_modem(args, func): def run_send(args): if args.calibrate: - calib.send(verbose=args.verbose) + calib.send(config=config, verbose=args.verbose) elif args.wave: - join_process(wave.play(fname=args.input)) + join_process(audio.play(Fs=config.Fs).launch(fname=args.input)) else: run_modem(args, send.main) def run_recv(args): if args.calibrate: - calib.recv(verbose=args.verbose) + calib.recv(config=config, verbose=args.verbose) elif args.wave: - join_process(wave.record(fname=args.output)) + join_process(audio.record(Fs=config.Fs).launch(fname=args.output)) else: run_modem(args, recv.main) diff --git a/amodem/audio.py b/amodem/audio.py new file mode 100644 index 0000000..8b16fcb --- /dev/null +++ b/amodem/audio.py @@ -0,0 +1,30 @@ +import subprocess as sp +import logging +import functools + +log = logging.getLogger(__name__) + +class ALSA(object): + def __init__(self, tool, Fs): + self.Fs = int(Fs) # sampling rate + self.bits_per_sample = 16 + self.bytes_per_sample = self.bits_per_sample / 8.0 + self.bytes_per_second = self.bytes_per_sample * self.Fs + # PCM signed little endian + self.audio_format = 'S{}_LE'.format(self.bits_per_sample) + self.audio_tool = tool + + def launch(self, fname=None, **kwargs): + if fname is None: + fname = '-' # use stdin/stdout if filename not specified + args = [self.audio_tool, fname, '-q', + '-f', self.audio_format, + '-c', '1', + '-r', str(self.Fs)] + log.debug('Running: %r', ' '.join(args)) + p = sp.Popen(args=args, **kwargs) + return p + +# Use ALSA tools for audio playing/recording +play = functools.partial(ALSA, tool='aplay') +record = functools.partial(ALSA, tool='arecord') diff --git a/amodem/calib.py b/amodem/calib.py index 8bfca2c..0290216 100644 --- a/amodem/calib.py +++ b/amodem/calib.py @@ -3,20 +3,21 @@ import logging log = logging.getLogger(__name__) +from subprocess import PIPE from . import common -from . import config -from . import wave +from . import audio -CALIBRATION_SYMBOLS = int(1.0 * config.Fs) ALLOWED_EXCEPTIONS = (IOError, KeyboardInterrupt) -def send(wave_play=wave.play, verbose=False): - t = np.arange(0, CALIBRATION_SYMBOLS) * config.Ts +def send(config, audio_play=audio.play, verbose=False): + calibration_symbols = int(1.0 * config.Fs) + 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) + player = audio_play(Fs=config.Fs) + p = player.launch(stdin=PIPE) fd = p.stdin try: while True: @@ -27,16 +28,17 @@ def send(wave_play=wave.play, verbose=False): p.kill() -FRAME_LENGTH = 200 * config.Nsym +def run_recorder(config, recorder): + FRAME_LENGTH = 200 * config.Nsym + process = recorder.launch(stdout=PIPE) + frame_size = int(recorder.bytes_per_sample * FRAME_LENGTH) -def recorder(process): t = np.arange(0, FRAME_LENGTH) * config.Ts scaling_factor = 0.5 * len(t) carriers = [np.exp(2j * np.pi * f * t) for f in config.frequencies] carriers = np.array(carriers) / scaling_factor - frame_size = int(wave.bytes_per_sample * FRAME_LENGTH) fd = process.stdout states = [True] @@ -80,13 +82,15 @@ def recorder(process): fmt = '{freq:6.0f} Hz: {message:s}{extra:s}' fields = ['peak', 'total', 'rms', 'coherency'] -def recv(wave_record=wave.record, verbose=False, output=None): +def recv(config, audio_record=audio.record, verbose=False): extra = '' if verbose: extra = ''.join(', {0}={{{0}:.4f}}'.format(f) for f in fields) - for r in recorder(wave_record(stdout=wave.sp.PIPE)): - msg = fmt.format(extra=extra.format(**r), **r) - if not r['error']: + + recorder = audio_record(Fs=config.Fs) + for result in run_recorder(config=config, recorder=recorder): + msg = fmt.format(extra=extra.format(**result), **result) + if not result['error']: log.info(msg) else: log.error(msg) diff --git a/amodem/common.py b/amodem/common.py index 104a48c..7a29dc2 100644 --- a/amodem/common.py +++ b/amodem/common.py @@ -28,11 +28,9 @@ 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): diff --git a/amodem/dsp.py b/amodem/dsp.py index 812f4bc..1626e9b 100644 --- a/amodem/dsp.py +++ b/amodem/dsp.py @@ -4,7 +4,6 @@ import logging log = logging.getLogger(__name__) -from . import config from . import common @@ -64,9 +63,9 @@ def estimate(x, y, order, lookahead=0): 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 +73,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 +82,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 @@ -151,9 +149,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__ diff --git a/amodem/equalizer.py b/amodem/equalizer.py index fd14312..c1b4930 100644 --- a/amodem/equalizer.py +++ b/amodem/equalizer.py @@ -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 @@ -10,76 +9,76 @@ import random _constellation = [1, 1j, -1, -1j] +class Equalizer(object): -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)]) + 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 i in range(length)]) -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 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 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(self, signal, symbols, order, lookahead=0): + assert symbols.shape[1] == self.Nfreq + length = symbols.shape[0] + matched = np.array(self.carriers) / (0.5*self.Nsym) + matched = matched[:, ::-1].transpose().conj() + signal = np.concatenate([signal, np.zeros(lookahead)]) + y = dsp.lfilter(x=signal, b=matched, a=[1]) -def equalize_symbols(signal, symbols, order, lookahead=0): - Nsym = config.Nsym - Nfreq = config.Nfreq - carriers = config.carriers + A = [] + b = [] - assert symbols.shape[1] == Nfreq - length = symbols.shape[0] + for j in range(self.Nfreq): + for i in range(length): + offset = (i+1)*self.Nsym + row = y[offset-order:offset+lookahead, j] + A.append(row) + b.append(symbols[i, j]) - 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 = np.array(A) + b = np.array(b) + h, residuals, rank, sv = lstsq(A, b) + h = h[::-1].real - A = [] - b = [] + return h - 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]) + def equalize_signal(self, signal, expected, order, lookahead=0): + signal = np.concatenate([np.zeros(order-1), signal, np.zeros(lookahead)]) + length = len(expected) - A = np.array(A) - b = np.array(b) - h, residuals, rank, sv = lstsq(A, b) - h = h[::-1].real + A = [] + b = [] - return h + for i in range(length - order): + offset = order + i + row = signal[offset-order:offset+lookahead] + A.append(np.array(row, ndmin=2)) + b.append(expected[i]) - -def equalize_signal(signal, expected, order, lookahead=0): - signal = np.concatenate([np.zeros(order-1), signal, np.zeros(lookahead)]) - length = len(expected) - - A = [] - b = [] - - for i in range(length - order): - offset = order + i - row = signal[offset-order:offset+lookahead] - A.append(np.array(row, ndmin=2)) - b.append(expected[i]) - - A = np.concatenate(A, axis=0) - b = np.array(b) - h, residuals, rank, sv = lstsq(A, b) - h = h[::-1].real - return h + A = np.concatenate(A, axis=0) + b = np.array(b) + h, residuals, rank, sv = lstsq(A, b) + h = h[::-1].real + return h diff --git a/amodem/recv.py b/amodem/recv.py index 3a02018..0b0dee3 100644 --- a/amodem/recv.py +++ b/amodem/recv.py @@ -12,87 +12,97 @@ 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) +class Detector(object): -# Plots' size (WIDTH x HEIGHT) -HEIGHT = np.floor(np.sqrt(config.Nfreq)) -WIDTH = np.ceil(config.Nfreq / float(HEIGHT)) + COHERENCE_THRESHOLD = 0.99 -COHERENCE_THRESHOLD = 0.99 + CARRIER_DURATION = sum(train.prefix) + CARRIER_THRESHOLD = int(0.99 * CARRIER_DURATION) + SEARCH_WINDOW = 10 # symbols -CARRIER_DURATION = sum(train.prefix) -CARRIER_THRESHOLD = int(0.99 * CARRIER_DURATION) -SEARCH_WINDOW = 10 # symbols + def __init__(self, config): + 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 + def run(self, samples): + counter = 0 + bufs = collections.deque([], maxlen=self.maxlen) + for offset, buf in common.iterate(samples, self.Nsym, enumerate=True): + bufs.append(buf) -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 + coeff = dsp.coherence(buf, self.omega) + if abs(coeff) > self.COHERENCE_THRESHOLD: + counter += 1 + else: + counter = 0 - -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 + if counter == self.CARRIER_THRESHOLD: + length = (self.CARRIER_THRESHOLD - 1) * self.Nsym + begin = offset - length + amplitude = self.report_carrier(bufs, begin=begin) + break else: - counter = 0 + raise ValueError('No carrier detected') - 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)) - log.debug('Buffered %d ms of audio', len(bufs)) + bufs = list(bufs)[-self.CARRIER_THRESHOLD-self.SEARCH_WINDOW:] + trailing = list(itertools.islice(samples, self.SEARCH_WINDOW*self.Nsym)) + bufs.append(np.array(trailing)) - 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 = self.find_start(buf, self.CARRIER_DURATION*self.Nsym) + log.debug('Carrier starts at %.3f ms', + offset * self.Tsym * 1e3 / self.Nsym) - 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 + 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) + def report_carrier(self, bufs, begin): + x = np.concatenate(tuple(bufs)[-self.CARRIER_THRESHOLD:-1]) + Hc = dsp.exp_iwt(-self.omega, 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 * self.Tsym * 1e3 / self.Nsym, self.freq / 1e3, + np.abs(dsp.coherence(x, self.omega)) * 100, amp) + return amp + + + def find_start(self, buf, length): + N = len(buf) + carrier = dsp.exp_iwt(self.omega, N) + z = np.cumsum(buf * carrier) + z = np.concatenate([[0], z]) + correlations = np.abs(z[length:] - z[:-length]) + return np.argmax(correlations) class Receiver(object): - def __init__(self, plt=None): + def __init__(self, config, plt=None): self.stats = {} self.plt = plt or common.Dummy() + 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_report = config.baud # report once per second + self.modem_bitrate = config.modem_bps + self.equalizer = equalizer.Equalizer(config) + self.carrier_index = config.carrier_index - 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, skip=5): + S = common.take(symbols, len(train.prefix)) + S = S[:, self.carrier_index] * gain sliced = np.round(np.abs(S)) self.plt.figure() self.plt.subplot(121) @@ -116,7 +126,7 @@ class Receiver(object): self.plt.plot(indices, phase, ':') self.plt.plot(indices, a * indices + b) - freq_err = a / (config.Tsym * config.Fc) + freq_err = a / (self.Tsym * self.frequencies[self.carrier_index]) last_phase = a * indices[-1] + b log.debug('Current phase on carrier: %.3f', last_phase) @@ -125,16 +135,16 @@ class Receiver(object): 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) + train_symbols = self.equalizer.train_symbols(train.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 = train.silence_length * self.Nsym + signal_length = train.equalizer_length * self.Nsym + prefix + postfix signal = sampler.take(signal_length + lookahead) - coeffs = equalizer.equalize_signal( + coeffs = self.equalizer.equalize_signal( signal=signal[prefix:-postfix], expected=train_signal, order=order, lookahead=lookahead @@ -147,7 +157,7 @@ class Receiver(object): equalized = list(equalization_filter(signal)) equalized = equalized[prefix+lookahead:-postfix+lookahead] - symbols = equalizer.demodulator(equalized, train.equalizer_length) + symbols = self.equalizer.demodulator(equalized, train.equalizer_length) sliced = np.array(symbols).round() errors = np.array(sliced - train_symbols, dtype=np.bool) error_rate = errors.sum() / errors.size @@ -160,17 +170,15 @@ 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 = {} 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 = {} @@ -178,52 +186,51 @@ 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: %s', self.modem) + 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: + if i % self.iters_per_report == 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() duration = time.time() - self.stats['rx_start'] - sampler.freq -= 0.01 * err / config.Fc + sampler.freq -= 0.01 * err * self.Tsym 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), + duration * 100.0 / (i*self.Tsym), (1.0 - sampler.freq) * 1e6 ) - def start(self, signal, freqs, gain=1.0): + def start(self, signal, gain=1.0): sampler = sampling.Sampler(signal, sampling.Interpolator()) - - freq_err = self._prefix(sampler, freq=freqs[0], gain=gain) + symbols = dsp.Demux(sampler=sampler, omegas=self.omegas, Nsym=self.Nsym) + freq_err = self._prefix(symbols, gain=gain) sampler.freq -= freq_err filt = self._train(sampler, order=11, lookahead=6) sampler.equalizer = lambda x: list(filt(x)) - bitstream = self._demodulate(sampler, freqs) + bitstream = self._demodulate(sampler, symbols) self.bitstream = itertools.chain.from_iterable(bitstream) def run(self, output): @@ -237,7 +244,7 @@ class Receiver(object): 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) @@ -247,13 +254,18 @@ class Receiver(object): 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 = {} 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,6 +279,7 @@ class Receiver(object): def main(args): + config = args.config reader = stream.Reader(args.input, data_type=common.loads) signal = itertools.chain.from_iterable(reader) @@ -275,12 +288,13 @@ def main(args): reader.check = common.check_saturation - receiver = Receiver(plt=args.plot) + detector = Detector(config=config) + receiver = Receiver(config=config, plt=args.plot) success = False try: log.info('Waiting for carrier tone: %.1f kHz', config.Fc / 1e3) - signal, amplitude = detect(signal, config.Fc) - receiver.start(signal, config.frequencies, gain=1.0/amplitude) + signal, amplitude = detector.run(signal) + receiver.start(signal, gain=1.0/amplitude) receiver.run(args.output) success = True except Exception: diff --git a/amodem/send.py b/amodem/send.py index 53e606c..a06110e 100644 --- a/amodem/send.py +++ b/amodem/send.py @@ -5,78 +5,72 @@ 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(train.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) + 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(train.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 %8.1f kB', total_bits / 8e3) def main(args): - writer = Writer(args.output) + sender = Sender(args.output, config=args.config) + Fs = args.config.Fs # pre-padding audio with silence - writer.write(np.zeros(int(config.Fs * args.silence_start))) + sender.write(np.zeros(int(Fs * args.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) data = itertools.chain.from_iterable(reader) bits = framing.encode(data) - log.info('Starting modulation: %s', modem) - writer.modulate(bits=bits) + log.info('Starting modulation: %s', sender.modem) + 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 * args.silence_stop))) diff --git a/amodem/wave.py b/amodem/wave.py deleted file mode 100644 index c735c14..0000000 --- a/amodem/wave.py +++ /dev/null @@ -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') diff --git a/tests/test_wave.py b/tests/test_audio.py similarity index 52% rename from tests/test_wave.py rename to tests/test_audio.py index d56090a..fc206b4 100644 --- a/tests/test_wave.py +++ b/tests/test_audio.py @@ -1,27 +1,29 @@ -from amodem import wave +from amodem import audio import subprocess as sp import signal def test_launch(): - p = wave.launch(tool='true', fname='fname') + p = audio.ALSA(tool='true', Fs=32000).launch(fname='fname') assert p.wait() == 0 def test_exit(): - p = wave.launch(tool='python', fname='-', stdin=sp.PIPE) + p = audio.ALSA(tool='python', Fs=32000).launch(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) + p = audio.ALSA(tool='python', Fs=32000) + p = p.launch(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 = audio.ALSA(tool='python', Fs=32000) + p = p.launch(fname='-', stdin=sp.PIPE, stdout=sp.PIPE) p.kill() assert p.wait() == -signal.SIGKILL diff --git a/tests/test_calib.py b/tests/test_calib.py index 6adc712..1848036 100644 --- a/tests/test_calib.py +++ b/tests/test_calib.py @@ -1,4 +1,5 @@ from amodem import calib +from amodem import config from io import BytesIO @@ -8,10 +9,13 @@ class ProcessMock(object): 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,9 +30,9 @@ 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(): @@ -37,11 +41,11 @@ def test_errors(): def _write(data): raise IOError() p.write = _write - calib.send(p) + calib.send(config, p) assert p.buf.tell() == 0 def _read(data): raise KeyboardInterrupt() p.read = _read - calib.recv(p, verbose=True) + calib.recv(config, p, verbose=True) assert p.buf.tell() == 0 diff --git a/tests/test_dsp.py b/tests/test_dsp.py index 9119c02..e3145e7 100644 --- a/tests/test_dsp.py +++ b/tests/test_dsp.py @@ -59,11 +59,12 @@ def test_estimate(): 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 diff --git a/tests/test_equalizer.py b/tests/test_equalizer.py index 713263f..b7148a3 100644 --- a/tests/test_equalizer.py +++ b/tests/test_equalizer.py @@ -13,8 +13,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,10 +36,11 @@ 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) @@ -46,23 +48,24 @@ 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) + e = equalizer.Equalizer(config) + symbols = e.train_symbols(length=length) + x = e.modulator(symbols) * gain + assert_approx(e.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( + h = e.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) + z = e.demodulator(y, size=length) assert_approx(z, symbols) @@ -72,9 +75,10 @@ def test_signal(): den = np.array([1, -0.6, 0.1]) num = np.array([0.5]) y = dsp.lfilter(x=x, b=num, a=den) + e = equalizer.Equalizer(config) lookahead = 2 - h = equalizer.equalize_signal( + h = e.equalize_signal( signal=y, expected=x, order=len(den), lookahead=lookahead) assert norm(h[:lookahead]) < 1e-12 diff --git a/tests/test_recv.py b/tests/test_recv.py index d2dca80..08a9f11 100644 --- a/tests/test_recv.py +++ b/tests/test_recv.py @@ -1,6 +1,7 @@ import numpy as np from amodem import config +from amodem import dsp from amodem import recv from amodem import train from amodem import sampling @@ -10,29 +11,34 @@ 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) + + detector = recv.Detector(config) + samples, amp = detector.run(x) assert abs(1 - amp) < 1e-12 x = np.cos(2 * np.pi * (2*config.Fc) * t) try: - recv.detect(x, config.Fc) + detector.run(x) assert False except ValueError: pass def test_prefix(): - symbol = np.cos(2 * np.pi * config.Fc * np.arange(config.Nsym) * config.Ts) + omega = 2 * np.pi * config.Fc / config.Fs + symbol = np.cos(omega * np.arange(config.Nsym)) 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) + def symbols_stream(signal): + sampler = sampling.Sampler(signal) + return dsp.Demux(sampler=sampler, omegas=[omega], Nsym=config.Nsym) + r = recv.Receiver(config) + freq_err = r._prefix(symbols_stream(signal)) assert abs(freq_err) < 1e-16 try: silence = 0 * signal - r._prefix(sampling.Sampler(silence), freq=config.Fc) + r._prefix(symbols_stream(silence)) assert False except ValueError: pass @@ -40,6 +46,7 @@ def test_prefix(): def test_find_start(): sym = np.cos(2 * np.pi * config.Fc * np.arange(config.Nsym) * config.Ts) + detector = recv.Detector(config) length = 200 prefix = postfix = np.tile(0 * sym, 50) @@ -48,6 +55,6 @@ def test_find_start(): prefix = [0] * offset bufs = [prefix, prefix, carrier, postfix] buf = np.concatenate(bufs) - start = recv.find_start(buf, length*config.Nsym) + start = detector.find_start(buf, length*config.Nsym) expected = offset + len(prefix) assert expected == start diff --git a/tests/test_full.py b/tests/test_whole.py similarity index 89% rename from tests/test_full.py rename to tests/test_whole.py index 545df35..02bc7aa 100644 --- a/tests/test_full.py +++ b/tests/test_whole.py @@ -8,6 +8,7 @@ from amodem import recv from amodem import common from amodem import dsp from amodem import sampling +from amodem import config import logging logging.basicConfig(level=logging.DEBUG, @@ -27,7 +28,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, + send.main(Args(config=config, silence_start=1, silence_stop=1, input=BytesIO(tx_data), output=tx_audio)) data = tx_audio.getvalue() @@ -43,7 +44,8 @@ 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)) + result = recv.main(Args(config=config, + skip=0, input=rx_audio, output=rx_data)) rx_data = rx_data.getvalue() assert result == success @@ -61,7 +63,7 @@ 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)