3838 * The following specifications are used for the implementation:
3939 * </p>
4040 * <ul>
41+ * <li>FLAC: <a href="https://www.rfc-editor.org/rfc/rfc9639">RFC 9639</a></li>
4142 * <li>Opus: All specs can be found at <a href="https://opus-codec.org/docs/">
4243 * https://opus-codec.org/docs/</a>.
4344 * <a href="https://datatracker.ietf.org/doc/html/rfc7845.html">RFC7845</a>
5051 * @author tobigr
5152 */
5253public class OggFromWebMWriter implements Closeable {
54+ private static final String TAG = OggFromWebMWriter .class .getSimpleName ();
55+
56+ /**
57+ * No flags set.
58+ */
5359 private static final byte FLAG_UNSET = 0x00 ;
54- //private static final byte FLAG_CONTINUED = 0x01;
60+ /**
61+ * The packet is continued from previous the previous page.
62+ */
63+ private static final byte FLAG_CONTINUED = 0x01 ;
64+ /**
65+ * BOS (beginning of stream).
66+ */
5567 private static final byte FLAG_FIRST = 0x02 ;
56- private static final byte FLAG_LAST = 0x04 ;
68+ /**
69+ * EOS (end of stream).
70+ */
71+ private static final byte FLAG_LAST = 0x04 ;;
5772
5873 private static final byte HEADER_CHECKSUM_OFFSET = 22 ;
5974 private static final byte HEADER_SIZE = 27 ;
@@ -66,6 +81,12 @@ public class OggFromWebMWriter implements Closeable {
6681 */
6782 private static final int OGG_SEGMENT_SIZE = 255 ;
6883
84+ /**
85+ * The maximum size of the Opus packet in bytes, to be included in the Ogg page.
86+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-6">
87+ * RFC7845 6. Packet Size Limits</a>
88+ */
89+ private static final int OPUS_MAX_PACKETS_PAGE_SIZE = 65_025 ;
6990
7091 private boolean done = false ;
7192 private boolean parsed = false ;
@@ -234,7 +255,7 @@ public void build() throws IOException {
234255 }
235256
236257 /* step 3: create packet with metadata */
237- final byte [] buffer = makeMetadata ();
258+ final byte [] buffer = makeCommentHeader ();
238259 if (buffer != null ) {
239260 addPacketSegment (buffer .length );
240261 makePacketHeader (0x00 , header , buffer );
@@ -325,17 +346,21 @@ private int makePacketHeader(final long granPos, @NonNull final ByteBuffer buffe
325346 /**
326347 * Creates the metadata header for the selected codec (Opus or Vorbis).
327348 *
328- * Opus metadata can contain
349+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
350+ * RFC7845 5.2. Comment Header</a> for OPUS metadata header format
351+ * @see <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2">
352+ * Vorbis I 4.2. Header decode and decode setup</a> and
353+ * <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-820005">
354+ * Vorbis 5. comment field and header specification</a>
355+ * for VORBIS metadata header format
329356 *
330- * @ImplNote See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
331- * RFC7845 5.2</a>
332- *
333- * @return
357+ * @return the metadata header as a byte array, or null if the codec is not supported
358+ * for metadata generation
334359 */
335360 @ Nullable
336- private byte [] makeMetadata () {
361+ private byte [] makeCommentHeader () {
337362 if (DEBUG ) {
338- Log .d ("OggFromWebMWriter" , "Downloading media with codec ID " + webmTrack .codecId );
363+ Log .d (TAG , "Downloading media with codec ID " + webmTrack .codecId );
339364 }
340365
341366 if ("A_OPUS" .equals (webmTrack .codecId )) {
@@ -350,23 +375,22 @@ private byte[] makeMetadata() {
350375 .getLocalDateTime ()
351376 .format (DateTimeFormatter .ISO_DATE )));
352377 if (thumbnail != null ) {
353- metadata .add (makeOpusPictureTag (thumbnail ));
378+ metadata .add (makeFlacPictureTag (thumbnail ));
354379 }
355380 }
356381
357382 if (DEBUG ) {
358- Log .d ("OggFromWebMWriter" , "Creating metadata header with this data:" );
359- metadata .forEach (p -> Log .d ("OggFromWebMWriter" , p .first + "=" + p .second ));
383+ Log .d (TAG , "Creating metadata header with this data:" );
384+ metadata .forEach (p -> Log .d (TAG , p .first + "=" + p .second ));
360385 }
361386
362387 return makeOpusTagsHeader (metadata );
363388 } else if ("A_VORBIS" .equals (webmTrack .codecId )) {
364- /**
365- * See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
366- * RFC7845 5.2</a>
367- */
389+ // See https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2
390+ // for the Vorbis comment header format
391+ // TODO: add Vorbis metadata: same as Opus, but with the Vorbis comment header format
368392 return new byte []{
369- 0x03 , // ???
393+ 0x03 , // packet type for Vorbis comment header
370394 0x76 , 0x6f , 0x72 , 0x62 , 0x69 , 0x73 , // "vorbis" binary string
371395 0x00 , 0x00 , 0x00 , 0x00 , // writing application string size (not present)
372396 0x00 , 0x00 , 0x00 , 0x00 // additional tags count (zero means no tags)
@@ -397,27 +421,37 @@ private static byte[] makeOpusMetadataTag(final Pair<String, String> pair) {
397421 }
398422
399423 /**
400- * Adds the {@code METADATA_BLOCK_PICTURE} tag to the Opus metadata,
401- * containing the provided bitmap as cover art.
424+ * Generates a FLAC picture block for the provided bitmap.
402425 *
403426 * <p>
404- * One could also use the COVERART tag instead, but it is not as widely supported
405- * as METADATA_BLOCK_PICTURE.
427+ * The {@code METADATA_BLOCK_PICTURE} tag is defined in the FLAC specification (RFC 9639)
428+ * and is supported by Opus and Vorbis metadata headers.
429+ * The picture block contains the image data which is converted to JPEG
430+ * and associated metadata such as picture type, dimensions, and color depth.
431+ * The image data is Base64-encoded as per specification.
406432 * </p>
407433 *
408- * @param bitmap The bitmap to use as cover art
409- * @return The key-value pair representing the tag
434+ * @see <a href="https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8">
435+ * RFC 9639 8.8 Picture</a>
436+ *
437+ * @param bitmap The bitmap to use for the picture block
438+ * @return The key-value pair representing the tag.
439+ * The key is {@code METADATA_BLOCK_PICTURE}
440+ * and the value is the Base64-encoded FLAC picture block.
410441 */
411- private static Pair <String , String > makeOpusPictureTag (final Bitmap bitmap ) {
442+ private static Pair <String , String > makeFlacPictureTag (final Bitmap bitmap ) {
412443 // FLAC picture block format (big-endian):
413444 // uint32 picture_type
414- // uint32 mime_length, mime_string
415- // uint32 desc_length, desc_string
445+ // uint32 mime_length,
446+ // mime_string
447+ // uint32 desc_length,
448+ // desc_string
416449 // uint32 width
417450 // uint32 height
418451 // uint32 color_depth
419452 // uint32 colors_indexed
420- // uint32 data_length, data_bytes
453+ // uint32 data_length,
454+ // data_bytes
421455
422456 final ByteArrayOutputStream baos = new ByteArrayOutputStream ();
423457 bitmap .compress (Bitmap .CompressFormat .JPEG , 100 , baos );
@@ -428,7 +462,8 @@ private static Pair<String, String> makeOpusPictureTag(final Bitmap bitmap) {
428462 // fixed ints + mime + desc
429463 final int headerSize = 4 * 8 + mimeBytes .length + descBytes .length ;
430464 final ByteBuffer buf = ByteBuffer .allocate (headerSize + imageData .length );
431- // See https://id3.org/id3v2.3.0#Attached_picture for a full list of picture types
465+ // See https://www.rfc-editor.org/rfc/rfc9639.html#table-13 for the complete list
466+ // of picture types
432467 // TODO: allow specifying other picture types, i.e. cover (front) for music albums;
433468 // but this info needs to be provided by the extractor first.
434469 buf .putInt (3 ); // picture type: 0 = Other, 2 = Cover (front)
@@ -442,7 +477,7 @@ private static Pair<String, String> makeOpusPictureTag(final Bitmap bitmap) {
442477 buf .putInt (bitmap .getWidth ());
443478 buf .putInt (bitmap .getHeight ());
444479 buf .putInt (24 ); // color depth for JPEG and PNG is usually 24 bits
445- buf .putInt (0 ); // colors indexed (0 for non-indexed images, i.e. JPEG, PNG )
480+ buf .putInt (0 ); // colors indexed (0 for non-indexed images like JPEG)
446481 buf .putInt (imageData .length );
447482 buf .put (imageData );
448483 final String b64 = Base64 .getEncoder ().encodeToString (buf .array ());
@@ -476,6 +511,7 @@ private static byte[] makeOpusTagsHeader(final List<Pair<String, String>> keyVal
476511
477512 final var head = ByteBuffer .allocate (byteCount );
478513 head .order (ByteOrder .LITTLE_ENDIAN );
514+ // See RFC7845 5.2: https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2
479515 head .put (new byte []{
480516 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
481517 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
@@ -559,9 +595,10 @@ private boolean addPacketSegment(final SimpleBlock block) {
559595 }
560596
561597 private boolean addPacketSegment (final int size ) {
562- if (size > 65025 ) {
563- throw new UnsupportedOperationException (
564- String .format ("page size is %s but cannot be larger than 65025" , size ));
598+ if (size > OPUS_MAX_PACKETS_PAGE_SIZE ) {
599+ throw new UnsupportedOperationException (String .format (
600+ "page size is %s but cannot be larger than %s" ,
601+ size , OPUS_MAX_PACKETS_PAGE_SIZE ));
565602 }
566603
567604 int available = (segmentTable .length - segmentTableSize ) * OGG_SEGMENT_SIZE ;
0 commit comments