Skip to content

Commit 33d3974

Browse files
committed
Add embedded audio metadata support
add embedded metadata read/write support across audio formats by treating in-file tags as canonical and sidecar values as fallback for non-core keys. extend metadata handling with MP3 ID3v1 read/write, AIFF text chunk read/write, and OGG/FLAC comment parsing. remove stale sidecars after writing embedded-capable formats to keep metadata sources consistent. expand conversion routing with Java Sound audio transcoding targets (wav/aiff/au) and add mp4/mov audio-track remux to m4a when codec compatible. update tests and docs for new metadata and conversion behavior, bump version to 1.1.5, and package CHANGELOG.md into the jar.
1 parent bbe055f commit 33d3974

File tree

13 files changed

+1426
-43
lines changed

13 files changed

+1426
-43
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ 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.5] - 2026-03-17
9+
10+
### Added
11+
- Added MP3 embedded ID3v1 metadata reader/writer via [`Mp3Id3v1Tag`](src/main/java/me/tamkungz/codecmedia/internal/audio/mp3/Mp3Id3v1Tag.java) and integrated it into engine metadata read/write flow in [`StubCodecMediaEngine`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java).
12+
- Added OGG Vorbis/Opus comment metadata reader via [`OggParser.readCommentMetadata()`](src/main/java/me/tamkungz/codecmedia/internal/audio/ogg/OggParser.java) and integrated it into [`StubCodecMediaEngine.readMetadata()`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java).
13+
- Added FLAC Vorbis comment metadata reader via [`FlacParser.readVorbisCommentMetadata()`](src/main/java/me/tamkungz/codecmedia/internal/audio/flac/FlacParser.java) and integrated it into [`StubCodecMediaEngine.readMetadata()`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java).
14+
- Added Java Sound audio transcoder [`JavaSoundAudioTranscodeConverter`](src/main/java/me/tamkungz/codecmedia/internal/convert/JavaSoundAudioTranscodeConverter.java) for JDK-only target formats (`wav`, `aiff`, `au`).
15+
- Added AIFF embedded text metadata read/write support (`NAME`, `AUTH`, `(c) `, `ANNO`) via [`AiffParser.readTextMetadata()`](src/main/java/me/tamkungz/codecmedia/internal/audio/aiff/AiffParser.java) and [`AiffParser.writeTextMetadata()`](src/main/java/me/tamkungz/codecmedia/internal/audio/aiff/AiffParser.java).
16+
- Added MP4/MOV to M4A audio-track remux route via [`Mp4MovToM4aRemuxConverter`](src/main/java/me/tamkungz/codecmedia/internal/convert/Mp4MovToM4aRemuxConverter.java) in the video-to-audio conversion hub path.
17+
18+
### Changed
19+
- Updated audio-to-audio conversion routing in [`DefaultConversionHub`](src/main/java/me/tamkungz/codecmedia/internal/convert/DefaultConversionHub.java) to use Java Sound transcoding path (while preserving explicit WAV/PCM routing).
20+
- Expanded facade regression coverage in [`CodecMediaFacadeTest`](src/test/java/me/tamkungz/codecmedia/CodecMediaFacadeTest.java) for MP3 embedded metadata and OGG/FLAC metadata read paths.
21+
- Standardized metadata merge behavior in [`StubCodecMediaEngine.readMetadata()`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java) to treat embedded metadata as canonical and sidecar values as fallback (`putIfAbsent` for non-core keys).
22+
- Updated metadata write behavior in [`StubCodecMediaEngine.writeMetadata()`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java) so embedded-capable formats (`wav`, `mp3`, `aif`/`aiff`/`aifc`) write in-file metadata and remove stale sidecar files.
23+
- Expanded metadata regression coverage in [`CodecMediaFacadeTest`](src/test/java/me/tamkungz/codecmedia/CodecMediaFacadeTest.java) and parser-level AIFF coverage in [`AiffParserTest`](src/test/java/me/tamkungz/codecmedia/internal/audio/aiff/AiffParserTest.java).
24+
25+
### Verified
26+
- Confirmed metadata and conversion/facade behavior with `mvn -Dtest=CodecMediaFacadeTest test`.
27+
- Confirmed AIFF embedded metadata parser and facade flows with `mvn -Dtest=AiffParserTest,CodecMediaFacadeTest test`.
28+
829
## [1.1.4] - 2026-03-17
930

1031
### Added

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 persistence (embedded LIST/INFO for WAV and sidecar for non-WAV), audio extraction, playback workflow handling, and conversion routing.
9+
CodecMedia is a Java library for media probing, validation, metadata persistence (embedded WAV/AIFF/MP3 where supported with sidecar fallback for other formats), audio extraction, playback workflow handling, and conversion routing.
1010

