Skip to content

Instantly share code, notes, and snippets.

@mattpitkin
Last active January 10, 2023 22:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattpitkin/242a6e39e65775e172ac4379695f28d6 to your computer and use it in GitHub Desktop.
Save mattpitkin/242a6e39e65775e172ac4379695f28d6 to your computer and use it in GitHub Desktop.
DTMF keypad
"""
GUI keypad that plays DTMF (Dual-tone multi-frequency) signals.
https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling
"""
import sys
from functools import partial
from string import ascii_uppercase
from threading import Thread, Event
import pyaudio
import numpy as np
from PyQt5.QtWidgets import QApplication, QGridLayout, QPushButton, QWidget
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt
# map between keyboard values and button text
KEYS = {
int(Qt.Key_0): "0",
int(Qt.Key_1): "1",
int(Qt.Key_2): "2",
int(Qt.Key_3): "3",
int(Qt.Key_4): "4",
int(Qt.Key_5): "5",
int(Qt.Key_6): "6",
int(Qt.Key_7): "7",
int(Qt.Key_8): "8",
int(Qt.Key_9): "9",
int(Qt.Key_A): "A",
int(Qt.Key_B): "B",
int(Qt.Key_C): "C",
int(Qt.Key_D): "D",
int(Qt.Key_Asterisk): "*",
int(Qt.Key_NumberSign): "#",
}
# map between button text and dual tone frequencies
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],
}
SR = 44100 # output sample rate
class KeyPad(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("DTMF keypad")
# grid layout
self._layout = QGridLayout()
self.keys_pressed = []
self.add_buttons()
self.setLayout(self._layout)
# open audio output
self._pa = pyaudio.PyAudio()
self._chunk = 1024
self._playing = Event()
self._playing.clear()
self.show()
def add_buttons(self):
# add buttons to keypad
self.buttons = {}
self.buttons["1"] = QPushButton("1")
self.buttons["2"] = QPushButton("2")
self.buttons["3"] = QPushButton("3")
self.buttons["A"] = QPushButton("A")
self.buttons["4"] = QPushButton("4")
self.buttons["5"] = QPushButton("5")
self.buttons["6"] = QPushButton("6")
self.buttons["B"] = QPushButton("B")
self.buttons["7"] = QPushButton("7")
self.buttons["8"] = QPushButton("8")
self.buttons["9"] = QPushButton("9")
self.buttons["C"] = QPushButton("C")
self.buttons["*"] = QPushButton("*")
self.buttons["0"] = QPushButton("0")
self.buttons["#"] = QPushButton("#")
self.buttons["D"] = QPushButton("D")
# key pad shape
shape = (4, 4)
# list of current tone frequencies to play
self._freqs = []
for i, key in enumerate(self.buttons):
self.buttons[key].setFixedSize(64, 64)
self.buttons[key].setFont(QFont("Arial", 24, QFont.Bold))
if key in ascii_uppercase:
self.buttons[key].setStyleSheet("background-color: #ff0000")
elif not key.isdigit():
self.buttons[key].setStyleSheet("background-color: #00ff00")
# add frequencies on button click
self.buttons[key].pressed.connect(partial(self._add_freqs, FREQUENCIES[key]))
# remove frequencies when button released
self.buttons[key].released.connect(partial(self._remove_freqs, FREQUENCIES[key]))
pos = np.unravel_index(i, shape)
self._layout.addWidget(self.buttons[key], pos[0], pos[1])
def _add_freqs(self, freqs):
"""
Add tone frequencies.
"""
start_audio = False
if len(self._freqs) == 0:
start_audio = True
self._freqs.append(freqs)
if start_audio:
# start audio output thread
self._playing.set()
# open audio stream
self._stream = self._pa.open(
format=pyaudio.paFloat32, # 32-bit float
channels=1, # mono channel
rate=SR,
output=True,
frames_per_buffer=self._chunk,
)
self._playing_thread = Thread(target=self.play_tones, daemon=True)
self._playing_thread.start()
def _remove_freqs(self, freqs):
"""
Remove tone frequencies.
"""
self._freqs.remove(freqs)
if len(self._freqs) == 0:
# end audio output thread
self._playing.clear()
def keyPressEvent(self, event):
if not event.isAutoRepeat():
for key in KEYS:
if int(event.key()) == key:
if FREQUENCIES[KEYS[key]] not in self._freqs:
self._add_freqs(FREQUENCIES[KEYS[key]])
self.buttons[KEYS[key]].setDown(True)
break
def keyReleaseEvent(self, event):
if not event.isAutoRepeat():
for key in KEYS:
if int(event.key()) == key:
if FREQUENCIES[KEYS[key]] in self._freqs:
self._remove_freqs(FREQUENCIES[KEYS[key]])
self.buttons[KEYS[key]].setDown(False)
break
def closeEvent(self, event):
# stop thread and close pyAudio stream
self._playing.clear()
try:
self._stream.close()
except:
pass
self._pa.terminate()
def play_tones(self):
t0 = 0
dt = 1 / SR
deltat = self._chunk * dt
amp = 1 / 16 # set amplitude so that total cannot go above 1
while self._playing.is_set():
times = np.arange(t0, t0 + deltat, dt)
tones = np.zeros_like(times, dtype=np.float32)
for f in np.unique(np.array(self._freqs).flatten()):
tones += (amp * np.sin(2 * np.pi * f * times)).astype(np.float32)
self._stream.write(tones.tobytes())
t0 += deltat
self._stream.close()
app = QApplication([])
app.setStyle("Fusion") # explicitly use "Fusion" style sheet rather than default https://stackoverflow.com/a/75075364/1862861
keypad = KeyPad()
sys.exit(app.exec())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment