Skip to content

Commit

Permalink
Add javadoc for Ogg opus stream classes.
Browse files Browse the repository at this point in the history
  • Loading branch information
leonfancy committed Jul 11, 2020
1 parent 810c64d commit 65ca13a
Show file tree
Hide file tree
Showing 15 changed files with 188 additions and 47 deletions.
1 change: 0 additions & 1 deletion src/main/java/org/chenliang/oggus/ogg/OggPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.google.common.io.LittleEndianDataOutputStream;
import com.google.common.primitives.Bytes;
import org.chenliang.oggus.opus.InvalidOpusException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/chenliang/oggus/ogg/OggStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ private OggStream(InputStream inputStream) {
*
* @param filePath path of an Ogg file
* @throws FileNotFoundException if the Ogg file doesn't exist.
* @return OggStream
*/
public static OggStream from(String filePath) throws FileNotFoundException {
return new OggStream(new BufferedInputStream(new FileInputStream(filePath)));
Expand All @@ -33,6 +34,7 @@ public static OggStream from(String filePath) throws FileNotFoundException {
* Create {@code OggStream} from an {@code InputStream}.
*
* @param inputStream the underlying input stream.
* @return OggStream
*/
public static OggStream from(InputStream inputStream) {
return new OggStream(inputStream);
Expand Down
37 changes: 36 additions & 1 deletion src/main/java/org/chenliang/oggus/opus/CommentHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@
import java.util.Collection;
import java.util.Map;

/**
* The Comment Header packet of a Ogg Opus stream. It has following structure:
* <pre>
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | 'O' | 'p' | 'u' | 's' |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | 'T' | 'a' | 'g' | 's' |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Vendor String Length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* : Vendor String... :
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | User Comment List Length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | User Comment #0 String Length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* : User Comment #0 String... :
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | User Comment #1 String Length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* : :
* </pre>
*/
public class CommentHeader {
public static final byte[] MAGIC_SIGNATURE = {'O', 'p', 'u', 's', 'T', 'a', 'g', 's'};
private String vendor;
Expand All @@ -22,7 +50,7 @@ private CommentHeader() {
}

/**
* Create {@code CommentHeader} from binary data, the data must start with 'OpusTags'.
* Parse {@code CommentHeader} from binary data, the data must start with 'OpusTags'.
* <p>
* Based on the specification, tag fields are case-insensitive. They are all converted to upper case when parsing
* from the binary data.
Expand Down Expand Up @@ -58,6 +86,8 @@ public static CommentHeader from(byte[] data) {

/**
* Create an empty Comment Header.
*
* @return CommentHeader
*/
public static CommentHeader emptyHeader() {
return new CommentHeader();
Expand All @@ -84,13 +114,18 @@ public String getVendor() {

/**
* Set Vendor
*
* @param vendor the Vendor string
*/
public void setVendor(String vendor) {
this.vendor = vendor;
}

/**
* Add a tag, the key will be transformed to upper case.
*
* @param key tag name
* @param value tag value
*/
public void addTag(String key, String value) {
this.tags.put(key.toUpperCase(), value);
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/org/chenliang/oggus/opus/IdHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@

import static java.lang.String.format;

/**
* The Identification Header packet of a Ogg Opus stream. It has following structure:
*
* <pre>
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | 'O' | 'p' | 'u' | 's' |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | 'H' | 'e' | 'a' | 'd' |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Version = 1 | Channel Count | Pre-skip |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Input Sample Rate (Hz) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Output Gain (Q7.8 in dB) | Mapping Family| |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ :
* | |
* : Optional Channel Mapping Table... :
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* </pre>
*/
public class IdHeader {
public static final byte[] MAGIC_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
private int majorVersion;
Expand All @@ -26,6 +48,12 @@ public class IdHeader {
private IdHeader() {
}

/**
* Parse {@code IdHeader} from binary data, the data must start with 'OpusHead'.
*
* @param data the binary data of ID Header
* @return IdHeader
*/
public static IdHeader from(byte[] data) {
IdHeader idHeader = new IdHeader();
LittleEndianDataInputStream in = new LittleEndianDataInputStream(new ByteArrayInputStream(data));
Expand Down Expand Up @@ -70,10 +98,20 @@ public static IdHeader from(byte[] data) {
}
}

/**
* Create an empty Id Header.
*
* @return IdHeader
*/
public static IdHeader emptyHeader() {
return new IdHeader();
}

/**
* Dump this IdHeader to binary.
*
* @return the binary bytes representation of this IdHeader
*/
public byte[] dump() {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
LittleEndianDataOutputStream out = new LittleEndianDataOutputStream(byteArrayOutputStream);
Expand Down
123 changes: 87 additions & 36 deletions src/main/java/org/chenliang/oggus/opus/OggOpusStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@
import java.util.List;
import java.util.Queue;

/**
* A class that provide methods to read an Ogg opus stream. An Ogg Opus stream is organized as follows:
*
* <pre>
* Page 0 Pages 1 ... n Pages (n+1) ...
* +------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +--
* | | | | | | | | | | | | |
* |+----------+| |+-----------------+| |+-------------------+ +-----
* || ID Header|| || Comment Header || ||Audio Data Packet 1| | ...
* |+----------+| |+-----------------+| |+-------------------+ +-----
* | | | | | | | | | | | | |
* +------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +--
* ^ ^ ^
* | | |
* | | Mandatory Page Break
* | ID header is contained on a single page
* 'Beginning Of Stream'
* </pre>
*/
public class OggOpusStream {
private final CommentHeader commentHeader;
private final IdHeader idHeader;
Expand All @@ -25,58 +44,54 @@ private OggOpusStream(OggStream oggStream) throws IOException {
this.oggStream = oggStream;
}

/**
* Read Ogg Opus stream from an InputStream.
*
* @param inputStream An InputStream that could read the Ogg Opus stream.
* @return The OggOpusStream object
* @throws IOException If IO read error
*/
public static OggOpusStream from(InputStream inputStream) throws IOException {
return new OggOpusStream(OggStream.from(inputStream));
}

/**
* Read Ogg Opus stream from file
*
* @param filePath The file path
* @return The OggOpusStream object
* @throws IOException If IO read error
*/
public static OggOpusStream from(String filePath) throws IOException {
return new OggOpusStream(OggStream.from(filePath));
}

private IdHeader readIdHeader(OggStream oggStream) throws IOException {
OggPage oggPage = readOpusBosPage(oggStream);
streamId = oggPage.getSerialNum();
return IdHeader.from(oggPage.getDataPackets().get(0));
}

private OggPage readOpusBosPage(OggStream oggStream) throws IOException {
while (true) {
OggPage oggPage = oggStream.readPage();
if (oggPage == null) {
throw new InvalidOpusException("No ID Header data in this opus file");
}

if (oggPage.isBOS()) {
if (oggPage.getDataPackets().size() > 1) {
throw new InvalidOpusException("The ID Header Ogg page must NOT contain other data");
}
return oggPage;
}
}
}

private CommentHeader readCommentHeader(OggStream oggStream) throws IOException {
byte[] commentHeaderData = new byte[0];
while (true) {
OggPage currentPage = oggStream.readPage(streamId);
List<byte[]> currentPagePackets = currentPage.getDataPackets();
if (currentPagePackets.size() != 1) {
throw new InvalidOpusException("Comment Header Ogg pages must only contain 1 data packet");
}
commentHeaderData = Bytes.concat(commentHeaderData, currentPagePackets.get(0));
if (currentPage.getGranulePosition() == 0) break;
}
return CommentHeader.from(commentHeaderData);
}

/**
* Get the Id header of this Ogg Opus stream
*
* @return IdHeader
*/
public IdHeader getIdHeader() {
return this.idHeader;
}

/**
* Get the Comment header of this Ogg Opus stream
*
* @return CommentHeader
*/
public CommentHeader getCommentHeader() {
return this.commentHeader;
}

/**
* Read an AudioDataPacket from the Ogg Opus stream. Return {@code null} if this is not more data to read.
*
* <p>If there multiple logical streams, the first Opus stream is read.</p>
*
* @return AudioDataPacket
* @throws IOException if IO read error
*/
public AudioDataPacket readAudioPacket() throws IOException {
if (lastPageLeftAudioDataPackets.isEmpty()) {
if (isEnd) {
Expand Down Expand Up @@ -121,4 +136,40 @@ public AudioDataPacket readAudioPacket() throws IOException {
}
return AudioDataPacket.from(data, idHeader.getStreamCount());
}

private IdHeader readIdHeader(OggStream oggStream) throws IOException {
OggPage oggPage = readOpusBosPage(oggStream);
streamId = oggPage.getSerialNum();
return IdHeader.from(oggPage.getDataPackets().get(0));
}

private OggPage readOpusBosPage(OggStream oggStream) throws IOException {
while (true) {
OggPage oggPage = oggStream.readPage();
if (oggPage == null) {
throw new InvalidOpusException("No ID Header data in this opus file");
}

if (oggPage.isBOS()) {
if (oggPage.getDataPackets().size() > 1) {
throw new InvalidOpusException("The ID Header Ogg page must NOT contain other data");
}
return oggPage;
}
}
}

private CommentHeader readCommentHeader(OggStream oggStream) throws IOException {
byte[] commentHeaderData = new byte[0];
while (true) {
OggPage currentPage = oggStream.readPage(streamId);
List<byte[]> currentPagePackets = currentPage.getDataPackets();
if (currentPagePackets.size() != 1) {
throw new InvalidOpusException("Comment Header Ogg pages must only contain 1 data packet");
}
commentHeaderData = Bytes.concat(commentHeaderData, currentPagePackets.get(0));
if (currentPage.getGranulePosition() == 0) break;
}
return CommentHeader.from(commentHeaderData);
}
}
4 changes: 4 additions & 0 deletions src/main/java/org/chenliang/oggus/opus/OpusPacket.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,15 @@ public int getPadDataLen() {

/**
* Dump Opus packet to standard binary.
*
* @return binary byte array
*/
public abstract byte[] dumpToStandardFormat();

/**
* Dump Opus packet to self delimiting binary.
*
* @return binary byte array
*/
public abstract byte[] dumpToSelfDelimitingFormat();

Expand Down
1 change: 0 additions & 1 deletion src/test/java/org/chenliang/oggus/ogg/OggPageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.google.common.primitives.Bytes;
import org.chenliang.oggus.TestUtil;
import org.chenliang.oggus.opus.InvalidOpusException;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.chenliang.oggus.TestUtil;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;

class AudioDataPacketTest {
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class CodeThreePacketTest {
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import org.chenliang.oggus.TestUtil;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class CodeTwoPacketTest {
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import org.chenliang.oggus.TestUtil;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

class CodeZeroPacketTest {
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import com.google.common.primitives.Bytes;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

class CommentHeaderTest {
@Test
Expand Down
Loading

0 comments on commit 65ca13a

Please sign in to comment.