1111

1212
<p align="center">
@@ -39,18 +39,18 @@ CodecMedia is a Java library for media probing, validation, metadata persistence
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 embedded WAV LIST/INFO support and sidecar persistence (`.codecmedia.properties`) for non-WAV inputs
42+
- Metadata read/write with embedded WAV LIST/INFO, AIFF text chunks (`NAME`/`AUTH`/`(c) `/`ANNO`), and MP3 ID3v1 support, plus sidecar persistence (`.codecmedia.properties`) for non-embedded fallback/compatibility paths
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`
4545
- Playback API with dry-run support, internal Java sampled backend for WAV/AIFF family, and optional desktop-open fallback
46-
- Conversion hub routing with explicit unsupported routes and a real `wav <-> pcm` path (`WAV -> PCM` data-chunk extraction, `PCM -> WAV` wrapping)
46+
- Conversion hub routing with explicit unsupported routes, a real `wav <-> pcm` path (`WAV -> PCM` data-chunk extraction, `PCM -> WAV` wrapping), JDK Java Sound audio targets (`wav`/`aiff`/`au`), and MP4/MOV audio-track remux to `m4a` when codec-compatible
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 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.
52+
- `readMetadata(input)`: returns derived probe metadata plus embedded metadata where supported (WAV LIST/INFO, AIFF text chunks, MP3 ID3v1, OGG/FLAC comments), then merges sidecar entries as fallback when present.
53+
- `writeMetadata(input, metadata)`: validates and writes embedded metadata where supported (WAV LIST/INFO, AIFF text chunks, MP3 ID3v1); for embedded-capable formats, stale sidecar files are removed; sidecar remains for compatibility/non-embedded paths.
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.
5656
- `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.
@@ -60,8 +60,8 @@ CodecMedia is a Java library for media probing, validation, metadata persistence
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` 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).
64-
- Audio-to-audio conversion is not implemented yet for general real transcode cases (for example `mp3 -> ogg`).
63+
- `readMetadata` supports embedded metadata for WAV (LIST/INFO), AIFF text chunks, MP3 (ID3v1), and OGG/FLAC comments; it is **not** a full embedded tag extractor for advanced tag families (for example ID3v2 APIC/album art).
64+
- Audio-to-audio conversion is partially implemented with JDK Java Sound targets (`wav`/`aiff`/`au`); general compressed-target transcode cases (for example `mp3 -> ogg`) are still not implemented.
6565
- The currently implemented audio route is `wav <-> pcm`:
6666
- `wav -> pcm`: extracts raw PCM payload from WAV `data` chunk
6767
- `pcm -> wav`: wraps PCM into PCM WAV container

pom.xml

Lines changed: 29 additions & 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.4</version>
10+
<version>1.1.5</version>
1111
<packaging>jar</packaging>
1212

1313
<name>CodecMedia</name>
@@ -115,6 +115,34 @@
115115
<build>
116116
<plugins>
117117

118+
<!-- Include top-level docs in JAR -->
119+
<plugin>
120+
<groupId>org.apache.maven.plugins</groupId>
121+
<artifactId>maven-resources-plugin</artifactId>
122+
<version>3.3.1</version>
123+
<executions>
124+
<execution>
125+
<id>copy-root-changelog-into-jar</id>
126+
<phase>process-resources</phase>
127+
<goals>
128+
<goal>copy-resources</goal>
129+
</goals>
130+
<configuration>
131+
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
132+
<resources>
133+
<resource>
134+
<directory>${project.basedir}</directory>
135+
<includes>
136+
<include>CHANGELOG.md</include>
137+
</includes>
138+
<filtering>false</filtering>
139+
</resource>
140+
</resources>
141+
</configuration>
142+
</execution>
143+
</executions>
144+
</plugin>
145+
118146
<!-- Compiler -->
119147
<plugin>
120148
<groupId>org.apache.maven.plugins</groupId>

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

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import me.tamkungz.codecmedia.internal.audio.flac.FlacParser;
3131
import me.tamkungz.codecmedia.internal.audio.flac.FlacProbeInfo;
3232
import me.tamkungz.codecmedia.internal.audio.mp3.Mp3Codec;
33+
import me.tamkungz.codecmedia.internal.audio.mp3.Mp3Id3v1Tag;
3334
import me.tamkungz.codecmedia.internal.audio.mp3.Mp3Parser;
3435
import me.tamkungz.codecmedia.internal.audio.mp3.Mp3ProbeInfo;
3536
import me.tamkungz.codecmedia.internal.audio.ogg.OggCodec;
@@ -592,16 +593,10 @@ public Metadata readMetadata(Path input) throws CodecMediaException {
592593
entries.put("mimeType", probe.mimeType());
593594
entries.put("extension", probe.extension());
594595
entries.put("mediaType", probe.mediaType().name());
595-
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-
}
596+
String normalizedExtension = normalizeExtension(probe.extension());
597+
Map<String, String> embeddedEntries = readEmbeddedMetadata(input, normalizedExtension);
598+
for (Map.Entry<String, String> entry : embeddedEntries.entrySet()) {
599+
entries.put(entry.getKey(), entry.getValue());
605600
}
606601

607602
Path sidecar = metadataSidecarPath(input);
@@ -613,7 +608,9 @@ public Metadata readMetadata(Path input) throws CodecMediaException {
613608
throw new CodecMediaException("Failed to read metadata sidecar: " + sidecar, e);
614609
}
615610
for (String key : properties.stringPropertyNames()) {
616-
entries.putIfAbsent(key, properties.getProperty(key));
611+
if (!isCoreMetadataKey(key)) {
612+
entries.putIfAbsent(key, properties.getProperty(key));
613+
}
617614
}
618615
}
619616

@@ -642,12 +639,37 @@ public void writeMetadata(Path input, Metadata metadata) throws CodecMediaExcept
642639
byte[] wavBytes = Files.readAllBytes(input);
643640
byte[] withMetadata = WavParser.writeInfoMetadata(wavBytes, metadata.entries());
644641
Files.write(input, withMetadata);
642+
deleteSidecarIfExists(input);
645643
return;
646644
} catch (IOException e) {
647645
throw new CodecMediaException("Failed to write WAV metadata: " + input, e);
648646
}
649647
}
650648

649+
if ("aif".equals(extension) || "aiff".equals(extension) || "aifc".equals(extension)) {
650+
try {
651+
byte[] aiffBytes = Files.readAllBytes(input);
652+
byte[] withMetadata = AiffParser.writeTextMetadata(aiffBytes, metadata.entries());
653+
Files.write(input, withMetadata);
654+
deleteSidecarIfExists(input);
655+
return;
656+
} catch (IOException e) {
657+
throw new CodecMediaException("Failed to write AIFF metadata: " + input, e);
658+
}
659+
}
660+
661+
if ("mp3".equals(extension)) {
662+
try {
663+
byte[] mp3Bytes = Files.readAllBytes(input);
664+
byte[] withTag = Mp3Id3v1Tag.write(mp3Bytes, metadata.entries());
665+
Files.write(input, withTag);
666+
deleteSidecarIfExists(input);
667+
return;
668+
} catch (IOException e) {
669+
throw new CodecMediaException("Failed to write MP3 metadata: " + input, e);
670+
}
671+
}
672+
651673
Path sidecar = metadataSidecarPath(input);
652674
Properties properties = new Properties();
653675
Map<String, String> sorted = new TreeMap<>(metadata.entries());
@@ -975,6 +997,30 @@ private static Path metadataSidecarPath(Path input) {
975997
return input.resolveSibling(input.getFileName() + ".codecmedia.properties");
976998
}
977999

1000+
private static void deleteSidecarIfExists(Path input) throws IOException {
1001+
Files.deleteIfExists(metadataSidecarPath(input));
1002+
}
1003+
1004+
private static boolean isCoreMetadataKey(String key) {
1005+
return "mimeType".equals(key) || "extension".equals(key) || "mediaType".equals(key);
1006+
}
1007+
1008+
private static Map<String, String> readEmbeddedMetadata(Path input, String normalizedExtension) throws CodecMediaException {
1009+
try {
1010+
byte[] bytes = Files.readAllBytes(input);
1011+
return switch (normalizedExtension) {
1012+
case "wav" -> WavParser.readInfoMetadata(bytes);
1013+
case "aif", "aiff", "aifc" -> AiffParser.readTextMetadata(bytes);
1014+
case "mp3" -> Mp3Id3v1Tag.read(bytes);
1015+
case "ogg" -> OggParser.readCommentMetadata(bytes);
1016+
case "flac" -> FlacParser.readVorbisCommentMetadata(bytes);
1017+
default -> Map.of();
1018+
};
1019+
} catch (IOException e) {
1020+
throw new CodecMediaException("Failed to read embedded metadata: " + input, e);
1021+
}
1022+
}
1023+
9781024
private static String normalizeExtension(String format) {
9791025
String value = format.trim().toLowerCase(Locale.ROOT);
9801026
return value.startsWith(".") ? value.substring(1) : value;

0 commit comments

Comments
 (0)