Class Mp3AudioSourceProvider

java.lang.Object
com.tino1b2be.dtmf.io.mp3.Mp3AudioSourceProvider
All Implemented Interfaces:
com.tino1b2be.dtmf.io.AudioSourceProvider

public final class Mp3AudioSourceProvider extends Object implements 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 in dtmf-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:
  1. 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).
  2. Hand a recognised input to AudioSystem.getAudioInputStream(java.io.File) / AudioSystem.getAudioInputStream(InputStream) and wrap the resulting AudioInputStream in Mp3AudioSource (Requirement 10.7), translating UnsupportedAudioFileException into the UnsupportedAudioFormatException the dtmf-io error-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:
  • Constructor Details

    • Mp3AudioSourceProvider

      public Mp3AudioSourceProvider()
      ServiceLoader requires 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

      public String formatName()
      Specified by:
      formatName in interface com.tino1b2be.dtmf.io.AudioSourceProvider
    • priority

      public int priority()
      Specified by:
      priority in interface com.tino1b2be.dtmf.io.AudioSourceProvider
    • canOpen

      public int canOpen(Path path) throws IOException

      Opens a BufferedInputStream of BUFFER_SIZE_BYTES bytes on top of Files.newInputStream(Path, java.nio.file.OpenOption...), runs Mp3HeaderScanner.scanForSyncLayer3(InputStream, int) with the SYNC_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 is SCORE_MATCH on a sync-word hit and -1 otherwise.

      Any IOException raised while opening or reading the file propagates to the caller; AudioSources catches such exceptions during scoring, records the provider as having returned -1, logs a warning, and continues (Requirement 5.9).

      Specified by:
      canOpen in interface com.tino1b2be.dtmf.io.AudioSourceProvider
      Parameters:
      path - file to score; must be non-null
      Returns:
      SCORE_MATCH on a Layer III sync-word match, -1 otherwise
      Throws:
      NullPointerException - if path is null
      IOException - on I/O failure while reading the file
    • canOpen

      public int canOpen(InputStream stream, String hint) throws IOException

      When stream supports mark/reset, marks up to MARK_READ_LIMIT bytes, runs Mp3HeaderScanner.scanForSyncLayer3(InputStream, int), and resets the stream in a finally block so the caller's 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); the AudioSources facade wraps such streams in a BufferedInputStream before scoring (Requirement 5.12), so in normal use this branch is defensive against direct callers of the provider.

      Specified by:
      canOpen in interface com.tino1b2be.dtmf.io.AudioSourceProvider
      Parameters:
      stream - the stream to score; must be non-null
      hint - optional caller-supplied hint; may be null and is ignored by this provider (content-based detection)
      Returns:
      SCORE_MATCH on a Layer III sync-word match, -1 otherwise
      Throws:
      NullPointerException - if stream is null
      IOException - on I/O failure while reading the header prefix
    • open

      public com.tino1b2be.dtmf.io.AudioSource open(Path path) throws IOException

      Delegates to AudioSystem.getAudioInputStream(java.io.File) which, thanks to mp3spi 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 and returns a ready-to-read source (Requirement 10.7). The returned source owns the AudioInputStream and closes it on AudioSource.close().

      An UnsupportedAudioFileException from AudioSystem means mp3spi did 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 into UnsupportedAudioFormatException identifying the cause (Requirements 10.8, 12.4), so callers can distinguish a format-level rejection from the generic IOException that covers real I/O failures.

      Specified by:
      open in interface com.tino1b2be.dtmf.io.AudioSourceProvider
      Parameters:
      path - file to open; must be non-null
      Returns:
      an opened Mp3AudioSource
      Throws:
      NullPointerException - if path is null
      com.tino1b2be.dtmf.io.UnsupportedAudioFormatException - if the file's sync word matched but mp3spi could not decode it (Requirement 10.8)
      IOException - on any other I/O failure
    • open

      public com.tino1b2be.dtmf.io.AudioSource open(InputStream stream, String hint) throws IOException

      If stream does not support mark/reset it is wrapped internally in a BufferedInputStream of BUFFER_SIZE_BYTES bytes, because AudioSystem.getAudioInputStream(InputStream) requires a markable stream for the format-probing rewinds that mp3spi performs. The wrapper is internal to this method; it is not the caller's stream, and while the returned Mp3AudioSource may legitimately close it (via the AudioInputStream chain) the caller's own stream is never closed by this provider or by the returned source (Requirement 4.10).

      An UnsupportedAudioFileException from AudioSystem is translated into UnsupportedAudioFormatException with the cause preserved (Requirements 10.8, 12.4); real I/O failures propagate as plain IOException.

      Specified by:
      open in interface com.tino1b2be.dtmf.io.AudioSourceProvider
      Parameters:
      stream - caller-supplied stream to open; must be non-null
      hint - optional caller-supplied hint; may be null and is ignored by this provider
      Returns:
      an opened Mp3AudioSource
      Throws:
      NullPointerException - if stream is null
      com.tino1b2be.dtmf.io.UnsupportedAudioFormatException - if the stream's sync word matched but mp3spi could not decode it (Requirement 10.8)
      IOException - on any other I/O failure