mirror of
https://github.com/romanz/amodem.git
synced 2026-02-06 16:48:06 +08:00
calib: automatic microphone gain calibration
$ amodem-cli send -vv -c 'pactl set-sink-volume @DEFAULT_SINK@' will set speaker level to 100%. $ amodem-cli recv -vv -c 'pactl set-source-volume @DEFAULT_SOURCE@' will use "binary search", to find the best microphone gain.
This commit is contained in:
15
amodem-cli
15
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__':
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user