;;;-----------------------------------------------------------------------------------
;;;
;;;  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
}
