This document is the normative behavioural contract for the DTMF-Decoder library. Every requirement is testable and has at least one unit or property test validating it. Where a requirement is backed by a property test, the test file carries a tag comment of the form // Feature: dtmf-v2-foundation, Property N: <title> and the property's Javadoc names the requirement it validates.
The reference standards are ITU-T Q.23 (tone generation) and ITU-T Q.24 (detector behaviour, including standard twist). Copies of both standards sit in docs/standards/ so the library can be read and understood offline.
Requirements use EARS phrasing — each acceptance criterion is of the form WHEN / WHILE / IF / WHERE / THE <system> SHALL …. This keeps conditions unambiguous and makes criteria directly machine-checkable.
- DTMF: Dual-Tone Multi-Frequency signalling per ITU-T Q.23/Q.24. Sixteen key symbols (
0–9,A–D,*,#) each encoded as the sum of one low-group tone (697, 770, 852, 941 Hz) and one high-group tone (1209, 1336, 1477, 1633 Hz). - DTMF_Decoder: The public batch API that converts an array of PCM samples into a list of detected DTMF tones. Implemented by
com.tino1b2be.dtmf.DtmfDecoder. - DTMF_Detector: The public push-based streaming API that accepts audio chunks and invokes a callback on each detected tone. Implemented by
com.tino1b2be.dtmf.DtmfDetector. Not thread-safe; one instance per stream. - DTMF_Stream: The public pull-based streaming API built on top of
DTMF_Detector, exposing anIterator<DtmfTone>over a source stream. - DTMF_Generator: The public generation API that produces PCM samples for a given DTMF key sequence. Implemented by
com.tino1b2be.dtmf.DtmfGenerator. - DtmfTone: The immutable record representing one detected or generated tone. Fields:
key(char),startSample(long),endSample(long),sampleRate(int),confidence(double in [0.0, 1.0]),channel(int, 0 for mono or left, 1 for right). - DtmfConfig: The public configuration type with six common knobs (sample rate, analysis block size, minimum tone duration, minimum gap duration, detection threshold, channel mode) plus an
advanced()builder exposing window function, twist tolerances, and confirmation-frame count. - GoertzelBank: The public low-level API in the
goertzelmodule that runs a set of Goertzel filters over a signal. Exposed to callers who want to build their own detectors. - Goertzel_Filter: A single Goertzel-algorithm filter targeting one frequency bin.
- Analysis_Block: A contiguous window of audio samples over which Goertzel energies are computed. Default length is chosen so bin width is approximately 50 Hz independent of sample rate.
- Standard_Twist: Per ITU-T Q.24, the permitted power ratio between high-group and low-group tones: +4 dB forward twist (high group louder than low group), −8 dB reverse twist (low group louder than high group).
- Tone_End_Event: The moment at which
DTMF_Detectoremits aDtmfToneto its callback, defined as the firstAnalysis_Blockthat does not confirm the currently active tone. - PCM16, PCM24, PCM32, PCM_Float, PCM_Double: Linear pulse-code-modulated sample formats with, respectively, 16-bit signed integer, 24-bit signed integer packed into
int, 32-bit signed integer, 32-bit IEEE float, and 64-bit IEEE double samples. Normalised range for float/double is[-1.0, 1.0]. - Supported_Sample_Rate: One of the exact values 8000, 16000, 44100, or 48000 Hz.
- BOM: Bill of Materials; a Maven/Gradle artifact with
pompackaging that pins versions of a set of coordinated artifacts.
User Story: As a maintainer, I want a standard Gradle multi-module layout, so that each published artifact has isolated dependencies and tests.
- The project SHALL be a Gradle multi-module build containing exactly five modules named
goertzel,dtmf-core,dtmf-benchmarks,dtmf-bom, and the root aggregator. - The project SHALL include a committed Gradle wrapper (
gradlew,gradlew.bat,gradle/wrapper/gradle-wrapper.jar,gradle/wrapper/gradle-wrapper.properties). - The project SHALL target Java 17 as both source and target bytecode level for every module.
- The
goertzelmodule SHALL declare zero runtime dependencies outside the Java standard library. - The
dtmf-coremodule SHALL declaregoertzelas its only runtime dependency outside the Java standard library. - The
dtmf-benchmarksmodule SHALL NOT be published and SHALL be marked as such in its build configuration. - The
dtmf-bommodule SHALL be packaged as a Maven BOM (pompackaging) listinggoertzelanddtmf-coreat version2.0.0. - The project SHALL place Java sources under each module's
src/main/javaand tests undersrc/test/java. - WHEN
./gradlew buildis executed on a clean checkout, the project SHALL compile every module and run every module's tests without requiring any pre-installed dependency outside a JDK 17.
User Story: As a library consumer, I want stable, conventional coordinates and packages, so that I can declare and import the library predictably.
- The
goertzelmodule SHALL publish under the Maven coordinatescom.tino1b2be:goertzel:2.0.0. - The
dtmf-coremodule SHALL publish under the Maven coordinatescom.tino1b2be:dtmf-core:2.0.0. - The
dtmf-bommodule SHALL publish under the Maven coordinatescom.tino1b2be:dtmf-bom:2.0.0. - The
goertzelmodule SHALL place all production classes under the Java package rootcom.tino1b2be.goertzel. - The
dtmf-coremodule SHALL place all production classes under the Java package rootcom.tino1b2be.dtmf. - The project SHALL NOT expose any class under the legacy package
com.tino1b2be.dtmfdecoder.
User Story: As a library consumer, I want to know exactly which sample rates are supported, so that I can reject or resample audio before calling the decoder.
- The DTMF_Decoder SHALL treat 8000, 16000, 44100, and 48000 Hz as Supported_Sample_Rate values.
- WHEN a caller constructs a
DtmfConfigwith a sample rate equal to a Supported_Sample_Rate, the DTMF_Decoder SHALL accept the configuration. - IF a caller constructs a
DtmfConfigvia the standard factory methods with a sample rate that is not a Supported_Sample_Rate, THEN the DTMF_Decoder SHALL throwIllegalArgumentExceptionidentifying the unsupported rate and listing the Supported_Sample_Rate set. - WHERE the advanced configuration API (
DtmfConfig.advanced()) is used, the DTMF_Decoder SHALL accept any integer sample rate in the closed range [4000, 192000] Hz. - The DTMF_Decoder SHALL size each Analysis_Block so that the Goertzel bin width is within the closed range [40, 60] Hz for every Supported_Sample_Rate.
- IF a caller supplies samples whose count implies a sample rate different from the one declared in
DtmfConfig, THEN the DTMF_Decoder SHALL process the samples using the declared sample rate without inspecting or inferring the actual rate.
User Story: As a library consumer, I want to pass audio in whichever PCM format I already have, so that I do not have to write normalisation code before decoding.
- The DTMF_Decoder SHALL expose a batch entry point accepting
double[]samples normalised to the range[-1.0, 1.0]. - The DTMF_Decoder SHALL expose a batch entry point accepting
short[]samples representing signed PCM16. - The DTMF_Decoder SHALL expose a batch entry point accepting
float[]samples normalised to the range[-1.0, 1.0]. - The DTMF_Decoder SHALL expose a batch entry point accepting
int[]samples representing signed PCM32, with a documented helper for callers supplying PCM24 packed into the low 24 bits. - WHEN a
short[]input is received, the DTMF_Decoder SHALL internally convert samples todoubleby dividing by 32768.0 before analysis. - WHEN a
float[]input is received, the DTMF_Decoder SHALL internally convert samples todoubleby widening cast without scaling. - WHEN an
int[]input is received, the DTMF_Decoder SHALL internally convert samples todoubleby dividing by 2147483648.0 before analysis. - IF any input sample array is
null, THEN the DTMF_Decoder SHALL throwNullPointerExceptionidentifying the parameter name. - The DTMF_Detector SHALL expose streaming
process(chunk)overloads mirroring the batch format set (double[],short[],float[],int[]).
User Story: As a library consumer, I want a single-call decode method that returns every tone in a buffer, so that I can process pre-recorded audio without managing state.
- The DTMF_Decoder SHALL expose a method with signature equivalent to
List<DtmfTone> decode(double[] samples, DtmfConfig config). - The DTMF_Decoder SHALL return detected tones in non-decreasing order of
startSample. - The DTMF_Decoder SHALL populate every returned
DtmfTonewithstartSampleandendSampleindices referring to positions within the input array, withstartSample >= 0,endSample > startSample, andendSample <= samples.length. - The DTMF_Decoder SHALL populate every returned
DtmfTonewith aconfidencevalue in the closed range[0.0, 1.0]. - The DTMF_Decoder SHALL populate every returned
DtmfTonewith asampleRateequal to the sample rate declared in the suppliedDtmfConfig. - The DTMF_Decoder SHALL populate every returned
DtmfTonewith akeyvalue drawn from the set{'0'..'9', 'A', 'B', 'C', 'D', '*', '#'}. - WHEN the input array is empty, the DTMF_Decoder SHALL return an empty list without throwing.
User Story: As a library consumer processing audio as it arrives, I want a push-based detector that emits tones via a callback, so that I do not have to buffer the whole stream.
- The DTMF_Detector SHALL expose a method equivalent to
void onTone(Consumer<DtmfTone> callback)that registers a single callback, replacing any previously registered callback. - The DTMF_Detector SHALL expose a method equivalent to
void process(double[] chunk)that consumes a variable-length chunk of samples. - The DTMF_Detector SHALL expose a method equivalent to
void flush()that forces any tone in progress to be emitted if its duration already satisfies the configured minimum. - WHEN a tone ends, the DTMF_Detector SHALL invoke the registered callback exactly once for that tone, at the Tone_End_Event, before returning from the
processorflushcall that detected the end. - The DTMF_Detector SHALL NOT invoke the registered callback at tone start.
- IF
processis called concurrently from more than one thread on the sameDTMF_Detectorinstance, THEN the behaviour is undefined and the DTMF_Detector SHALL document that instances are not thread-safe. - WHEN
processis called repeatedly with chunks whose concatenation equals a single bufferB, the DTMF_Detector SHALL emit the same sequence of tones (same keys, same order, samestartSample/endSamplerelative to the cumulative sample count) as a singleprocess(B)call on a fresh detector. - The DTMF_Detector SHALL populate
startSampleandendSampleon emitted tones as cumulative sample indices measured from the first sample ever passed to that detector instance.
User Story: As a library consumer who prefers pull-style iteration, I want an Iterator<DtmfTone> over a source stream, so that I can integrate with code that expects iterators.
- The DTMF_Stream SHALL expose a static factory method that constructs a
DtmfStreamfrom an audio sample source and aDtmfConfig. - The DTMF_Stream SHALL implement
Iterator<DtmfTone>. - WHEN
hasNext()is called, the DTMF_Stream SHALL pull and analyse audio from its source until either a tone is available or the source is exhausted. - WHEN the source is exhausted and no tone is pending, the DTMF_Stream SHALL return
falsefromhasNext(). - The DTMF_Stream SHALL produce tones in the same order and with the same field values as DTMF_Detector configured identically and fed the same samples.
User Story: As a library consumer, I want a small default configuration for common cases and an advanced configuration for specialist tuning, so that I do not have to learn DSP to use the library.
- The DtmfConfig SHALL expose exactly six common knobs: sample rate, analysis block size, minimum tone duration, minimum gap duration, detection threshold, and channel mode.
- The DtmfConfig SHALL expose a nested advanced builder accessible via
DtmfConfig.advanced()that additionally exposes: window function, forward twist tolerance in dB, reverse twist tolerance in dB, and confirmation-frame count. - The DtmfConfig SHALL expose a static factory
defaults()returning a configuration suitable for 8 kHz mono telephony audio with Standard_Twist tolerances. - The DtmfConfig SHALL expose a static factory
forTelephony()returning a configuration with Standard_Twist tolerances and a 40 ms minimum tone duration. - The DtmfConfig SHALL expose a static factory
forVoip()returning a configuration tuned for packet-loss concealment artifacts, with a larger confirmation-frame count thanforTelephony(). - The DtmfConfig SHALL expose a static factory
forNoisyAudio()returning a configuration with a stricter detection threshold and more confirmation frames thanforTelephony(). - The DtmfConfig SHALL be immutable once constructed.
- IF a caller attempts to construct a
DtmfConfigwith a minimum tone duration less than 10 ms, THEN the DtmfConfig SHALL throwIllegalArgumentException.
User Story: As a telephony engineer, I want the decoder to reject tone pairs that violate standard twist limits, so that signalling artifacts outside Q.24 bounds are not mistaken for valid keys.
- The DTMF_Decoder SHALL compute twist as
10 * log10(high_group_energy / low_group_energy)for each candidate tone. - WHEN
DtmfConfig.forTelephony()orDtmfConfig.defaults()is used, the DTMF_Decoder SHALL reject candidate tones whose twist exceeds +4 dB (forward twist) or is less than −8 dB (reverse twist). - WHERE the advanced API configures custom twist tolerances, the DTMF_Decoder SHALL apply exactly those tolerances and no others.
- WHEN a candidate tone is rejected for twist, the DTMF_Decoder SHALL NOT emit a
DtmfTonefor that candidate.
User Story: As a performance-conscious consumer, I want a single well-characterised detection backend, so that library behaviour is predictable and cheap.
- The DTMF_Decoder SHALL use the
goertzelmodule's Goertzel_Filter implementation as its sole detection backend in production code paths. - The DTMF_Decoder SHALL NOT expose an FFT-based detection backend in its public API.
- The
dtmf-benchmarksmodule MAY include an FFT-based implementation solely for comparative JMH benchmarks. - The
goertzelmodule SHALL expose aGoertzelBankpublic API that allows callers outside DTMF use cases to run arbitrary target frequencies over arbitrary sample arrays.
User Story: As a library consumer and test author, I want to generate PCM audio for a DTMF key sequence, so that I can produce test vectors and play back tones.
- The DTMF_Generator SHALL expose a method equivalent to
double[] generate(String sequence, DtmfConfig config)that produces normalised samples in[-1.0, 1.0]. - The DTMF_Generator SHALL produce tones using the frequency pairs defined by ITU-T Q.23 for each key in
{'0'..'9', 'A', 'B', 'C', 'D', '*', '#'}. - IF the input sequence contains a character outside the accepted key set, THEN the DTMF_Generator SHALL throw
IllegalArgumentExceptionidentifying the offending character and its position. - The DTMF_Generator SHALL produce each tone at the minimum tone duration configured in the supplied
DtmfConfig. - The DTMF_Generator SHALL produce silence equal to the minimum gap duration configured in the supplied
DtmfConfigbetween consecutive tones. - WHEN a sequence is passed through
DTMF_Generator.generateand then throughDTMF_Decoder.decodeusing a configuration with matching sample rate and Standard_Twist tolerances, the DTMF_Decoder SHALL return a list ofDtmfTonewhosekeyfield, read in order, equals the input sequence (round-trip property).
User Story: As a telephony engineer, I want measurable accuracy targets, so that 'good enough' is a defined bar rather than a judgement call.
- The DTMF_Decoder SHALL achieve a correct-detection rate of at least 99.5% over a test corpus of Q.23-compliant generated tones at least 40 ms in duration, at Standard_Twist, across every Supported_Sample_Rate.
- The DTMF_Decoder SHALL emit zero false-positive tones when decoding a pure silence input of any length up to 60 seconds at every Supported_Sample_Rate.
- The DTMF_Decoder SHALL emit at most one false-positive tone per hour of white-noise-only input at a signal-to-noise ratio of 15 dB or better, evaluated at 8 kHz.
- WHEN detecting tones at least 40 ms long generated by
DTMF_Generator, the DTMF_Decoder SHALL producestartSamplevalues within ±1 Analysis_Block length of the true tone start, andendSamplevalues within ±1 Analysis_Block length of the true tone end.
User Story: As a library consumer processing recorded calls, I want to decode mono or stereo audio and either tag tones per channel or downmix first, so that I can choose the behaviour that matches my pipeline.
- The DtmfConfig SHALL expose a channel mode value with three cases:
MONO,STEREO_INDEPENDENT, andSTEREO_DOWNMIX. - WHEN channel mode is
MONO, the DTMF_Decoder SHALL treat the input array as a single channel and tag every emittedDtmfTonewithchannel = 0. - WHEN channel mode is
STEREO_INDEPENDENT, the DTMF_Decoder SHALL treat the input array as interleaved left/right PCM, decode each channel independently, and tag emitted tones withchannel = 0(left) orchannel = 1(right). - WHEN channel mode is
STEREO_DOWNMIX, the DTMF_Decoder SHALL average left and right samples into a single mono stream before detection and tag every emittedDtmfTonewithchannel = 0. - IF channel mode is
STEREO_INDEPENDENTorSTEREO_DOWNMIXand the input sample count is odd, THEN the DTMF_Decoder SHALL throwIllegalArgumentExceptionidentifying the malformed input.
User Story: As a library consumer reporting timestamps to humans, I want easy conversion from sample indices to wall-clock durations, so that I do not have to do the arithmetic myself.
- The DtmfTone SHALL expose a method equivalent to
Duration startTime()returning the start position as ajava.time.Durationcomputed fromstartSampleandsampleRate. - The DtmfTone SHALL expose a method equivalent to
Duration endTime()returning the end position as ajava.time.Durationcomputed fromendSampleandsampleRate. - The DtmfTone SHALL expose a method equivalent to
Duration duration()returningendTime().minus(startTime()).
User Story: As a maintainer tracking performance over time, I want a JMH benchmark module that exercises Goertzel and optional FFT baselines, so that regressions are detectable.
- The
dtmf-benchmarksmodule SHALL apply the JMH Gradle plugin and compile under Java 17. - The
dtmf-benchmarksmodule SHALL include at least one benchmark class exercisingDTMF_Decoder.decodeacross every Supported_Sample_Rate. - The
dtmf-benchmarksmodule SHALL include at least one benchmark class exercising rawGoertzelBankthroughput. - The project SHALL NOT enforce a hard performance target in v2 foundation; benchmark results SHALL be published as a baseline for future regression tracking.
User Story: As a library consumer, I want invalid inputs to fail fast with clear messages, so that I can debug quickly.
- IF any public API parameter is
nullwhere non-null is required, THEN the project SHALL throwNullPointerExceptionwith a message identifying the parameter name. - IF a caller passes a numeric argument outside its documented domain (negative sample rate, negative duration, threshold outside
[0.0, 1.0]), THEN the project SHALL throwIllegalArgumentExceptionwith a message identifying the parameter and the expected domain. - The project SHALL NOT throw checked exceptions from any public API.
- The project SHALL NOT retain references to caller-supplied sample arrays after the method that received them returns (except where the API is explicitly streaming and documents otherwise).
Every requirement with a non-trivial acceptance criterion is backed by a jqwik property test in dtmf-core/src/test/java/com/tino1b2be/dtmf/. The full list of 21 correctness properties is in docs/design.md. Each property test file tags itself with a comment:
// Feature: dtmf-v2-foundation, Property N: <title>and the Javadoc names the validated requirement explicitly. This closes the loop between the prose requirement, the encoded property, and the runtime check.
The following are deliberately not part of this library and are tracked for later work:
- File I/O — no WAV, MP3, or OGG readers. Callers supply PCM samples as
double[],short[],float[], orint[]. - CLI — no command-line interface module.
- GUI — no Swing, AWT, JavaFX, or applet code.
- Microphone capture — no real-time audio input.
- Android — no Android-specific code or dependencies.
- Maven Central publishing — no signing, no release workflows.
- v1 API compatibility — the v1 API under
com.tino1b2be.dtmfdecoderhas been removed without a shim.