#!/usr/bin/python # -*- coding: utf-8 -*- """Report wireless signal strength using audio synthesis. This quick kludge is useful for figuring out where the dead spots in your wireless network coverage are. They’re where the pitch goes high and maybe you start getting clicks from audio buffer underruns. Example audio output can be found at . This is based on a brilliant hack by Seth Schoen, in which he generated a tone controlled by the signal strength indication from his wireless card in order to get a sort of 21st-century software Theremin. This adds some envelope modulation so that it’s useful as a network diagnostic tool rather than a musical instrument. It depends on PulseAudio and the interface to the usual set of Linux commands: pacat, ping, and iwconfig. (I should really just read /proc/net/wireless instead of depending on iwconfig.) """ from __future__ import print_function, division import argparse import errno import re import subprocess import sys import time try: unichr except NameError: unichr = chr def main(): args = parse_args() rate = 48000 jitter_buffer_seconds = 0.1 cmd = ["pacat", "--rate=%d" % rate, "--format=u8", "--channels=1", "--raw", "--latency-msec=%d" % int(round(1000 * jitter_buffer_seconds / 2)), ] audio = subprocess.Popen(cmd, stdin=subprocess.PIPE) null = open("/dev/null", "w") start = time.time() emitted_data = 0 master_volume = 10**(args.volume/20) network_pinger = NetworkPinger(timeout=jitter_buffer_seconds/4) bpm = 84 # Marcia moderato beat_time = 60.0 / bpm while True: now = time.time() elapsed = now - start dBm, err = get_signal_level(null) if err: print(err) time.sleep(0.2) # That will probably cause an underrun. start = time.time() emitted_data = 0 continue err = network_pinger.ping() if err: print("ping:", err) f = 13.75 * 2**(dBm/12.0) # Convert to A440 12-tone equal temperament period = int(round(rate / f)) print("-%d dBm, %d Hz" % (dBm, f), end=' ') if elapsed % .1 < .02: # keep lines short, kind of at random print() sys.stdout.flush() # Envelope modulation: triangle wave per note and per measure note_phase = (elapsed / beat_time) % 1 note_volume = 5 - (elapsed / beat_time) % 4 amplitude = int(255 * note_volume * master_volume * (1 - note_phase)) # Tone synthesis: square wave, 16 periods neg = unichr(max(0, 128-amplitude)).encode('iso-8859-1') neglen = period//2 pos = unichr(min(255, 128+amplitude)).encode('iso-8859-1') poslen = period - neglen data = (neg * neglen + pos * poslen) * 16 audio.stdin.write(data) # Prevent latency from building up (audio bufferbloat) emitted_data += len(data) overfull_secs = emitted_data / float(rate) - (elapsed + jitter_buffer_seconds) if overfull_secs > 0: time.sleep(overfull_secs) # If we’ve had underruns in the past we might find latency # increasing, as the soundcard/PulseAudio will not have caught # up to where we think it should be. A way to solve that is # to not emit any audio for a beat, then restart all counts: if elapsed > 14 * beat_time and note_phase > .97: time.sleep(beat_time) start = time.time() emitted_data = 0 def parse_args(): parser = argparse.ArgumentParser( description="Indicates Wi-Fi signal strength with sound pitch.", ) parser.add_argument('volume', default=-20, type=int, nargs='?', help='audio volume in dB (default %(default)s)') return parser.parse_args() class NetworkPinger: def __init__(self, timeout): self.process = None self.timeout = timeout self.default_gateway = get_default_gateway() self.null = open("/dev/null", "w") def ping(self): err = None if self.process: # Sometimes ping isn’t very reliable about exiting after # the timeout! try: self.process.kill() except OSError as exc: if exc.errno == errno.ESRCH: pass else: raise self.process.wait() if self.process.returncode != 0: # Maybe ping failed because it was pinging the wrong # default gateway. self.default_gateway = get_default_gateway() if self.process.returncode != -9: # Don’t return an error if it merely took too long # to return and so we killed it (although I guess # that’s actually when ping fails usually?) err = "ping failed: %s" % (self.process.returncode,) self.process = subprocess.Popen(["ping", "-c", "1", self.default_gateway, '-W', '%f' % self.timeout, ], stdout=self.null, stdin=self.null) return err signal_level = re.compile(r"Signal level=-(\d+) dBm") def get_signal_level(null): iwconfig_output = subprocess.check_output("/sbin/iwconfig", stderr=null).decode('utf-8') signal_match = signal_level.search(iwconfig_output) if not signal_match: return None, "can’t find level in " + iwconfig_output return int(signal_match.group(1)), None def get_default_gateway(): default_gateway = "192.168.0.1" # a default for line in subprocess.Popen(["/sbin/route", "-n"], stdout=subprocess.PIPE).stdout: fields = line.split() if fields[0] == fields[2] == "0.0.0.0": default_gateway = fields[1] return default_gateway if __name__ == '__main__': main()