Class Mp3AudioSourceProvider
- All Implemented Interfaces:
com.tino1b2be.dtmf.io.AudioSourceProvider
AudioSourceProvider implementation for MPEG-1 and MPEG-2 Layer
III (.mp3) content. This is the single public entry point of
the dtmf-io-mp3 module, discovered by
ServiceLoader through the
META-INF/services/com.tino1b2be.dtmf.io.AudioSourceProvider
registration (Requirement 10.2) and normally invoked indirectly via
AudioSources.open(...).
Design
Unlike the clean-room RIFF parser indtmf-io-wav, this provider
is a thin veneer over javax.sound.sampled. Decoding is
delegated to the two external libraries declared in the module's
build.gradle.kts (Requirement 1.4, 10.7):
javazoom:jlayer:1.0.1 contributes the MPEG Layer III decoder
and com.googlecode.soundlibs:mp3spi:1.9.5.4 registers the
FormatConversionProvider that plugs JLayer into
AudioSystem's ServiceLoader. All this
provider does is:
- Decide whether an input looks like an MPEG Layer III stream, by
delegating content detection to
Mp3HeaderScanner.scanForSyncLayer3(InputStream, int)(Requirements 10.5, 10.6). - Hand a recognised input to
AudioSystem.getAudioInputStream(java.io.File)/AudioSystem.getAudioInputStream(InputStream)and wrap the resultingAudioInputStreaminMp3AudioSource(Requirement 10.7), translatingUnsupportedAudioFileExceptioninto theUnsupportedAudioFormatExceptionthedtmf-ioerror-handling contract mandates (Requirements 10.8, 12.4).
Detection (canOpen)
Content-based detection (Requirement 4.4, 4.5) runs the shared
Mp3HeaderScanner: skip any leading ID3v2 tag, then scan up to
10_240 post-tag bytes for an MPEG sync word whose version
field is not reserved and whose layer field is Layer III. A match
returns a score of 90; anything else returns -1
(Requirements 10.5, 10.6).
The score is deliberately lower than WAV's 100 because the
MP3 sync word is an 11-bit pattern (plus a small amount of contextual
validation) rather than a full magic number, and false positives on
pathological inputs are possible. Against a real .mp3 file the
heuristic is robust; against random bytes a WAV-or-nothing fallback
through the provider chain is the right outcome.
Path overload
Opens a short-lived BufferedInputStream of 16 KiB
sitting on top of Files.newInputStream(Path, java.nio.file.OpenOption...),
via try-with-resources so the file handle is closed before
canOpen returns. The buffer size gives
Mp3HeaderScanner a few reads' worth of headroom over the
10 (ID3v2 header) + 10_240 (sync scan) = 10_250-byte budget it
walks through before giving up.
InputStream overload
Marks the caller's stream at 10_250 bytes (ID3v2 header +
post-tag scan budget), runs the scanner, and resets the stream in a
finally block so the caller's read position is restored on
both the success and failure paths (Requirement 4.6). Non-markable
streams are declined with -1 without consuming any bytes
(Requirement 4.7); AudioSources.open(InputStream, String)
wraps non-markable inputs in a BufferedInputStream before
scoring (Requirement 5.12), so this branch mostly protects direct
callers of the provider.
Full parse (open)
Path overload
Delegates to AudioSystem.getAudioInputStream(java.io.File)
which, thanks to mp3spi being on the runtime classpath,
accepts MP3 files and returns an AudioInputStream in the
MP3's native format. Mp3AudioSource.wrap(AudioInputStream)
then converts that stream to PCM16 LE before handing the source back
to the caller. The returned source owns the AudioInputStream
and closes it on AudioSource.close().
InputStream overload
Same pipeline, but on an AudioInputStream obtained from
AudioSystem.getAudioInputStream(InputStream). That overload
requires a mark/reset-capable stream (it rewinds by a
few KiB while probing format), so non-markable inputs are wrapped in
a BufferedInputStream of 16 KiB here — the
wrapper is internal and therefore something the returned
AudioSource may close; it is not the caller's
stream. The caller's stream itself is never closed
by this provider or by the returned source (Requirement 4.10); closure
remains the caller's responsibility.
Translation of UnsupportedAudioFileException
When mp3spi recognises the container but cannot decode it
— for example an MPEG Layer I or Layer II payload, or a
structurally-malformed frame sequence — it throws
UnsupportedAudioFileException. Both open(...)
overloads catch that and rethrow it as
UnsupportedAudioFormatException, preserving the original
exception as the cause so the full diagnostic chain remains available
to the caller (Requirements 10.8, 12.4, 12.5). Real I/O failures
— the disk is broken, the stream is cut mid-read —
propagate as plain IOException, distinct from the
format-level failure above, so callers can handle the two cases
separately.
Thread safety
Instances are stateless;formatName(), priority(),
every canOpen(...) overload, and every open(...)
overload can be called concurrently from multiple threads. The
AudioSource instances returned from open(...) carry
their own lifecycle and are not thread-safe — see
Mp3AudioSource.- Since:
- 2.1.0
- See Also:
-
Mp3AudioSourceMp3HeaderScannerAudioSourceProvider
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionintcanOpen(InputStream stream, String hint) intcom.tino1b2be.dtmf.io.AudioSourceopen(InputStream stream, String hint) com.tino1b2be.dtmf.io.AudioSourceintpriority()
-
Constructor Details
-
Mp3AudioSourceProvider
public Mp3AudioSourceProvider()ServiceLoaderrequires a public no-argument constructor (Requirement 4.1). Instances are stateless and cheap to construct; the provider caches no data across calls.
-
-
Method Details
-
formatName
- Specified by:
formatNamein interfacecom.tino1b2be.dtmf.io.AudioSourceProvider
-
priority
public int priority()- Specified by:
priorityin interfacecom.tino1b2be.dtmf.io.AudioSourceProvider
-
canOpen
Opens a
BufferedInputStreamofBUFFER_SIZE_BYTESbytes on top ofFiles.newInputStream(Path, java.nio.file.OpenOption...), runsMp3HeaderScanner.scanForSyncLayer3(InputStream, int)with theSYNC_SCAN_BUDGET_BYTES-byte post-tag budget, and closes the stream via try-with-resources before returning (Requirements 10.5, 10.6). The return value isSCORE_MATCHon a sync-word hit and-1otherwise.Any
IOExceptionraised while opening or reading the file propagates to the caller;AudioSourcescatches such exceptions during scoring, records the provider as having returned-1, logs a warning, and continues (Requirement 5.9).- Specified by:
canOpenin interfacecom.tino1b2be.dtmf.io.AudioSourceProvider- Parameters:
path- file to score; must be non-null- Returns:
SCORE_MATCHon a Layer III sync-word match,-1otherwise- Throws:
NullPointerException- ifpathisnullIOException- on I/O failure while reading the file
-
canOpen
When
streamsupportsmark/reset, marks up toMARK_READ_LIMITbytes, runsMp3HeaderScanner.scanForSyncLayer3(InputStream, int), and resets the stream in afinallyblock so the caller's position is restored on both the success and failure paths (Requirement 4.6).Non-markable streams are declined with
-1without consuming any bytes (Requirement 4.7); theAudioSourcesfacade wraps such streams in aBufferedInputStreambefore scoring (Requirement 5.12), so in normal use this branch is defensive against direct callers of the provider.- Specified by:
canOpenin interfacecom.tino1b2be.dtmf.io.AudioSourceProvider- Parameters:
stream- the stream to score; must be non-nullhint- optional caller-supplied hint; may benulland is ignored by this provider (content-based detection)- Returns:
SCORE_MATCHon a Layer III sync-word match,-1otherwise- Throws:
NullPointerException- ifstreamisnullIOException- on I/O failure while reading the header prefix
-
open
Delegates to
AudioSystem.getAudioInputStream(java.io.File)which, thanks tomp3spion the runtime classpath, accepts MP3 files and returns anAudioInputStreamin the MP3's native format.Mp3AudioSource.wrap(AudioInputStream)then converts that stream to PCM16 LE and returns a ready-to-read source (Requirement 10.7). The returned source owns theAudioInputStreamand closes it onAudioSource.close().An
UnsupportedAudioFileExceptionfromAudioSystemmeansmp3spidid not recognise the file as a decodable MPEG Layer III container — in practice a Layer I/II payload or a structurally malformed MP3 whose sync-word prefix nonetheless convinced the scanner. That case is translated intoUnsupportedAudioFormatExceptionidentifying the cause (Requirements 10.8, 12.4), so callers can distinguish a format-level rejection from the genericIOExceptionthat covers real I/O failures.- Specified by:
openin interfacecom.tino1b2be.dtmf.io.AudioSourceProvider- Parameters:
path- file to open; must be non-null- Returns:
- an opened
Mp3AudioSource - Throws:
NullPointerException- ifpathisnullcom.tino1b2be.dtmf.io.UnsupportedAudioFormatException- if the file's sync word matched butmp3spicould not decode it (Requirement 10.8)IOException- on any other I/O failure
-
open
If
streamdoes not supportmark/resetit is wrapped internally in aBufferedInputStreamofBUFFER_SIZE_BYTESbytes, becauseAudioSystem.getAudioInputStream(InputStream)requires a markable stream for the format-probing rewinds thatmp3spiperforms. The wrapper is internal to this method; it is not the caller's stream, and while the returnedMp3AudioSourcemay legitimately close it (via theAudioInputStreamchain) the caller's own stream is never closed by this provider or by the returned source (Requirement 4.10).An
UnsupportedAudioFileExceptionfromAudioSystemis translated intoUnsupportedAudioFormatExceptionwith the cause preserved (Requirements 10.8, 12.4); real I/O failures propagate as plainIOException.- Specified by:
openin interfacecom.tino1b2be.dtmf.io.AudioSourceProvider- Parameters:
stream- caller-supplied stream to open; must be non-nullhint- optional caller-supplied hint; may benulland is ignored by this provider- Returns:
- an opened
Mp3AudioSource - Throws:
NullPointerException- ifstreamisnullcom.tino1b2be.dtmf.io.UnsupportedAudioFormatException- if the stream's sync word matched butmp3spicould not decode it (Requirement 10.8)IOException- on any other I/O failure
-