This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 6ffb5dda61 TIKA-4779: Add audio:bitrate, audio:is-variable-bitrate and 
audio:has-drm (#2953)
6ffb5dda61 is described below

commit 6ffb5dda616a231b309754313451c24faa432969
Author: Dominik Schmidt <[email protected]>
AuthorDate: Tue Jul 21 22:15:49 2026 +0200

    TIKA-4779: Add audio:bitrate, audio:is-variable-bitrate and audio:has-drm 
(#2953)
---
 .../main/java/org/apache/tika/metadata/Audio.java  |  22 +++
 .../java/org/apache/tika/parser/mp3/Mp3Parser.java |  51 ++++++
 .../org/apache/tika/parser/mp3/MpegStream.java     |  34 +++-
 .../apache/tika/parser/mp4/TikaMp4BoxHandler.java  |   6 +
 .../tika/parser/mp4/TikaMp4SoundHandler.java       | 193 +++++++++++++++++++++
 .../org/apache/tika/parser/ogg/VorbisParser.java   |  18 ++
 .../org/apache/tika/parser/mp3/Mp3ParserTest.java  |  36 ++++
 .../org/apache/tika/parser/mp4/MP4ParserTest.java  |  22 +++
 .../apache/tika/parser/ogg/VorbisParserTest.java   | 104 +++++++++++
 .../resources/test-documents/testMP3cbrInfoTag.mp3 | Bin 0 -> 6681 bytes
 .../test/resources/test-documents/testMP3vbr.mp3   | Bin 0 -> 3129 bytes
 .../test/resources/test-documents/testMP4_drm.m4a  | Bin 0 -> 464 bytes
 .../resources/test-documents/testMP4_esdsFlags.m4a | Bin 0 -> 512 bytes
 .../test/resources/test-documents/testVORBIS.ogg   | Bin 0 -> 4241 bytes
 14 files changed, 484 insertions(+), 2 deletions(-)

diff --git a/tika-core/src/main/java/org/apache/tika/metadata/Audio.java 
b/tika-core/src/main/java/org/apache/tika/metadata/Audio.java
index f20463bfd3..9b84b0eb3f 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/Audio.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/Audio.java
@@ -49,4 +49,26 @@ public interface Audio {
      * The disc value exactly as tagged, see {@link #RAW_TRACK_NUMBER}.
      */
     Property RAW_DISC_NUMBER = Property.internalText("audio:raw-disc-number");
+
+    /**
+     * Average or nominal bitrate in bits per second (averaged over the MP3
+     * frames, the Vorbis nominal bitrate, or the MP4 'esds' average bitrate).
+     * A per-stream value: in a file with several audio tracks it reflects
+     * the last sound track's sample description.
+     */
+    Property BITRATE = Property.internalInteger("audio:bitrate");
+
+    /**
+     * True if the stream is variable bitrate: the MP3 frames declare differing
+     * bitrates, or the Vorbis identification header does not declare one fixed
+     * rate for upper, nominal and lower.
+     */
+    Property IS_VARIABLE_BITRATE = 
Property.internalBoolean("audio:is-variable-bitrate");
+
+    /**
+     * True if the container declares DRM protection through a protected
+     * sample entry format such as 'drms' or 'enca'. A file-level flag: any
+     * protected audio track sets it. Only set when protection is detected.
+     */
+    Property HAS_DRM = Property.internalBoolean("audio:has-drm");
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
index 2f51477eb9..82fb40dd4f 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
@@ -96,12 +96,30 @@ public class Mp3Parser implements Parser {
         // Now iterate over all audio frames in the file
         AudioFrame frame = mpegStream.nextFrame();
         float duration = 0;
+        long bitRateSum = 0;
+        long frameCount = 0;
+        int baselineBitRate = -1;
+        boolean variableBitRate = false;
+        boolean firstFrame = true;
         boolean skipped = true;
         while (frame != null && skipped) {
             duration += frame.getDuration();
             if (firstAudio == null) {
                 firstAudio = frame;
             }
+            //the Xing/Info/VBRI header is a valid MPEG frame but carries no
+            //audio and may use a different bitrate, so keep it out of the 
stats
+            if (!(firstFrame && isMetadataFrame(mpegStream))) {
+                int bitRate = frame.getBitRate();
+                bitRateSum += bitRate;
+                frameCount++;
+                if (baselineBitRate < 0) {
+                    baselineBitRate = bitRate;
+                } else if (bitRate != baselineBitRate) {
+                    variableBitRate = true;
+                }
+            }
+            firstFrame = false;
             skipped = mpegStream.skipFrame();
             if (skipped) {
                 frame = mpegStream.nextFrame();
@@ -136,6 +154,12 @@ public class Mp3Parser implements Parser {
         ret.lyrics = lyrics;
         ret.tags = tags.toArray(new ID3Tags[0]);
         ret.duration = duration;
+        if (frameCount > 0) {
+            //MP3 frame duration does not depend on the bitrate, so the plain
+            //mean over the frames is the true average bitrate
+            ret.averageBitRate = (int) (bitRateSum / frameCount);
+            ret.variableBitRate = variableBitRate;
+        }
         return ret;
     }
 
@@ -159,6 +183,10 @@ public class Mp3Parser implements Parser {
             metadata.set(XMPDM.DURATION, audioAndTags.durationSeconds());
         }
 
+        if (audioAndTags.averageBitRate > 0) {
+            metadata.set(Audio.BITRATE, audioAndTags.averageBitRate);
+            metadata.set(Audio.IS_VARIABLE_BITRATE, 
audioAndTags.variableBitRate);
+        }
         if (audioAndTags.audio != null) {
             metadata.set("channels", 
String.valueOf(audioAndTags.audio.getChannels()));
             metadata.set("version", audioAndTags.audio.getVersion());
@@ -278,11 +306,34 @@ public class Mp3Parser implements Parser {
     public int getMaxRecordSize() {
         return ID3v2Frame.getMaxRecordSize();
     }
+    /**
+     * Does the current frame's payload start with a Xing, Info or VBRI
+     * header? Those tag frames describe the stream rather than carrying
+     * audio. The markers sit at small fixed offsets that depend on version
+     * and channel mode, all within the first 40 payload bytes.
+     */
+    private static boolean isMetadataFrame(MpegStream mpegStream) throws 
IOException {
+        byte[] payload = mpegStream.peekFramePayload(40);
+        for (int i = 0; i + 4 <= payload.length; i++) {
+            if ((payload[i] == 'X' && payload[i + 1] == 'i'
+                    && payload[i + 2] == 'n' && payload[i + 3] == 'g')
+                    || (payload[i] == 'I' && payload[i + 1] == 'n'
+                    && payload[i + 2] == 'f' && payload[i + 3] == 'o')
+                    || (payload[i] == 'V' && payload[i + 1] == 'B'
+                    && payload[i + 2] == 'R' && payload[i + 3] == 'I')) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     protected static class ID3TagsAndAudio {
         private ID3Tags[] tags;
         private AudioFrame audio;
         private LyricsHandler lyrics;
         private float duration; // Milliseconds
+        private int averageBitRate; // bits per second, 0 if no frame was seen
+        private boolean variableBitRate;
 
         private float durationSeconds() {
             return duration / 1000;
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
index a87757253a..2a8c1aa839 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
@@ -19,6 +19,7 @@ package org.apache.tika.parser.mp3;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PushbackInputStream;
+import java.util.Arrays;
 
 import org.apache.commons.io.IOUtils;
 
@@ -113,6 +114,12 @@ class MpegStream extends PushbackInputStream {
      */
     private static final int HEADER_SIZE = 4;
 
+    /**
+     * Pushback capacity: enough for the header handling plus
+     * {@link #peekFramePayload(int)} peeks into the frame payload.
+     */
+    private static final int PEEK_BUFFER_SIZE = 48;
+
     /**
      * The current MPEG header.
      */
@@ -130,7 +137,7 @@ class MpegStream extends PushbackInputStream {
      * @param in the underlying audio stream
      */
     public MpegStream(InputStream in) {
-        super(in, 2 * HEADER_SIZE);
+        super(in, PEEK_BUFFER_SIZE);
     }
 
     /**
@@ -285,7 +292,9 @@ class MpegStream extends PushbackInputStream {
     public boolean skipFrame() throws IOException {
         if (currentHeader != null) {
             long toSkip = currentHeader.getLength() - HEADER_SIZE;
-            long skipped = IOUtils.skip(in, toSkip);
+            //skip through this stream, not the wrapped one, so bytes pushed
+            //back by peekFramePayload are honored
+            long skipped = IOUtils.skip(this, toSkip);
             currentHeader = null;
             if (skipped < toSkip) {
                 return false;
@@ -295,6 +304,27 @@ class MpegStream extends PushbackInputStream {
         return false;
     }
 
+    /**
+     * Reads up to {@code count} bytes of the current frame's payload and
+     * pushes them back, leaving the stream position and the frame accounting
+     * undisturbed. Used to recognize metadata-only frames (Xing/Info/VBRI).
+     */
+    byte[] peekFramePayload(int count) throws IOException {
+        byte[] buffer = new byte[count];
+        int read = 0;
+        while (read < count) {
+            int r = read(buffer, read, count - read);
+            if (r < 0) {
+                break;
+            }
+            read += r;
+        }
+        if (read > 0) {
+            unread(buffer, 0, read);
+        }
+        return read == count ? buffer : Arrays.copyOf(buffer, Math.max(read, 
0));
+    }
+
     /**
      * Advances the underlying stream until the first byte of frame sync is
      * found.
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
index d59070bfca..9d15d3afeb 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
@@ -117,6 +117,12 @@ public class TikaMp4BoxHandler extends Mp4BoxHandler {
             Long movieTimescale = 
directory.getLongObject(Mp4Directory.TAG_TIME_SCALE);
             return new TikaMp4MetaHandler(metadata, context, tikaMetadata,
                     emptyEditDuration, movieTimescale == null ? 0 : 
movieTimescale);
+        } else if (box.equals("hdlr") && payload != null && payload.length >= 
12
+                && payload[8] == 's' && payload[9] == 'o'
+                && payload[10] == 'u' && payload[11] == 'n') {
+            //sound track: our handler additionally reads DRM markers and the
+            //esds average bitrate from the sample description
+            return new TikaMp4SoundHandler(metadata, context, tikaMetadata);
         }
 
         return super.processBox(box, payload, size, context);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4SoundHandler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4SoundHandler.java
new file mode 100644
index 0000000000..770c4cd722
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4SoundHandler.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.mp4;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import com.drew.imaging.mp4.Mp4Handler;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.mp4.Mp4Context;
+import com.drew.metadata.mp4.media.Mp4SoundHandler;
+
+import org.apache.tika.io.EndianUtils;
+import org.apache.tika.metadata.Audio;
+
+/**
+ * Extends the sound track handling with what the base handler does not read
+ * from the sample description: DRM protection markers (protected sample entry
+ * formats such as 'drms' or 'enca') and the average bitrate from the 'esds'
+ * elementary stream descriptor. See TIKA-4779.
+ */
+class TikaMp4SoundHandler extends Mp4SoundHandler {
+
+    private final org.apache.tika.metadata.Metadata tikaMetadata;
+
+    TikaMp4SoundHandler(Metadata metadata, Mp4Context context,
+                        org.apache.tika.metadata.Metadata tikaMetadata) {
+        super(metadata, context);
+        this.tikaMetadata = tikaMetadata;
+    }
+
+    @Override
+    public Mp4Handler<?> processBox(String type, byte[] payload, long boxSize,
+                                    Mp4Context context) throws IOException {
+        if ("stsd".equals(type) && payload != null) {
+            extractFromSampleDescriptions(payload);
+        }
+        return super.processBox(type, payload, boxSize, context);
+    }
+
+    /**
+     * Walks the sample description entries: 4 bytes version and flags, a
+     * 4 byte entry count, then one sample entry per count, each starting with
+     * its own size and format fourcc.
+     */
+    private void extractFromSampleDescriptions(byte[] b) {
+        if (b.length < 8) {
+            return;
+        }
+        long entryCount = EndianUtils.getUIntBE(b, 4);
+        int pos = 8;
+        for (long i = 0; i < entryCount && pos + 8 <= b.length; i++) {
+            long size = EndianUtils.getUIntBE(b, pos);
+            if (size < 16 || size > b.length - pos) {
+                break;
+            }
+            int end = pos + (int) size;
+            String format = fourCc(b, pos + 4);
+            //protected streams replace the codec fourcc with a protected
+            //entry format: 'drms' (FairPlay) or 'enca' (ISO common encryption)
+            if ("drms".equals(format) || "enca".equals(format)) {
+                tikaMetadata.set(Audio.HAS_DRM, true);
+            }
+            if (pos + 18 <= end) {
+                //sample entry: 8 byte header, 6 reserved, 2 data ref index,
+                //then version-dependent fixed sound fields before child boxes
+                int version = EndianUtils.getUShortBE(b, pos + 16);
+                int bitRate = findEsdsAverageBitRate(b, pos + 
soundEntrySize(version), end);
+                if (bitRate > 0) {
+                    tikaMetadata.set(Audio.BITRATE, bitRate);
+                }
+            }
+            pos = end;
+        }
+    }
+
+    /**
+     * Size of the fixed part of a sound sample entry, after which the child
+     * boxes start: 36 bytes for version 0, 52 for version 1 (four extra
+     * 32-bit QuickTime fields), 72 for version 2.
+     */
+    private static int soundEntrySize(int version) {
+        if (version == 1) {
+            return 52;
+        }
+        if (version == 2) {
+            return 72;
+        }
+        return 36;
+    }
+
+    /**
+     * Scans the child boxes of a sample entry for an 'esds' box and returns
+     * its average bitrate, or 0 if there is none. QuickTime version 1/2
+     * entries may nest the 'esds' inside a 'wave' extension box.
+     */
+    private static int findEsdsAverageBitRate(byte[] b, int pos, int end) {
+        while (pos >= 0 && pos + 8 <= end) {
+            long size = EndianUtils.getUIntBE(b, pos);
+            if (size < 8 || size > end - pos) {
+                return 0;
+            }
+            String type = fourCc(b, pos + 4);
+            if ("esds".equals(type)) {
+                return readEsdsAverageBitRate(b, pos + 8, pos + (int) size);
+            }
+            if ("wave".equals(type)) {
+                int nested = findEsdsAverageBitRate(b, pos + 8, pos + (int) 
size);
+                if (nested > 0) {
+                    return nested;
+                }
+            }
+            pos += (int) size;
+        }
+        return 0;
+    }
+
+    /**
+     * Extracts the average bitrate from an 'esds' box body, or returns 0 if
+     * the descriptors cannot be walked. The chain is an ES_Descriptor (tag
+     * 0x03) with three optional fields signalled by its flags byte, followed
+     * by a DecoderConfigDescriptor (tag 0x04) whose fixed fields end with the
+     * maximum and average bitrates.
+     */
+    private static int readEsdsAverageBitRate(byte[] b, int pos, int end) {
+        //4 bytes version and flags, then the ES descriptor
+        pos += 4;
+        if (pos >= end || b[pos] != 0x03) {
+            return 0;
+        }
+        pos = skipDescriptorLength(b, pos + 1);
+        if (pos + 3 > end) {
+            return 0;
+        }
+        //ES_ID (2 bytes), then a flags/priority byte announcing the
+        //optional stream dependence, URL and OCR fields
+        int flags = b[pos + 2] & 0xFF;
+        pos += 3;
+        if ((flags & 0x80) != 0) {
+            pos += 2;
+        }
+        if ((flags & 0x40) != 0) {
+            if (pos >= end) {
+                return 0;
+            }
+            pos += 1 + (b[pos] & 0xFF);
+        }
+        if ((flags & 0x20) != 0) {
+            pos += 2;
+        }
+        if (pos >= end || b[pos] != 0x04) {
+            return 0;
+        }
+        pos = skipDescriptorLength(b, pos + 1);
+        //object type (1), stream type (1), buffer size (3), max bitrate (4)
+        pos += 9;
+        if (pos + 4 > end) {
+            return 0;
+        }
+        long averageBitRate = EndianUtils.getUIntBE(b, pos);
+        return averageBitRate > 0 && averageBitRate <= Integer.MAX_VALUE
+                ? (int) averageBitRate : 0;
+    }
+
+    /**
+     * Skips a descriptor's variable length encoding (bytes with the high bit
+     * set continue the length) and returns the position of the payload.
+     */
+    private static int skipDescriptorLength(byte[] b, int pos) {
+        while (pos < b.length && (b[pos] & 0x80) != 0) {
+            pos++;
+        }
+        return pos + 1;
+    }
+
+    private static String fourCc(byte[] b, int pos) {
+        return new String(b, pos, 4, StandardCharsets.ISO_8859_1);
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
index a4263db45a..dc6b78f995 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
@@ -32,6 +32,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Audio;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.XMPDM;
 import org.apache.tika.mime.MediaType;
@@ -88,6 +89,23 @@ public class VorbisParser extends OggAudioParser {
         metadata.set(XMPDM.AUDIO_SAMPLE_RATE, (int) info.getRate());
         metadata.add("version", "Vorbis " + info.getVersion());
 
+        int upper = info.getBitrateUpper();
+        int nominal = info.getBitrateNominal();
+        int lower = info.getBitrateLower();
+        if (nominal > 0 || upper > 0 || lower > 0) {
+            if (nominal > 0) {
+                metadata.set(Audio.BITRATE, nominal);
+            } else if (upper > 0 && upper == lower) {
+                //a zero-width bitrate bracket declares an exact rate as well
+                metadata.set(Audio.BITRATE, upper);
+            }
+            //fixed rate when all three declared rates agree, or when a
+            //zero-width bracket is declared without a nominal rate
+            boolean fixedRate = (nominal > 0 && upper == nominal && lower == 
nominal)
+                    || (nominal <= 0 && upper > 0 && upper == lower);
+            metadata.set(Audio.IS_VARIABLE_BITRATE, !fixedRate);
+        }
+
         extractChannelInfo(metadata, info);
     }
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
index e59f5716ca..af1c03f55d 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
@@ -104,6 +104,8 @@ public class Mp3ParserTest extends TikaTest {
         assertEquals("MPEG 3 Layer III Version 1", metadata.get("version"));
         assertEquals("44100", metadata.get(XMPDM.AUDIO_SAMPLE_RATE));
         assertEquals("1", metadata.get("channels"));
+        assertEquals("128000", metadata.get(Audio.BITRATE));
+        assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
 
         // Check XMPDM-typed audio properties
         assertEquals("Test Album", metadata.get(XMPDM.ALBUM));
@@ -166,6 +168,36 @@ public class Mp3ParserTest extends TikaTest {
         checkDuration(metadata, 2);
     }
 
+    /**
+     * The frames of testMP3vbr.mp3 alternate between 128 and 192 kbps, so
+     * the reported bitrate is the average and the stream is variable rate.
+     */
+    @Test
+    public void testMp3VariableBitRate() throws Exception {
+        Metadata metadata = new Metadata();
+        getText("testMP3vbr.mp3", metadata);
+
+        assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE));
+        assertEquals("160000", metadata.get(Audio.BITRATE));
+        assertEquals("true", metadata.get(Audio.IS_VARIABLE_BITRATE));
+    }
+
+    /**
+     * The first frame of testMP3cbrInfoTag.mp3 is an 'Info' tag frame
+     * written at 128 kbps while the audio frames are 320 kbps. The tag frame
+     * must not show up in the bitrate statistics: the file is constant rate
+     * at 320 kbps, not variable rate.
+     */
+    @Test
+    public void testMp3ConstantBitRateWithInfoTagFrame() throws Exception {
+        Metadata metadata = new Metadata();
+        getText("testMP3cbrInfoTag.mp3", metadata);
+
+        assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE));
+        assertEquals("320000", metadata.get(Audio.BITRATE));
+        assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
+    }
+
     /**
      * Test that with only ID3v2 tags, of version 2.4, we get the full
      * set of information out.
@@ -271,6 +303,10 @@ public class Mp3ParserTest extends TikaTest {
         Metadata metadata = new Metadata();
         String content = getText("testMP3lyrics.mp3", metadata);
 
+        //a real 192 kbps constant bitrate file
+        assertEquals("192000", metadata.get(Audio.BITRATE));
+        assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
+
         assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE));
         assertEquals("Test Title", metadata.get(TikaCoreProperties.TITLE));
         assertEquals("Test Artist", metadata.get(TikaCoreProperties.CREATOR));
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
index 4ceb23d49d..addf349d73 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
@@ -108,6 +108,9 @@ public class MP4ParserTest extends TikaTest {
         assertEquals("Test Genre", metadata.get(XMPDM.GENRE));
         assertEquals("Test Comments", 
metadata.get(XMPDM.LOG_COMMENT.getName()));
         assertEquals("1", metadata.get(XMPDM.TRACK_NUMBER));
+        //average bitrate from the esds elementary stream descriptor
+        assertEquals("256000", metadata.get(Audio.BITRATE));
+        assertNull(metadata.get(Audio.HAS_DRM));
         //the totals from the trkn/disk atoms were previously read and 
discarded
         assertEquals("42", metadata.get(Audio.TRACK_COUNT));
         assertEquals("Test Album Artist", metadata.get(XMPDM.ALBUM_ARTIST));
@@ -292,6 +295,25 @@ public class MP4ParserTest extends TikaTest {
         return vals;
     } */
 
+    @Test
+    public void testDrmProtectedM4a() throws Exception {
+        //the sample description declares a protected 'drms' sample entry
+        Metadata metadata = new Metadata();
+        getText("testMP4_drm.m4a", metadata);
+        assertEquals("true", metadata.get(Audio.HAS_DRM));
+    }
+
+    @Test
+    public void testEsdsWithDescriptorFlags() throws Exception {
+        //the ES descriptor declares the optional stream dependence, URL and
+        //OCR fields, which shift the DecoderConfigDescriptor; the URL string
+        //deliberately reads "sinf" so a raw fourcc scan would misreport DRM
+        Metadata metadata = new Metadata();
+        getText("testMP4_esdsFlags.m4a", metadata);
+        assertEquals("96000", metadata.get(Audio.BITRATE));
+        assertNull(metadata.get(Audio.HAS_DRM));
+    }
+
     @Test
     public void testQuickTimeMetadataKeys() throws Exception {
         //QuickTime item-list metadata (moov/meta/keys+ilst, the 
com.apple.quicktime.*
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
new file mode 100644
index 0000000000..142d202da2
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.ogg;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.gagravarr.vorbis.VorbisInfo;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Audio;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.XMPDM;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Tests the bitrate mapping from the Vorbis identification header, end to end
+ * against a real file and at unit level with directly constructed info
+ * objects for the header combinations the real fixture does not cover.
+ */
+public class VorbisParserTest extends TikaTest {
+
+    @Test
+    public void testRealFile() throws Exception {
+        //a real quality-mode encoding: nominal 80000, no upper/lower bounds
+        Metadata metadata = new Metadata();
+        try (TikaInputStream tis = TikaInputStream.get(
+                getResourceAsStream("/test-documents/testVORBIS.ogg"))) {
+            new VorbisParser().parse(tis, new DefaultHandler(), metadata,
+                    new ParseContext());
+        }
+        assertEquals("80000", metadata.get(Audio.BITRATE));
+        assertEquals("true", metadata.get(Audio.IS_VARIABLE_BITRATE));
+        assertEquals("44100", metadata.get(XMPDM.AUDIO_SAMPLE_RATE));
+    }
+
+    private static Metadata extractInfo(int upper, int nominal, int lower) 
throws Exception {
+        VorbisInfo info = new VorbisInfo();
+        info.setRate(44100);
+        info.setBitrateUpper(upper);
+        info.setBitrateNominal(nominal);
+        info.setBitrateLower(lower);
+
+        Metadata metadata = new Metadata();
+        new VorbisParser().extractInfo(metadata, info);
+        return metadata;
+    }
+
+    @Test
+    public void testNominalOnlyIsVariable() throws Exception {
+        //the common quality-mode encoding declares only a nominal rate
+        Metadata metadata = extractInfo(0, 192000, 0);
+        assertEquals("192000", metadata.get(Audio.BITRATE));
+        assertEquals("true", metadata.get(Audio.IS_VARIABLE_BITRATE));
+    }
+
+    @Test
+    public void testAllRatesEqualIsFixed() throws Exception {
+        Metadata metadata = extractInfo(192000, 192000, 192000);
+        assertEquals("192000", metadata.get(Audio.BITRATE));
+        assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
+    }
+
+    @Test
+    public void testZeroWidthBracketIsFixed() throws Exception {
+        //managed-mode encoding without a nominal rate: an upper and lower
+        //bound that agree still pin the stream to one exact rate
+        Metadata metadata = extractInfo(128000, 0, 128000);
+        assertEquals("128000", metadata.get(Audio.BITRATE));
+        assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
+    }
+
+    @Test
+    public void testDivergentBracketIsVariable() throws Exception {
+        //a real bracket without a nominal rate: variable, no single bitrate
+        Metadata metadata = extractInfo(256000, 0, 96000);
+        assertNull(metadata.get(Audio.BITRATE));
+        assertEquals("true", metadata.get(Audio.IS_VARIABLE_BITRATE));
+    }
+
+    @Test
+    public void testNoDeclaredRates() throws Exception {
+        Metadata metadata = extractInfo(0, 0, 0);
+        assertNull(metadata.get(Audio.BITRATE));
+        assertNull(metadata.get(Audio.IS_VARIABLE_BITRATE));
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3cbrInfoTag.mp3
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3cbrInfoTag.mp3
new file mode 100644
index 0000000000..25cfcb0e49
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3cbrInfoTag.mp3
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3vbr.mp3
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3vbr.mp3
new file mode 100644
index 0000000000..6d0267ca55
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3vbr.mp3
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_drm.m4a
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_drm.m4a
new file mode 100644
index 0000000000..4a37c1282b
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_drm.m4a
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_esdsFlags.m4a
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_esdsFlags.m4a
new file mode 100644
index 0000000000..5fda062fb7
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_esdsFlags.m4a
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS.ogg
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS.ogg
new file mode 100644
index 0000000000..1a02d2205a
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS.ogg
 differ


Reply via email to