Raymond Preble

CMPR 271 · Course project

DTMF tone generator

For CMPR 271, I wrote a C program that generates DTMF tone sequences and saves them as 16-bit PCM WAV files. The program uses only the C standard library and its sin() function; it does not rely on an audio or DSP library.

Spring 2026 C — standard library only Output — 16-bit PCM, 44.1 kHz, mono

Note on scope

This page explains the program's design, mathematics, implementation decisions and verification. It uses diagrams and pseudocode instead of the original source code, in accordance with RIT's Academic Honesty Policy.

01Project overview

DTMF, or dual-tone multi-frequency signalling, is the system used by traditional telephone keypads. Each keypad character is represented by two sine waves: one frequency from a low-frequency group and one from a high-frequency group. The two signals are added together to produce the final tone.

The program accepts three inputs:

It then generates the corresponding waveform and writes it to a WAV file. The resulting file can be played by standard audio software or used as a test signal for a DTMF decoder.

The project involved more than generating two sine waves. I also had to manage the signal amplitude, calculate the correct number of samples, construct a valid WAV header, validate command-line arguments, and prevent buffer-indexing errors in longer sequences.

02How DTMF works

DTMF uses a frequency grid. Each row corresponds to one low-group frequency, and each column corresponds to one high-group frequency.

Table 1 — The DTMF frequency matrix. Each key transmits its row and column frequency simultaneously.
1209 Hz1336 Hz1477 Hz
697 Hz123
770 Hz456
852 Hz789
941 Hz*0#

When a key is pressed, the corresponding row and column frequencies are transmitted at the same time. The digit 5, for example, uses 770 Hz and 1336 Hz. A DTMF sample can be represented as

x[n] = A·sin(2π flow n / fs) + A·sin(2π fhigh n / fs) A is the amplitude of each tone, flow and fhigh are the selected group frequencies, fs is the sampling rate, and n is the sample index.

The two tones are generated independently and added together for every sample. The frequencies are separated into two groups so that a receiver can analyse the low and high ranges independently: a valid symbol requires one frequency from each group rather than a single tone.

03Signal generation

The program uses a 44.1 kHz sample rate, so the Nyquist frequency is 22 050 Hz. The highest DTMF frequency is 1477 Hz, well below that limit, so the generated tones do not alias at this sampling rate.

Each sample is calculated directly with sin() rather than being generated with a wavetable, an oscillator object, or an audio library. For each keypad character, the program:

  1. Looks up the corresponding low and high frequencies.
  2. Calculates the two sine-wave values for each sample.
  3. Adds the two values together.
  4. Scales the result into the signed 16-bit range.
  5. Stores the sample in the output buffer.
Digit string 585-2400 Key → tone pair table lookup Low-group tone 697 / 770 / 852 / 941 Hz High-group tone 1209 / 1336 / 1477 Hz Σ Scale to 16-bit A = 16382 per tone RIFF/WAVE header + samples f low f high x[n]
Fig. 1 — Signal path. Each digit selects one frequency from each group; the two sinusoids are summed at every sample instant and scaled once into the 16-bit range.

The output uses signed 16-bit samples, whose range is −32768 to 32767.

04Amplitude scaling

Adding two sine waves can produce a peak value twice as large as either individual sine wave. If each tone used an amplitude of 32767, their sum could exceed the range of a signed 16-bit sample.

To avoid clipping, I used an amplitude of 16382 for each tone, so the maximum theoretical sum is three counts below the positive limit:

16382 + 16382 = 32764 < 32767 The amplitude was determined mathematically rather than adjusted by listening to the output.

This leaves a small amount of headroom and prevents the waveform from overflowing the 16-bit range. Without this scaling, an overflowing value could wrap around to the opposite side of the signed integer range, creating a large discontinuity in the waveform. That discontinuity would sound like a click, and could also interfere with DTMF detection.

05Sample-count calculation

Every digit must occupy the same number of samples. The number of samples per tone is calculated from the sample rate and the requested duration, and the conversion to an integer happens once, before the program generates the sequence. For a sequence of digits, the starting position of digit k is k × samples_per_tone.

samples_per_tone ← integer(sample_rate × duration)

