;;;----------------------------------------------------------------------------------- ;;; ;;; dtmf.asm: A DTMF decoder for the Alesis 1K DSP ;;; ;;; Description: This program listens to an audio signal and outputs a message ;;; corresponding to which touch-tone button, if any, it hears. ;;; ;;; Input: The audio signal. ;;; ;;; Output: The detected button number / 2^12, or zero if no valid touch-tone is heard. ;;; ;;; Background: When you press a button on a touch-tone phone, what you hear are two ;;; sine waves, with each wave's frequency indicating a particular row or column ;;; of the keypad respectively. The frequencies of the rows and columns are, in Hz: ;;; ;;; 1209 1336 1477 ;;; 697 [1] [2] [3] ;;; 770 [4] [5] [6] ;;; 852 [7] [8] [9] ;;; 941 [*] [0] [#] ;;; ;;; Theory of Operation: This DTMF detector correlates the input signal with seven ;;; locally-generated sine waves at the DTMF frequencies. If a valid pair of tones is ;;; detected, the appropriate message is output. The particular implemention below ;;; is not necessarily especially robust, which is why you found it in your assembler's ;;; reference manual and not in your answering machine. ;;; .option allow-destructive-if .option list-macro-definitions=0 .option list-included-files=0 ;;; constants NUM_TONES .equ 7 NUM_BUTTONS .equ 12 DTMF_FREQUENCIES .table 697, 770, 852, 941, 1209, 1336, 1477 ; Hz FS .equ 48000 ; Hz OUTPUT_DEBOUNCE_TIME .equ 50 ; ms TONE_DETECTION_THRESHOLD .equ 4 ;;; make a table of bitmasks that correspond to the pair of tones for each button #define tonebit(Ptone) ( 0.5^(Ptone) ) #define buttonmask(Pfirsttone,Psecondtone) ( tonebit(Pfirsttone) + tonebit(Psecondtone) ) BUTTON_MASKS .table buttonmask(0,4), buttonmask(0,5), buttonmask(0,6), \ buttonmask(1,4), buttonmask(1,5), buttonmask(1,6), \ buttonmask(2,4), buttonmask(2,5), buttonmask(2,6), \ buttonmask(3,4), buttonmask(3,5), buttonmask(3,6) ;;;----------------------------------------------------------------------------------- ;;; ;;; Macro: dtmf_detect_tone $tone ;;; Params: $tone is the tone number to listen to, from 0 to NUM_TONES - 1 ;;; Input: The audio signal is assumed to be in [input]. ;;; Output: Adds tonebit($tone) to [detected_tone_mask] if tone is heard. ;;; ;;; Description: Generates a sine wave at the tone's frequency and correlates it ;;; with the audio signal. Since we don't know the phase of the component we're ;;; listening for, we correlate with both a sine and cosine wave, and then take ;;; the magnitude of the resulting vector. If this magnitude is above a threshold, ;;; we claim to have heard the tone. The oscillator used is the modified coupled ;;; form or "magic circle" algorithm, which has a particularly convenient ;;; implementation (just four 1K instructions), long-term stability, and generates ;;; two outputs which are approximately in quadrature at low frequencies (close ;;; enough for our purposes.) In a real implementation, the oscillator would have ;;; to be initialized. ;;; dtmf_detect_tone .macro $tone { `sin_wave .ds ; reference sine wave for this tone `sin_sum .ds ; correlation accumulator for the sine wave `cos_wave .ds ; reference cosine wave for this tone `cos_sum .ds ; correlation accumulator for the cosine wave epsilon .lequ 2 * sin(pi * DTMF_FREQUENCIES($tone) / FS) ;;; update oscillator a = -epsilon * [`sin_wave] ; a = -ep * sin a += [`cos_wave] ; a = cos - ep * sin [`cos_wave] = a ; z*cos = cos - ep * sin b = [`sin_wave] ; b = sin a = epsilon * a + b ; a = sin + ep * (z*cos) [`sin_wave] = a ; z*sin = sin + ep * (z*cos) ;;; correlate with sine wave a *= [input] ; multiply sine wave by input signal a = a/16 + [`sin_sum] ; scale down the result and add it to the sine accumulator [`sin_sum] = a ; store it back ;;; correlate with cosine wave b = [input] ; b = input signal (piggybacks above) a = b * [`cos_wave] ; multiply cosine wave by input signal a = a/16 + [`cos_sum] ; scale down the result and add it to the cosine accumulator [`cos_sum] = a ; store it back ;;; take vector magnitude (squared) a = a^2 ; square the cosine accumulator value b = a ; b = cos_sum^2 a = [`sin_sum]^2 ; a = sin_sum^2 a += b ; a = cos_sum^2 + sin_sum^2 ;;; set output bit if above threshold if (a > TONE_DETECTION_THRESHOLD) { ; is the correlation magnitude above the threshold? a = tonebit($tone) + [detected_tone_mask] ; if so, set a bit in the detected tone mask [detected_tone_mask] = a ; and store it back } } .endm ;;;----------------------------------------------------------------------------------- ;;; ;;; Macro: dtmf_detect $input, $output ;;; Params: The audio signal is assumed to be in [$input]. ;;; The output message is written to [$output]. ;;; ;;; Description: Listens for the seven DTMF tones, and outputs the appropriate message ;;; if they correspond to a valid DTMF pair, or zero if not. The detected tones (or ;;; lack thereof) must be stable for OUTPUT_DEBOUNCE_TIME ms before the output message ;;; is changed. ;;; dtmf_detect .macro $input, $output { `previous_tone_mask .ds ; detected_tone_mask from last sample `output_debounce_counter .ds ; counter to debounce the output message detected_tone_mask .ls ; bitmask representing which tones were heard input .lequ $input ; memory location of the input signal ;;; listen for the seven DTMF tones a = 0 ; clear all bits in the the detected_tone_mask [detected_tone_mask] = a .for tone = 0 .. NUM_TONES - 1 ; go through all seven tones dtmf_detect_tone tone ; and look for a signal at that frequency .endloop ;;; if we've heard something consistent for the entire debounce time, ;;; figure out which button we're hearing and set the output message. b = [detected_tone_mask] ; get the results of the search if (b != [`previous_tone_mask]) { ; are they different than last time? a = I(FS*OUTPUT_DEBOUNCE_TIME/1000) ; if so, get the max value for the debounce counter [`output_debounce_counter] = a ; and reset output_debounce_counter a = b ; set previous_tone_mask to detected_tone_mask, [`previous_tone_mask] = a ; so we can start counting down next time } else { ; if detected_tone_mask is the same as last time, a = [`output_debounce_counter] - I(1) ; decrement the debounce counter [`output_debounce_counter] = a ; and store it back if (a < 0) { ; have we seen a consistent tone mask for OUTPUT_DEBOUNCE_TIME ms? a = 0 ; if so, let's change the output message [$output] = a ; initialize the output message to zero (meaning "no button") .for button = 0 .. NUM_BUTTONS - 1 ; go through all twelve buttons if ([detected_tone_mask] == BUTTON_MASKS(button)) { ; did we hear just this button's two tones? a = I(button + 1) ; if so, set the output message to indicate this button is down [$output] = a ; store it back } .endloop } } } .endm ;;;----------------------------------------------------------------------------------- ;;; ;;; Routine: main ;;; ;;; Description: This instantiates a DTMF detector and hooks it up to a couple of ;;; IO ports. ;;; main: { dtmf_detect 0x400, 0x401 }