Skip to content

Commit bbe055f

Browse files
committed
Add wav info metadata and sampled playback
read and write embedded RIFF LIST/INFO tags for wav inputs in the engine metadata flow, while keeping sidecar metadata for non-wav files. this improves metadata persistence consistency for wav files. route wav/aiff-family playback through an internal java sampled backend before optional desktop-open fallback to reduce reliance on external app integration and improve playback behavior. add regression tests for wav metadata round-trip and playback routing, and update docs/changelog plus project version to 1.1.4
1 parent 21cfc5f commit bbe055f

File tree

8 files changed

+630
-12
lines changed

8 files changed

+630
-12
lines changed

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,32 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [1.1.4] - 2026-03-16
8+
## [1.1.4] - 2026-03-17
9+
10+
### Added
11+
- Added WAV embedded metadata round-trip handling (RIFF `LIST/INFO`) in [`WavParser`](src/main/java/me/tamkungz/codecmedia/internal/audio/wav/WavParser.java) and engine metadata flow in [`StubCodecMediaEngine`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java).
12+
- Added internal Java sampled playback route (`java-sampled`) for WAV/AIFF-family playback in [`StubCodecMediaEngine`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java).
13+
- Added playback routing regression coverage in [`StubCodecMediaEnginePlaybackRoutingTest`](src/test/java/me/tamkungz/codecmedia/internal/StubCodecMediaEnginePlaybackRoutingTest.java).
914

1015
### Changed
1116
- Replaced temporary WAV/PCM stub converter with production path via [`WavPcmConverter`](src/main/java/me/tamkungz/codecmedia/internal/convert/WavPcmConverter.java), including real `wav -> pcm` data-chunk extraction and `pcm -> wav` container wrapping.
1217
- Updated conversion hub wiring in [`DefaultConversionHub`](src/main/java/me/tamkungz/codecmedia/internal/convert/DefaultConversionHub.java) to route WAV/PCM through the renamed real converter.
1318
- Added preset-driven PCM->WAV parameter parsing in [`WavPcmConverter.parsePcmWavParams()`](src/main/java/me/tamkungz/codecmedia/internal/convert/WavPcmConverter.java) supporting `sr=`, `ch=`, and `bits=`.
1419
- Updated facade regression behavior in [`CodecMediaFacadeTest`](src/test/java/me/tamkungz/codecmedia/CodecMediaFacadeTest.java) to assert real re-encode behavior and preset-based output stream properties for WAV/PCM route.
20+
- Updated metadata behavior to use embedded WAV `LIST/INFO` read/write for WAV inputs while keeping sidecar (`.codecmedia.properties`) persistence for non-WAV formats in [`StubCodecMediaEngine`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java).
21+
- Updated playback behavior in [`CodecMediaEngine.play()`](src/main/java/me/tamkungz/codecmedia/CodecMediaEngine.java) implementation path to prioritize internal Java sampled playback for WAV/AIFF family before desktop-open fallback.
1522

1623
### Fixed
1724
- Added defensive bounds checks for little-endian reads in [`WavPcmConverter.readLeInt()`](src/main/java/me/tamkungz/codecmedia/internal/convert/WavPcmConverter.java) and [`WavPcmConverter.readLeUnsignedShort()`](src/main/java/me/tamkungz/codecmedia/internal/convert/WavPcmConverter.java).
1825
- Added WAV `fmt ` validation before payload extraction in [`WavPcmConverter.extractWavDataChunk()`](src/main/java/me/tamkungz/codecmedia/internal/convert/WavPcmConverter.java), rejecting non-PCM WAV payload extraction.
1926
- Hardened chunk traversal and container construction against arithmetic overflow in [`WavPcmConverter.extractWavDataChunk()`](src/main/java/me/tamkungz/codecmedia/internal/convert/WavPcmConverter.java) and [`WavPcmConverter.wrapPcmAsWav()`](src/main/java/me/tamkungz/codecmedia/internal/convert/WavPcmConverter.java).
27+
- Fixed WAV metadata flow so write/read operations no longer rely on sidecar-only behavior for WAV files, improving in-file metadata persistence consistency.
28+
- Fixed playback routing so WAV/AIFF-family inputs no longer depend solely on desktop integration when internal sampled playback is available.
2029

