Conversation
Scaffolding for the dtmf-io spec: three new subprojects join the build
and the BOM pins their coordinates, but no Java source lands here yet.
- settings.gradle.kts: add include(":dtmf-io"), include(":dtmf-io-wav"),
include(":dtmf-io-mp3"); update the top-of-file module-list comment
from four to seven subprojects and cite dtmf-io Requirement 1.1 next
to the foundation spec's Requirement 1.1.
- gradle/libs.versions.toml: add jlayer (1.0.1) and mp3spi (1.9.5.4)
under [versions] and [libraries] so :dtmf-io-mp3 can reference them
via libs.jlayer / libs.mp3spi — per the rule that all third-party
versions live in the catalog, never inline.
- dtmf-bom/build.gradle.kts: pin com.tino1b2be:dtmf-io:2.0.0,
dtmf-io-wav:2.0.0, dtmf-io-mp3:2.0.0 alongside the existing goertzel
and dtmf-core constraints (Req 2.1, 2.2 of the dtmf-io spec).
At this commit the three module directories are referenced by
settings.gradle.kts but contain no build.gradle.kts or Java source.
Gradle tolerates that — `./gradlew help` and `./gradlew tasks` still
configure cleanly — so this commit lands a coherent, buildable slice
of scaffolding before any production code.
Refs: .kiro/specs/dtmf-io/requirements.md (Req 1.1, 2.1, 2.2)
Land the core of the dtmf-io layer: the format-agnostic pull-based
`AudioSource` read contract, the `AudioSourceProvider` SPI, the
`AudioSources` facade that dispatches via `ServiceLoader`, and the
`DtmfFileDecoder` glue that opens a source, auto-resolves the detected
sample rate into the caller's `DtmfConfig`, and feeds samples through
`dtmf-core`. Plus the shared `RawPcmAudioSource` for raw PCM input and
an internal `SampleConversion` helper that normalises every supported
integer/float encoding to double[-1, 1].
Public surface (com.tino1b2be.dtmf.io):
- AudioSource — read(double[], int, int) / seek / close, normalised
double samples in [-1, 1] (Req 3.6, 3.7).
- AudioSourceProvider — canOpen(Path/InputStream, hint) returns a score
in {-1} ∪ [0, 100], plus priority() for tie-breaking (Req 4.2-4.8).
- AudioSources — static facade. Discovers providers via ServiceLoader,
wraps non-markable InputStreams in a 16KiB BufferedInputStream before
scoring (Req 5.12), picks strictly-highest-score provider, throws
UnsupportedAudioFormatException with diagnostics populated on miss
(Req 5.7, 5.8). File-not-found is reported as bare IOException
(Req 6.3) with the supplied Path preserved.
- DtmfFileDecoder — four overloads (Path/InputStream/URL + the explicit
AudioSource form). Rebuilds the caller's DtmfConfig with the source's
actual sampleRate() / channelCount() substituted in (Req 8.6, 17.1);
stereo sources are downmixed via ChannelMode.STEREO_DOWNMIX; always
closes the opened AudioSource, even on exception (Req 8.11, 11.3).
- PcmEncoding enum — SIGNED, UNSIGNED_OFFSET_BINARY, IEEE_FLOAT.
- UnsupportedAudioFormatException — checked IOException subtype with
attemptedFormat + message hint for the caller (Req 6.4, 6.5, 6.6).
- RawPcmAudioSource — pre-validated raw-PCM constructor + the common
`fromPcm16LittleEndian(byte[], sampleRate, channelCount)` factory.
Internal (com.tino1b2be.dtmf.io.internal):
- SampleConversion — PCM8/16/24/32/64 signed/unsigned LE/BE plus IEEE
float32/64 → double[-1, 1], with exact fixed-point normalisation
constants verified against Property 3.
- ProviderScore — immutable record pairing a provider with the score it
returned, used by AudioSources for tie-break ordering.
Tests (25 classes; includes 11 of the 13 dtmf-io property tests):
- Property 1: AudioSource.read contract
- Property 2: AudioSource close and seek lifecycle
- Property 3: RawPcmAudioSource sample normalization
- Property 4: RawPcmAudioSource constructor input validation
- Property 5: AudioSources scoring and selection
- Property 6: UnsupportedAudioFormatException diagnostics populated
- Property 7: Non-markable stream is wrapped before scoring
- Property 8: Provider close lifecycle and caller-stream ownership
- Property 9: DtmfFileDecoder close semantics
- Property 10: DtmfFileDecoder auto-resolve config preservation
- Property 11: DtmfFileDecoder channel-count and sample-rate rejection
Plus ErrorPathsTest (exception-type invariants), FixtureSmokeTest,
ModuleStructureTest, and scenario-based example tests for the public
types. Property tests are tagged
"// Feature: dtmf-io, Property N: <title>" per the project convention.
Integration-test source set wires :dtmf-io-wav and :dtmf-io-mp3 onto
the IT runtime classpath so ServiceLoader discovers both real
providers. IT classes (RegisteredFormatsIT, OpenWavIT, OpenMp3IT,
UnrecognizedInputIT, Mp3RoundTripIT) land in the same commit as the
:dtmf-io SPI they exercise — the providers themselves follow in the
next two commits but Gradle's `integrationTestRuntimeOnly` declarations
resolve lazily, so this commit's `:dtmf-io:build` passes.
Build shape: only runtime dep is `api(project(":dtmf-core"))` — zero
external codec libraries here by design (Req 1.2). Test classpath
adds :dtmf-io-wav so ErrorPathsTest can feed hand-built byte fixtures
through the real WavAudioSourceProvider.
Verification: `./gradlew :dtmf-io:check` is green (218 unit tests +
9 integration tests, 0 failures).
Refs: .kiro/specs/dtmf-io/requirements.md (Req 1.2, 3, 4, 5, 6, 7, 8,
11, 12, 17), .kiro/specs/dtmf-io/design.md (Properties 1-11)
Ship the WAV provider as a sibling module to dtmf-io. The parser is
clean-room per Req 9.12 — no javax.sound.sampled on the decode path,
no third-party RIFF library — so the entire WAV format surface (RIFF,
RF64, WAVEFORMATEXTENSIBLE, WAVE_FORMAT_PCM/IEEE_FLOAT, PCM8/16/24/32,
IEEE float32/64, stereo interleaving) is handled by fresh code under
`com.tino1b2be.dtmf.io.wav.internal`.
Public surface (com.tino1b2be.dtmf.io.wav):
- WavAudioSourceProvider — implements AudioSourceProvider. Header
detection via canOpen(InputStream, hint) returns 100 on a strict
"RIFF....WAVE" or "RF64....WAVE" match and -1 otherwise
(Req 9.5-9.6). Opens a WavAudioSource backed by WavSampleReader.
- WavAudioSource — implements AudioSource. Parses the fmt/data chunks
eagerly at open time (Req 9.7), then streams samples via
WavSampleReader. Stereo is surfaced via channelCount(); callers that
want a mono mix go through DtmfFileDecoder's auto-downmix.
Internal (com.tino1b2be.dtmf.io.wav.internal):
- RiffReader + RiffChunk — chunk walker with RF64 ds64 handling for
>4GB files (Req 9.10). Enforces WORD-alignment and catches the
Microsoft "garbage after data" quirk.
- WaveFormat — parsed fmt chunk. Resolves WAVEFORMATEXTENSIBLE's
SubFormat GUID into the canonical WAVE_FORMAT_PCM /
WAVE_FORMAT_IEEE_FLOAT codes. Rejects μ-law (0x0007), A-law (0x0006),
MS-ADPCM (0x0002), IMA-ADPCM (0x0011), and every other compressed
format with UnsupportedAudioFormatException carrying the rejected
format tag as attemptedFormat diagnostics (Req 9.8, 12.2).
- WavSampleReader — pulls bytes from the data chunk and normalises via
dtmf-io's SampleConversion helper (no re-implementation here).
SPI registration: META-INF/services/com.tino1b2be.dtmf.io.
AudioSourceProvider lists `com.tino1b2be.dtmf.io.wav.
WavAudioSourceProvider`, so ServiceLoader discovers the provider as
soon as this module is on the runtime classpath.
Tests (7 classes; includes 2 of the 13 dtmf-io property tests):
- Property 12: WAV provider header detection (strict magic match)
- Property 13: WAV round-trip preserves samples within quantization
tolerance (DtmfGenerator → WavEncoder test helper → WavAudioSource
→ DtmfDecoder, checks key sequence recovered bit-exact for PCM16
and within ULP tolerance for float32/64)
- WavAudioSourceProviderTest — nested scenarios covering
ChunkWalking, Extensible (WAVEFORMATEXTENSIBLE GUID dispatch),
Rf64 (>4GB ds64 chunk), StereoInterleaving, StructuralDefects
(missing fmt/data, truncated header, bogus chunk size),
UnsupportedFormatTag (μ-law, A-law, ADPCM rejection).
- RiffReaderTest and RiffChunkTest — chunk-walker unit coverage.
- BuildShapeTest — pins no external runtime deps on this module's
classpath (Req 1.3 invariant guard).
WavEncoder lives in src/test and is deliberately kept test-only: it
exists to synthesize canonical WAV bytes for round-trip tests, not as
a public encoding API (production WAV encoding is out of scope, Req
18.1).
Build shape: only runtime dep is `api(project(":dtmf-io"))`. Zero
external codec libraries, zero javax.sound.sampled in src/main —
verified by BuildShapeTest.
Verification: `./gradlew :dtmf-io-wav:build` is green (63 unit tests,
0 failures).
Refs: .kiro/specs/dtmf-io/requirements.md (Req 1.3, 9, 12.2),
.kiro/specs/dtmf-io/design.md (Properties 12-13)
Ship the MP3 provider. Unlike WAV this one is not clean-room: MP3
framing, Huffman decoding, and all the Layer III arithmetic is handled
by JLayer (javazoom:jlayer:1.0.1) with mp3spi
(com.googlecode.soundlibs:mp3spi:1.9.5.4) bridging JLayer into
javax.sound.sampled so the provider can decode via
AudioSystem.getAudioInputStream(...). Those are the only two external
runtime dependencies this module adds (Req 1.4); a third is a
requirements regression.
Public surface (com.tino1b2be.dtmf.io.mp3):
- Mp3AudioSourceProvider — implements AudioSourceProvider. Header
detection via Mp3HeaderScanner looks for the 11-bit frame sync,
validates MPEG version / Layer III, and tolerates ID3v2 tags at the
start of the stream by skipping over them before sync-word scanning
(Req 10.5). Returns a score of 90 on a confirmed Layer III sync
(strictly below WAV's 100 so a "WAVE" magic always wins when both
providers are on the classpath), and -1 on non-MP3 bytes.
- Mp3AudioSource — implements AudioSource. Opens via
AudioSystem.getAudioInputStream(InputStream), decodes frames on
demand into the caller's double[] buffer, and normalises 16-bit PCM
through dtmf-io's SampleConversion helper. Stereo is preserved via
channelCount(); DtmfFileDecoder's auto-downmix handles the
mono-mix path. seek() advances by skip-and-discard because JLayer's
AudioInputStream does not support random access on MP3 frames.
Internal (com.tino1b2be.dtmf.io.mp3.internal):
- Mp3HeaderScanner — pre-SPI content sniffer. Skips ID3v2 header
(0x49 0x44 0x33 + size synchsafe), then walks up to 4KiB of bytes
searching for a valid MPEG Layer III frame header. Distinguishes
Layer III from Layers I/II so MP2 and MP1 don't false-positive.
Consumed by both canOpen() scoring and open() pre-flight.
SPI registration: META-INF/services/com.tino1b2be.dtmf.io.
AudioSourceProvider lists `com.tino1b2be.dtmf.io.mp3.
Mp3AudioSourceProvider`, so ServiceLoader picks up the provider once
this module is on the runtime classpath.
Shared test fixtures: the three MP3 binaries live in
dtmf-core/src/integrationTest/resources/samples/ and are aliased onto
this module's test classpath under shared-samples/ via a
`processTestResources` copy in build.gradle.kts (Req 15.4). No MP3
bytes are duplicated under this module's source tree.
Tests (3 classes; BuildShapeTest is the deps-pin guard):
- Mp3HeaderScannerTest — nested scenarios covering Id3v2 (tag skip),
InputStreamContract (mark/reset behaviour), NonAudio (jazz.mp3 is
positive, a PNG header is negative, a WAV header is negative),
SyncWordLayerDispatch (MP1 / MP2 / MP3 Layer bits — only MP3
returns > 0).
- Mp3AudioSourceTest — opens the shared 12345678.mp3 fixture end to
end, decodes through Mp3AudioSource, and asserts sampleRate() /
channelCount() match the known fixture metadata plus a smoke
DtmfDecoder pass that recovers all 8 keys.
- BuildShapeTest — pins the dependency surface: only jlayer + mp3spi
from Maven Central, plus :dtmf-io and :dtmf-core, nothing else.
No dtmf-io Property test lives here — the two MP3-related properties
(fixture integrity, Mp3RoundTripIT) are integration-scoped and live
in :dtmf-io's integrationTest source set.
Build shape: `api(project(":dtmf-io"))`, `implementation(libs.jlayer)`,
`implementation(libs.mp3spi)`. Two external runtime deps total, both
pinned via the version catalog.
Verification: `./gradlew :dtmf-io-mp3:build` is green (23 unit tests,
0 failures).
Refs: .kiro/specs/dtmf-io/requirements.md (Req 1.4, 10, 15.4),
.kiro/specs/dtmf-io/design.md (MP3 provider section)
Document the `processTestResources` alias wired into `dtmf-io-mp3/build.gradle.kts` that copies three MP3 fixtures (`12345678.mp3`, `jazz.mp3`, `stereo.mp3`) from `dtmf-core/src/integrationTest/resources/samples/` onto the `dtmf-io-mp3` test classpath under `shared-samples/`. The section covers: - Which three files are aliased and why each exists (a DTMF sequence, a non-DTMF negative anchor, a stereo positive). - How unit tests load them (classpath resources under `/shared-samples/…`). - The fail-fast behaviour: the task's inputs are named explicitly, so a rename or removal in `dtmf-core` surfaces as a missing-input build failure rather than a silently dropped fixture. This is the docs side of the Stage 1 / Stage 7 work in the dtmf-io spec (Req 15.4) — everything else about the aliasing was already captured as comments in the relevant `build.gradle.kts` files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
dtmf-io: file-I/O layer with pull-based AudioSource SPI
Ships the
dtmf-iospec in full — three new published modules that add file-I/O to the v2 foundation without touchingdtmf-core. The core library still takes PCM asdouble[]/short[]/float[]/int[]; everything file-, URL-, orInputStream-shaped lives here.What lands
Three new Gradle subprojects joining the build under the root aggregator:
dtmf-iocom.tino1b2be:dtmf-io:2.0.0:dtmf-coreonlydtmf-io-wavcom.tino1b2be:dtmf-io-wav:2.0.0:dtmf-ioonly (clean-room RIFF parser)dtmf-io-mp3com.tino1b2be:dtmf-io-mp3:2.0.0:dtmf-io,jlayer:1.0.1,mp3spi:1.9.5.4The BOM (
dtmf-bom:2.0.0) pins all three alongside the existinggoertzelanddtmf-coreconstraints.Design in one screen
AudioSource.int read(double[] buf, int off, int len)— same shape asjava.io.InputStream/AudioInputStream, but samples are always normaliseddoublein[-1, 1]. Matches the contractdtmf-core'sDtmfStream.SampleSourcealready speaks.AudioSourceProviderimplementations register viaMETA-INF/services.AudioSourcesdiscovers them throughServiceLoader, scores each one on header bytes (not file extension), picks the strictly-highest score (tie-broken by declaredpriority()), and delegates. Adding FLAC/OGG/Opus later is a new module, not a patch todtmf-io.DtmfFileDecoderreads the source's actual sample rate and channel count, rebuilds the caller'sDtmfConfigwith those values substituted in, and decodes. Callers don't pre-inspect headers.IOExceptionat the I/O boundary. Unlikedtmf-core(which forbids checked exceptions),dtmf-ioreintroducesthrows IOExceptionplus a dedicatedUnsupportedAudioFormatExceptionsubtype so callers can tell "disk broken" apart from "bytes not valid WAV/MP3".Commits (narrative order)
build: register dtmf-io* modules in settings, catalog, and BOM— pure scaffolding (settings.gradle.kts, gradle/libs.versions.toml, dtmf-bom)../gradlew helpstill configures cleanly.feat(dtmf-io): add pull-based AudioSource SPI and DtmfFileDecoder— the SPI heart:AudioSource,AudioSourceProvider,AudioSources,DtmfFileDecoder,RawPcmAudioSource,PcmEncoding,UnsupportedAudioFormatException, plus the internalSampleConversionhelper. 25 test classes, 11 of the 13 design properties.feat(dtmf-io-wav): add clean-room WAV AudioSourceProvider— RIFF/RF64 parser,WAVEFORMATEXTENSIBLEGUID dispatch, rejects μ-law/A-law/ADPCM. Nojavax.sound.sampledin main sources (Req 9.12). Properties 12–13.feat(dtmf-io-mp3): add MP3 AudioSourceProvider via JLayer + mp3spi— the only module with external codec deps. Handles ID3v2-prefixed streams and distinguishes MP3 Layer III from MP1/MP2 in the scanner. Shared MP3 fixtures aliased fromdtmf-core/src/integrationTest/resources/samples/viaprocessTestResources.docs: describe shared MP3 test-fixture aliasing in CONTRIBUTING— documents the aliasing scheme and its fail-fast rename behaviour.Each commit leaves the tree in a buildable state.
Testing
:dtmf-io:integrationTestwith both real providers on the runtime classpath —RegisteredFormatsIT,OpenWavIT,OpenMp3IT,UnrecognizedInputIT, andMp3RoundTripIT(opt-in, needs LAME).// Feature: dtmf-io, Property N: <title>tag comment for traceability.Final verification on this branch:
./gradlew clean build :dtmf-io:integrationTest— BUILD SUCCESSFUL.Out of scope
CLI, GUI, microphone capture, FLAC/OGG/Opus/AAC providers, production WAV/MP3 encoding, Maven Central signing/publishing. Those stay deferred to later specs (Req 18).
Refs:
.kiro/specs/dtmf-io/{requirements,design,tasks}.md