for each digit k in the input string:
    (f_low, f_high) ← lookup(digit)

    for n from 0 to samples_per_tone − 1:
        index ← k × samples_per_tone + n

        buffer[index] ←
            A × sin(2π × f_low  × n / sample_rate)
          + A × sin(2π × f_high × n / sample_rate)

06A floating-point indexing bug

One of the bugs in the program involved calculating buffer offsets from the floating-point duration directly.

A duration such as 0.3 seconds does not have an exact binary floating-point representation. Multiplying that value by the sample rate produces an approximate sample count, and if that approximate value is multiplied by the digit index each time an offset is calculated, the small error accumulates across a long sequence.

As a result, later digits could begin slightly before or after the expected position. That could create a one-sample gap, overlap adjacent tones, or cause the final write to extend beyond the allocated buffer.

The solution was to convert the duration to an integer sample count once, and calculate all later offsets from that integer. This guarantees that every digit uses the same number of samples and that the buffer positions remain exact.

The issue was not caused by the overall algorithm. The algorithm was correct, but the calculation mixed a continuous floating-point quantity with a discrete sample index. Converting to an integer at the point where the duration becomes a sample count removed that ambiguity.

07WAV file structure

The generated audio is stored in the WAV format. WAV files use the RIFF container format, which consists of a header followed by the raw sample data.

Table 2 — Header fields that have to be calculated rather than copied.
FieldValuePurpose
ChunkSize36 + data bytesSize of the file after the first eight bytes
ByteRaterate × channels × bytesNumber of bytes played per second
BlockAlignchannels × bytesSize of one sample frame
SubChunk2Sizesamples × channels × bytesSize of the audio data

For this project the file contains one audio channel, a 44.1 kHz sample rate, 16-bit samples, PCM encoding and no compression. The header fields are derived from those settings and the total number of generated samples.

A WAV file can still open even when one of these fields is incorrect. The audio may simply play at the wrong speed, end prematurely, or cause the player to read the wrong amount of data. That made the header another part of the project that required careful calculation rather than trial and error.

The header was represented with a packed C structure so that its layout matched the WAV format byte for byte. Without packing, the compiler could insert padding between fields for alignment, changing the positions of fields in the file.

08Input validation and memory

The program validates its arguments before allocating the audio buffer. It checks the number of command-line arguments, the requested tone duration, each character in the digit string, and whether each character belongs to the supported DTMF keypad. If an argument is invalid, the program prints usage information and exits with a non-zero status.

The buffer size is calculated from the number of input characters and the number of samples per tone. The program also checks whether memory allocation succeeds, and after the WAV file has been written, the allocated memory is released before the program exits.

09Verification

Listening to the output is useful, but it is not enough to verify the generator. A file can sound correct while still containing an incorrect header, inaccurate timing, or an indexing problem that appears only in longer sequences. I verified the program by checking the following:

Spectrogram of a generated DTMF sequence, showing two horizontal bands per digit below 1500 Hz
Fig. 2 — Spectrogram of a generated sequence. Every digit resolves into exactly two bands — one in the 697–941 Hz low group, one in the 1209–1477 Hz high group — matching the pairs in Table 1, holding steady for the tone duration and changing cleanly at each boundary. It is a visual check against the frequency plan, not a measurement against the DTMF tolerance spec.

These tests confirmed that the program could generate arbitrary supported DTMF sequences at the requested duration.

10Lessons from the project

This project reinforced several ideas that apply beyond audio programming.

First, sampling parameters should be chosen before implementation. The sample rate determines the Nyquist limit, while the signal amplitude determines whether the output will clip. Both decisions can be checked mathematically before generating any samples.

Second, binary file formats must be treated as interfaces. Every header field has a specific meaning and must be calculated from the format specification. The compiler also cannot be assumed to arrange a structure in the same way as the file format unless that layout is explicitly controlled.

Finally, sample indexing highlighted the difference between continuous and discrete quantities. A duration can begin as a floating-point value, but once it determines a buffer length it should be converted to an integer sample count. Making that conversion once avoids inconsistent offsets and makes the rest of the algorithm easier to reason about.

This project gave me experience with waveform synthesis, numerical representation, binary file formats, memory management and signal verification, all within a relatively small C program. It is also the work here that comes closest to the audio engineering I want to do more of.