diff --git a/amodem-cli b/amodem-cli index 0b65dc0..80d9482 100755 --- a/amodem-cli +++ b/amodem-cli @@ -106,14 +106,14 @@ def main(): '-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') + '-c', '--calibrate', nargs='?', default=False) 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 + config=config, dst=args.dst, volume_cmd=args.calibrate ), input_type=FileType('rb'), output_type=FileType('wb', interface) @@ -128,7 +128,7 @@ def main(): receiver.add_argument( '-o', '--output', help='output file (use "-" for stdout).') receiver.add_argument( - '-c', '--calibrate', default=False, action='store_true') + '-c', '--calibrate', nargs='?', default=False) receiver.add_argument( '-d', '--dump', type=FileType('wb'), help='Filename to save recorded audio') @@ -141,7 +141,8 @@ def main(): pylab=args.pylab, dump_audio=args.dump ), calib=lambda config, args: calib.recv( - config, src=args.src, verbose=args.verbose + config=config, src=args.src, verbose=args.verbose, + volume_cmd=args.calibrate ), input_type=FileType('rb', interface), output_type=FileType('wb') @@ -181,10 +182,10 @@ def main(): args.src = args.input_type(args.input) args.dst = args.output_type(args.output) - if args.calibrate: - args.calib(config=config, args=args) - else: + if args.calibrate is False: return args.main(config=config, args=args) + else: + args.calib(config=config, args=args) if __name__ == '__main__': diff --git a/amodem/calib.py b/amodem/calib.py index 99bc40f..547d4be 100644 --- a/amodem/calib.py +++ b/amodem/calib.py @@ -1,6 +1,7 @@ import numpy as np import itertools import logging +import subprocess log = logging.getLogger(__name__) @@ -10,8 +11,20 @@ from . import sampling ALLOWED_EXCEPTIONS = (IOError, KeyboardInterrupt) +def volume_controller(cmd): + def controller(level): + assert 0 < level <= 1 + percent = 100 * level + args = '{0} {1:.0f}%'.format(cmd, percent) + log.debug('Setting volume: %.3f%% (via "%s")', percent, args) + subprocess.check_call(args=args, shell=True) + return controller if cmd else (lambda level: None) + + +def send(config, dst, volume_cmd=None): + volume_ctl = volume_controller(volume_cmd) + volume_ctl(1.0) # full scale output volume -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] @@ -48,7 +61,6 @@ def frame_iter(config, src, frame_length): 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): @@ -56,33 +68,64 @@ def detector(config, src, frame_length=200): freq = config.frequencies[max_index] rms = abs(coeffs[max_index]) coherency = rms / total - flags = [rms > 0.1, peak < 1.0, coherency > 0.99] + flags = [total > 0.1, peak < 1.0, coherency > 0.99] - states.append(all(flags)) - states = states[-2:] - - message = 'good signal' - error = not any(states) - if error: + success = all(flags) + if success: + message = 'good signal' + else: 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 + total=total, success=success, message=message )) except ALLOWED_EXCEPTIONS: pass +def volume_calibration(result_iterator, volume_ctl): -def recv(config, src, verbose=False): - fmt = '{0.freq:6.0f} Hz: {0.message:s}' + min_level = 0.01 + max_level = 1.0 + level = 0.5 + step = 0.25 + + target_level = 0.4 # not too strong, not too weak + iters_per_update = 10 # update every 2 seconds + + for index, result in enumerate(itertools.chain([None], result_iterator)): + if index % iters_per_update == 0: + if index > 0: # skip dummy (first result) + sign = 1 if (result.total < target_level) else -1 + level = level + step * sign + level = min(max(level, min_level), max_level) + step = step * 0.5 + + volume_ctl(level) # should run "before" first iteration + + if index > 0: # skip dummy (first result) + yield result + +def recv(config, src, verbose=False, volume_cmd=None): + fmt = '{0.freq:6.0f} Hz: {0.message:20s}' if verbose: fields = ['peak', 'total', 'rms', 'coherency'] - fmt += ''.join(', {0}={{0.{0}:.4f}}'.format(f) for f in fields) + fmt += ', '.join('{0}={{0.{0}:.4f}}'.format(f) for f in fields) + + result_iterator = detector(config=config, src=src) + if volume_cmd: + log.info('Using automatic calibration (via "%s")', volume_cmd) + + volume_ctl = volume_controller(volume_cmd) + + errors = [] + for result in volume_calibration(result_iterator, volume_ctl): + errors.append(not result.success) + errors = errors[-3:] - for result in detector(config=config, src=src): msg = fmt.format(result) - if not result.error: - log.info(msg) - else: + if all(errors): log.error(msg) + else: + log.info(msg) +