OK: I’ve taken davidc’s initialisation, and just the minimal IRQ and Event handlers - and I still get the double-detection. 
Here it is:
#include "adl_global.h"
/******************************************************************************
* Open-AT mandatory Variables
*****************************************************************************/
const u16 wm_apmCustomStackSize = 4096*3;
const u32 wm_apmIRQLowLevelStackSize = 4096;
const u32 wm_apmIRQHighLevelStackSize = 4096;
/******************************************************************************
* Globals
*****************************************************************************/
s32 voice_DtmfAudioStreamHandle; // Handle for the Audio Subscription
s32 voice_DTMFLowIrqHandle; // Handle for the Low-Level IRQ
s32 voice_DTMFHighIrqHandle; // Handle for the High-Level IRQ
adl_audioPostProcessedDecoder_t * postProcessedDTMF; // Post-processed DTMF
void * pDtmfStreamBuffer; // Audio stream buffer
ascii dtmfResultStr [300]; // For tracing
/******************************************************************************
* Audio Event Handler
*****************************************************************************/
void voice_dtmfAudioEventHandler( s32 audioHandle, adl_audioEvents_e audioEvent )
{
TRACE(( 1, "Audio Ev: %d", audioEvent ));
}
/******************************************************************************
* Low-Level IRQ Handler:
*
* Retrieves the post-processed DTMF Data, and checks the duration;
* If the duration is too short to be valid, it is ignored (returns FALSE);
* If the duration is valid, returns TRUE so that the High-Level IRQ Handler
* will be called to deal with it.
*****************************************************************************/
bool voice_DtmfLowIrqHandler( adl_irqID_e src, adl_irqNotificationLevel_e lvl, adl_irqEventData_t *Data )
{
TRACE(( 1, "LoIRQ: Ctx=%p Inst=%d Usr=%p Src=%p Buf=%p",
Data->Context,
Data->Instance,
Data->UserData,
Data->SourceData,
((adl_audioStream_t*)Data->SourceData)->DataBuffer
));
if( ((adl_audioPostProcessedDecoder_t*)((adl_audioStream_t*)Data->SourceData)->DataBuffer)->Duration > 4 )
{ // Valid DTMF duration;
// Cause ADL to call the High level IRQ handler.
return TRUE;
}
else
{ // Invalid DTMF duration (too short);
// Do not cause ADL to call the High level IRQ handler (ie, ignore it!)
// retrieve DTMF - just for the Trace
postProcessedDTMF = ((adl_audioPostProcessedDecoder_t*)((adl_audioStream_t*)Data->SourceData)->DataBuffer) ;
TRACE(( 1, " PP-DTMF: %c; dur=%d ms - IGNORE!",
postProcessedDTMF->DecodedDTMF,
(postProcessedDTMF->Duration*10)
));
return FALSE;
}
}
/******************************************************************************
* High-Level IRQ Handler:
*
* Outputs the received DTMF symbol in an AT Response
* (assumes that the Low-Level IRQ Hndler has already extracted the data)
*****************************************************************************/
bool voice_DtmfHighIrqHandler( adl_irqID_e src, adl_irqNotificationLevel_e lvl, adl_irqEventData_t *Data )
{
// retrieve DTMF
postProcessedDTMF = ((adl_audioPostProcessedDecoder_t*)((adl_audioStream_t*)Data->SourceData)->DataBuffer) ;
wm_sprintf ( dtmfResultStr, "Post-proc DTMF: '%c'; duration: %d ms\r\n", postProcessedDTMF->DecodedDTMF, (postProcessedDTMF->Duration*10) );
adl_atSendResponse( ADL_AT_RSP, dtmfResultStr );
TRACE(( 1, dtmfResultStr ));
return FALSE; // The return value is not used for a High-Level handler
}
/******************************************************************************
* Open-AT Entry Point
*
*****************************************************************************/
void adl_main ( adl_InitType_e initType )
{
s32 result; // For API return result codes
// Values for the Audio option Get/Set functions;
// they all require a type of s32.
s32 rawSampleDuration;
s32 dtmfBlankDuration;
s32 bufferSize;
TRACE(( 1, "\n" ));
TRACE(( 1, "Embedded Application: Main; Init=%d\n", initType ));
/* DTMF decoding test application */
TRACE(( 1, "DTMF Listener" ));
TRACE(( 1, __DATE__ ));
TRACE(( 1, __TIME__ ));
// Subscribe the IRQ Handlers
voice_DTMFLowIrqHandle = adl_irqSubscribe( voice_DtmfLowIrqHandler, ADL_IRQ_NOTIFY_LOW_LEVEL, ADL_IRQ_PRIORITY_LOW_LEVEL, 1 );
voice_DTMFHighIrqHandle = adl_irqSubscribe( voice_DtmfHighIrqHandler, ADL_IRQ_NOTIFY_HIGH_LEVEL, ADL_IRQ_PRIORITY_HIGH_LEVEL, 1 );
// Subscribe to Audio Stream
voice_DtmfAudioStreamHandle = adl_audioSubscribe
(
ADL_AUDIO_VOICE_CALL_RX, // Audio Stream ("Resource")
voice_dtmfAudioEventHandler, // Event Handler
ADL_AUDIO_RESOURCE_OPTION_FORBID_PREEMPTION // Options: this subscription "owns" the stream exclusively.
);
TRACE(( 1, "Audio Handle: %d", voice_DtmfAudioStreamHandle ));
// Get the DTMF sample size
result = adl_audioGetOption
(
voice_DtmfAudioStreamHandle, // The subscribed resource handle
ADL_AUDIO_RAW_DTMF_SAMPLE_DURATION, // The required parameter to get
&rawSampleDuration // Will receive the requested value
);
TRACE((1, "audioGetOption=%d, sample duration: %d", result, rawSampleDuration));
// Set the DTMF Blank Duration:
// The ADL User Guide says this must be a multiple of ADL_AUDIO_MAX_DTMF_PER_FRAME and
// the raw sample duration (obtained above). Unhelpfully, it doesn't give any advice on
// what multiple to use - ten is, apparently, arbitrary.
dtmfBlankDuration = rawSampleDuration * ADL_AUDIO_MAX_DTMF_PER_FRAME * 10;
result = adl_audioSetOption
(
voice_DtmfAudioStreamHandle, // The subscribed resource handle
ADL_AUDIO_DTMF_DETECT_BLANK_DURATION, // The required parameter to get
dtmfBlankDuration // The value to set
);
TRACE(( 1, "audioSetOption= %d, blank duration: %d", result, dtmfBlankDuration ));
// Get the required buffer size
result = adl_audioGetOption
(
voice_DtmfAudioStreamHandle,
ADL_AUDIO_DTMF_PROCESSED_STREAM_BUFFER_SIZE,
&bufferSize
);
TRACE(( 1, "audioGetOption=%d, buffer: %d", result, bufferSize ));
// Allocate a buffer of the indicated size
pDtmfStreamBuffer = adl_memGet( bufferSize );
TRACE(( 1, "pDtmfStreamBuffer=%p", pDtmfStreamBuffer ));
// Start listening to the Audio Stream
result = adl_audioStreamListen
(
voice_DtmfAudioStreamHandle, // The subscribed resource handle
ADL_AUDIO_DTMF, // The required audio format
voice_DTMFLowIrqHandle, // The Low-level IRQ handle
voice_DTMFHighIrqHandle, // The High-level IRQ handle
pDtmfStreamBuffer // The Stream Buffer;
// The post-processed DTMF data will be delivered
// to the IRQ Handlers in this buffer.
);
TRACE ((1, "audioStreamListen=%d", result));
// Received DTMF data will now be passed to the Low-Level IRQ Handler...
}
Note that the Low-Level IRQ Handler just checks for too-short DTMF durations, and only causes the High-Level IRQ Handler to be called when the duration is valid
The source file is also attached - as it can be rather hard to read in the little code window on this forum! 
adl_main.zip (2.22 KB)