2130
### Verified
2231
- Confirmed facade regression coverage with `mvn -Dtest=CodecMediaFacadeTest test`.
32+
- Confirmed WAV metadata parser behavior with `mvn -Dtest=WavParserTest test`.
33+
- Confirmed engine metadata and playback routing behavior with `mvn -Dtest=CodecMediaFacadeTest,StubCodecMediaEnginePlaybackRoutingTest test`.
2334

2435
## [1.1.3] - 2026-03-16
2536

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![Java](https://img.shields.io/badge/Java-17%2B-ED8B00?logo=openjdk&logoColor=white)](https://openjdk.org/)
77
[![Maven](https://img.shields.io/badge/Maven-3.9%2B-C71A36?logo=apachemaven&logoColor=white)](https://maven.apache.org/)
88

9-
CodecMedia is a Java library for media probing, validation, metadata sidecar persistence, audio extraction, playback workflow simulation, and conversion routing.
9+
CodecMedia is a Java library for media probing, validation, metadata persistence (embedded LIST/INFO for WAV and sidecar for non-WAV), audio extraction, playback workflow handling, and conversion routing.
1010

1111

1212
<p align="center">
@@ -39,28 +39,28 @@ CodecMedia is a Java library for media probing, validation, metadata sidecar per
3939
- WebM (EBML container parsing)
4040
- Validation with size limits and strict parser checks for MP3/OGG/WAV/AIFF/FLAC/PNG/JPEG/WebP/BMP/TIFF/HEIC/HEIF/AVIF/MOV/MP4/WebM
4141
- MOV/MP4/WebM probe tags now include richer video metadata when present (for example `displayAspectRatio`, `bitDepth`, `videoBitrateKbps`, `audioBitrateKbps`)
42-
- Metadata read/write with sidecar persistence (`.codecmedia.properties`)
42+
- Metadata read/write with embedded WAV LIST/INFO support and sidecar persistence (`.codecmedia.properties`) for non-WAV inputs
4343
- In-Java extraction and conversion file operations
4444
- Image-to-image conversion in Java for: `png`, `jpg`/`jpeg`, `webp`, `bmp`, `tif`/`tiff`, `heic`/`heif`/`avif`
45-
- Playback API with dry-run support and optional desktop-open backend
45+
- Playback API with dry-run support, internal Java sampled backend for WAV/AIFF family, and optional desktop-open fallback
4646
- Conversion hub routing with explicit unsupported routes and a real `wav <-> pcm` path (`WAV -> PCM` data-chunk extraction, `PCM -> WAV` wrapping)
4747

4848
## API Behavior Summary
4949

5050
- `get(input)`: alias of `probe(input)` for convenience.
5151
- `probe(input)`: detects media/container characteristics and returns technical stream info for supported formats.
52-
- `readMetadata(input)`: returns derived probe metadata plus sidecar entries when present.
53-
- `writeMetadata(input, metadata)`: validates and writes metadata to a sidecar properties file next to the input.
52+
- `readMetadata(input)`: returns derived probe metadata plus embedded LIST/INFO tags for WAV, and sidecar entries for non-WAV when present.
53+
- `writeMetadata(input, metadata)`: validates and writes embedded LIST/INFO tags for WAV, and writes a sidecar properties file next to non-WAV inputs.
5454
- `extractAudio(input, outputDir, options)`: validates audio input and writes extracted output into `outputDir`.
5555
- `convert(input, output, options)`: performs routed conversion behavior and enforces `overwrite` handling.
56-
- `play(input, options)`: supports dry-run playback and optional system default app launch.
56+
- `play(input, options)`: supports dry-run playback, routes WAV/AIFF-family playback through an internal Java sampled backend, and falls back to optional system default app launch.
5757
- `validate(input, options)`: validates existence, max size, and optional strict parser-level checks.
5858

5959
## Notes and Limitations
6060

6161
- Current probing focuses on **technical media info** (mime/type/streams/basic tags).
6262
- Probe routing now performs a lightweight header-prefix sniff before full decode to reduce unnecessary full-file reads for clearly unsupported/unknown inputs.
63-
- `readMetadata` currently uses sidecar metadata persistence; it is **not** a full embedded tag extractor (for example ID3 album art/APIC).
63+
- `readMetadata` supports embedded LIST/INFO for WAV plus sidecar metadata persistence for non-WAV inputs; it is **not** a full embedded tag extractor for other formats (for example ID3 album art/APIC).
6464
- Audio-to-audio conversion is not implemented yet for general real transcode cases (for example `mp3 -> ogg`).
6565
- The currently implemented audio route is `wav <-> pcm`:
6666
- `wav -> pcm`: extracts raw PCM payload from WAV `data` chunk

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<groupId>me.tamkungz.codecmedia</groupId>
99
<artifactId>codecmedia</artifactId>
10-
<version>1.1.3</version>
10+
<version>1.1.4</version>
1111
<packaging>jar</packaging>
1212

1313
<name>CodecMedia</name>

src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
import java.util.Properties;
1515
import java.util.TreeMap;
1616

17+
import javax.sound.sampled.AudioInputStream;
18+
import javax.sound.sampled.AudioSystem;
19+
import javax.sound.sampled.Clip;
20+
import javax.sound.sampled.LineEvent;
21+
import javax.sound.sampled.LineUnavailableException;
22+
import javax.sound.sampled.UnsupportedAudioFileException;
23+
1724
import me.tamkungz.codecmedia.CodecMediaEngine;
1825
import me.tamkungz.codecmedia.CodecMediaException;
1926
import me.tamkungz.codecmedia.internal.audio.aiff.AiffCodec;
@@ -76,7 +83,23 @@ public final class StubCodecMediaEngine implements CodecMediaEngine {
7683

7784
private static final long STRICT_VALIDATION_MAX_BYTES = 32L * 1024L * 1024L;
7885
private static final int PROBE_PREFIX_BYTES = 128 * 1024;
79-
private final ConversionHub conversionHub = new DefaultConversionHub();
86+
private final ConversionHub conversionHub;
87+
private final JavaSampledPlaybackBackend javaSampledPlaybackBackend;
88+
private final DesktopPlaybackBackend desktopPlaybackBackend;
89+
90+
public StubCodecMediaEngine() {
91+
this(new DefaultConversionHub(), new JdkJavaSampledPlaybackBackend(), new AwtDesktopPlaybackBackend());
92+
}
93+
94+
StubCodecMediaEngine(
95+
ConversionHub conversionHub,
96+
JavaSampledPlaybackBackend javaSampledPlaybackBackend,
97+
DesktopPlaybackBackend desktopPlaybackBackend
98+
) {
99+
this.conversionHub = conversionHub;
100+
this.javaSampledPlaybackBackend = javaSampledPlaybackBackend;
101+
this.desktopPlaybackBackend = desktopPlaybackBackend;
102+
}
80103

81104
@Override
82105
public ProbeResult get(Path input) throws CodecMediaException {
@@ -570,6 +593,17 @@ public Metadata readMetadata(Path input) throws CodecMediaException {
570593
entries.put("extension", probe.extension());
571594
entries.put("mediaType", probe.mediaType().name());
572595

596+
if ("wav".equals(normalizeExtension(probe.extension()))) {
597+
try {
598+
Map<String, String> wavInfo = WavParser.readInfoMetadata(Files.readAllBytes(input));
599+
for (Map.Entry<String, String> infoEntry : wavInfo.entrySet()) {
600+
entries.putIfAbsent(infoEntry.getKey(), infoEntry.getValue());
601+
}
602+
} catch (IOException e) {
603+
throw new CodecMediaException("Failed to read WAV metadata: " + input, e);
604+
}
605+
}
606+
573607
Path sidecar = metadataSidecarPath(input);
574608
if (Files.exists(sidecar)) {
575609
Properties properties = new Properties();
@@ -602,6 +636,18 @@ public void writeMetadata(Path input, Metadata metadata) throws CodecMediaExcept
602636
}
603637
}
604638

639+
String extension = normalizeExtension(extractExtension(input));
640+
if ("wav".equals(extension)) {
641+
try {
642+
byte[] wavBytes = Files.readAllBytes(input);
643+
byte[] withMetadata = WavParser.writeInfoMetadata(wavBytes, metadata.entries());
644+
Files.write(input, withMetadata);
645+
return;
646+
} catch (IOException e) {
647+
throw new CodecMediaException("Failed to write WAV metadata: " + input, e);
648+
}
649+
}
650+
605651
Path sidecar = metadataSidecarPath(input);
606652
Properties properties = new Properties();
607653
Map<String, String> sorted = new TreeMap<>(metadata.entries());
@@ -699,15 +745,38 @@ public PlaybackResult play(Path input, PlaybackOptions options) throws CodecMedi
699745
return new PlaybackResult(true, "dry-run", probe.mediaType(), "Playback simulation successful");
700746
}
701747

702-
if (effective.allowExternalApp() && Desktop.isDesktopSupported()) {
748+
CodecMediaException javaSampledFailure = null;
749+
if (probe.mediaType() == MediaType.AUDIO && supportsJavaSampledExtension(probe.extension())) {
703750
try {
704-
Desktop.getDesktop().open(input.toFile());
751+
javaSampledPlaybackBackend.play(input);
752+
return new PlaybackResult(true, "java-sampled", probe.mediaType(), "Started playback using javax.sound.sampled");
753+
} catch (CodecMediaException e) {
754+
javaSampledFailure = e;
755+
}
756+
}
757+
758+
if (effective.allowExternalApp() && desktopPlaybackBackend.isSupported()) {
759+
try {
760+
desktopPlaybackBackend.open(input);
705761
return new PlaybackResult(true, "desktop-open", probe.mediaType(), "Opened with system default application");
706762
} catch (IOException | RuntimeException e) {
763+
if (javaSampledFailure != null) {
764+
throw new CodecMediaException(
765+
"Failed to start internal Java sampled playback and external application fallback: " + input,
766+
e
767+
);
768+
}
707769
throw new CodecMediaException("Failed to open media with system player/viewer: " + input, e);
708770
}
709771
}
710772

773+
if (javaSampledFailure != null) {
774+
throw new CodecMediaException(
775+
"No playback backend available after Java sampled attempt: " + javaSampledFailure.getMessage(),
776+
javaSampledFailure
777+
);
778+
}
779+
711780
throw new CodecMediaException("No playback backend available. Try dryRun=true or allowExternalApp=true");
712781
}
713782

@@ -916,6 +985,14 @@ private static String baseName(String fileName) {
916985
return dot > 0 ? fileName.substring(0, dot) : fileName;
917986
}
918987

988+
private static boolean supportsJavaSampledExtension(String extension) {
989+
String normalized = normalizeExtension(extension);
990+
return "wav".equals(normalized)
991+
|| "aif".equals(normalized)
992+
|| "aiff".equals(normalized)
993+
|| "aifc".equals(normalized);
994+
}
995+
919996
private static MediaType mediaTypeByExtension(String extension) {
920997
return switch (normalizeExtension(extension)) {
921998
case "mp3", "ogg", "wav", "aif", "aiff", "aifc", "pcm", "m4a", "aac", "flac" -> MediaType.AUDIO;
@@ -924,4 +1001,44 @@ private static MediaType mediaTypeByExtension(String extension) {
9241001
default -> MediaType.UNKNOWN;
9251002
};
9261003
}
1004+
1005+
interface JavaSampledPlaybackBackend {
1006+
void play(Path input) throws CodecMediaException;
1007+
}
1008+
1009+
interface DesktopPlaybackBackend {
1010+
boolean isSupported();
1011+
1012+
void open(Path input) throws IOException;
1013+
}
1014+
1015+
private static final class JdkJavaSampledPlaybackBackend implements JavaSampledPlaybackBackend {
1016+
@Override
1017+
public void play(Path input) throws CodecMediaException {
1018+
try (AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(input.toFile())) {
1019+
Clip clip = AudioSystem.getClip();
1020+
clip.open(audioInputStream);
1021+
clip.addLineListener(event -> {
1022+
if (event.getType() == LineEvent.Type.STOP || event.getType() == LineEvent.Type.CLOSE) {
1023+
clip.close();
1024+
}
1025+
});
1026+
clip.start();
1027+
} catch (UnsupportedAudioFileException | LineUnavailableException | IOException | RuntimeException e) {
1028+
throw new CodecMediaException("Java sampled playback failed for " + input + ": " + e.getMessage(), e);
1029+
}
1030+
}
1031+
}
1032+
1033+
private static final class AwtDesktopPlaybackBackend implements DesktopPlaybackBackend {
1034+
@Override
1035+
public boolean isSupported() {
1036+
return Desktop.isDesktopSupported();
1037+
}
1038+
1039+
@Override
1040+
public void open(Path input) throws IOException {
1041+
Desktop.getDesktop().open(input.toFile());
1042+
}
1043+
}
9271044
}

0 commit comments

Comments
 (0)