Skip to content

Instantly share code, notes, and snippets.

@wecsam
Created October 10, 2017 22:50
Show Gist options
  • Save wecsam/d3c08295ae94c605d83d962609ca2025 to your computer and use it in GitHub Desktop.
Save wecsam/d3c08295ae94c605d83d962609ca2025 to your computer and use it in GitHub Desktop.
Generates touch tones like on a telephone
#!/usr/bin/env python3
# DTMF = Dual Tone Multi-Frequency
import itertools, math, pyaudio, random, sys
BITRATE = 8000
TONE_DURATION = 0.1 # seconds
KEY_FREQUENCIES = {
'1': (697, 1209),
'2': (697, 1336),
'3': (697, 1477),
'A': (697, 1633),
'4': (770, 1209),
'5': (770, 1336),
'6': (770, 1477),
'B': (770, 1633),
'7': (852, 1209),
'8': (852, 1336),
'9': (852, 1477),
'C': (852, 1633),
'*': (941, 1209),
'0': (941, 1336),
'#': (941, 1477),
'D': (941, 1633)
}
def sine_wave(frequency, frame=0):
# Yield frames forever.
while True:
yield int(math.sin(frequency * frame * 2.0 * math.pi / BITRATE) * 63.0 + 128.0)
frame += 1
def mix(*waves):
# Add the waves.
wave_iterators = [iter(wave) for wave in waves]
while True:
yield max(0, min(255, sum(next(wave) - 128 for wave in wave_iterators) + 128))
def mix_sine_waves(*frequencies):
# Return the sum of sine waves at these frequencies.
return mix(*[sine_wave(frequency) for frequency in frequencies])
def key_tone(key):
# Return the wave that would play when this key is pressed.
return mix_sine_waves(*KEY_FREQUENCIES[key])
def num_frames(seconds):
# Return the number of frames in this number of seconds.
return int(seconds * BITRATE)
def tone_for_duration(tone_generator, seconds):
return bytes(itertools.islice(tone_generator, num_frames(seconds)))
def silence(seconds):
return bytes(128 for i in range(num_frames(seconds)))
if __name__ == "__main__":
random.seed()
# Set up the audio output.
p = pyaudio.PyAudio()
try:
stream = p.open(
format=pyaudio.paUInt8,
channels=1,
rate=BITRATE,
output=True
)
# Loop until the user enters a blank line.
try:
print("Enter a blank line to quit.")
while True:
user_input = input("Type a sequence of keys on a telephone keypad: ").strip().upper()
if user_input:
# First, make sure that all the characters have tones defined.
valid = True
for c in user_input:
if c not in KEY_FREQUENCIES:
print(" > Error:", repr(c), "is not on the telephone keypad.")
valid = False
if valid:
# Play the dial tone.
sys.stdout.write(" > Off-hook...")
sys.stdout.flush()
stream.write(tone_for_duration(mix_sine_waves(350, 440), 1.5))
# Dial the numbers.
sys.stdout.write(" dialing ")
sys.stdout.flush()
stream.write(silence(0.1))
for c in user_input:
sys.stdout.write(c)
sys.stdout.flush()
# Write the dual-tone to the stream.
stream.write(tone_for_duration(key_tone(c), TONE_DURATION))
# Write a little bit of silence.
stream.write(silence(0.05))
# Write some additional silence.
sys.stdout.write("...")
sys.stdout.flush()
stream.write(silence(2.0))
# Play the ringback tone or busy signal.
if random.random() < 0.9:
sys.stdout.write(" ringing...")
sys.stdout.flush()
for i in range(4):
stream.write(tone_for_duration(mix_sine_waves(440, 480), 2.0))
stream.write(silence(4.0))
else:
sys.stdout.write(" busy signal...")
sys.stdout.flush()
for i in range(4):
stream.write(tone_for_duration(mix_sine_waves(480, 620), 0.5))
stream.write(silence(0.5))
print(" sequence complete.")
else:
break
finally:
# Clean up.
stream.stop_stream()
stream.close()
finally:
p.terminate()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment