Close

Don't Use That Tone with Me

A project log for The Phone Friend

Bring any old phone to life, no disassembly required!

stephSteph 04/12/2023 at 05:320 Comments

Next Stop: Dial Tone

Dial tone is comprised of two tones playing at the same time: 440hz and 350hz (in North America anyway, the exact tones vary by location).  

A common way to reproduce a dial tone is to loop the playback of a short wav or mp3 recording, but this method has drawbacks:

With careful loop creation these issues can be managed, but there is another way.

Let's Roll Our Own

Instead of playing back a recording of some dial tone, we can generate our own. Here are the benefits:

Here's how it works. In this code, we'll build on our last code by importing some math and audio libraries then generating and playing the dial tone. It's a little longer than our last script, but it's still easy to understand:

import digitalio
import board
import time

# We'll need these to generate our sine waves
import math
import array

# These libraries handle our audio needs
from audiopwmio import PWMAudioOut
from audiocore import RawSample
import audiomixer

# Setup Pins
audio = PWMAudioOut(board.GP22)
                        
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

SHK = digitalio.DigitalInOut(board.GP21)
SHK.direction = digitalio.Direction.INPUT

slicEnable = digitalio.DigitalInOut(board.GP18)
slicEnable.direction = digitalio.Direction.OUTPUT 

# Per datasheet, AG1171 needs 50ms to wake up
slicEnable.value = True
time.sleep(.05) 

# This function accepts a frequency in Hz
# and returns a playable RawSample
def generateTone(frequency): 
    samples = sampleRate // frequency
    buffer = array.array("h", [0] * samples)
    # The ugly bit below fills the buffer with our wave
    for i in range(samples):
        # Calculate the angle for this sample in radians
        angle = 2 * math.pi * i / samples
        # Calculate the sine value, scale to the range of
        # int16 then stick it back in the buffer
        buffer[i] = int(math.sin(angle) * 32767)    
    tone = RawSample(buffer, sample_rate=sampleRate)
    return tone

# This function sets volume for both voices,
# then starts playing both tones in a loop
def dialTone():
    if not audio.playing:
        audio.play(mixer)            
        mixer.voice[0].level = .2    
        mixer.voice[1].level = .2
        mixer.voice[0].play(tone440, loop=True)    
        mixer.voice[1].play(tone350, loop=True)
        
# Setup the sample rate (other rates sound worse for me)
sampleRate = 31000

# The mixer combines our tones and controls volume
mixer = audiomixer.Mixer(buffer_size=1024, voice_count=2, 
                   sample_rate=sampleRate, channel_count=1,
                   bits_per_sample=16, samples_signed=True)
                        
# Use our tone function to generate and save the dial tones
tone440 = generateTone(440)
tone350 = generateTone(350)

# Main loop
while True:
    # Turn on LED when off-hook
    led.value = SHK.value
    
    # Turn on dial tone when handset is lifted
    if SHK.value:
        dialTone()
    else:
        audio.stop()
    
    time.sleep(.1)

Results!

Listen to the Phone Friend dial tone here!

(I tried to embed it here but it didn't work, sorry)

PWM output is normally filtered to roll off high-frequency noise. I've skipped this step in the Phone Friend because the low-quality nature of telephony serves as it's own treble filter. Because of this, you'll hear some harshness in this recording that ordinarily isn't audible on the telephone handset.

This clip was captured by tapping into the Pico's audio output on Pin 22 directly, and passing to the input of a Marantz recorder set at 48khz, 16bit uncompressed wave. You'll hear a loud click as the handset is lifted, followed immediately by our dial tone starting. 

This is a waveform produced by recording the output of the above code running on the Phone Friend. 

Here's ideal dial tone made in Pro Tools on the left, and our result on the right:

Looks Pretty Good!

Zooming in, we can see that our waveform (right) isn't quite as smooth as the one generated in Pro Tools. The noise introduced by our PWM technique is also visible:

Discussions