-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOggParser.java
More file actions
168 lines (141 loc) · 6.34 KB
/
OggParser.java
File metadata and controls
168 lines (141 loc) · 6.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package me.tamkungz.codecmedia.internal.audio.ogg;
import me.tamkungz.codecmedia.CodecMediaException;
import me.tamkungz.codecmedia.internal.audio.BitrateMode;
import me.tamkungz.codecmedia.internal.io.ByteArrayReader;
public final class OggParser {
private static final int OPUS_GRANULE_RATE = 48_000;
private OggParser() {
}
public static OggProbeInfo parse(byte[] data) throws CodecMediaException {
if (data == null || data.length < 27) {
throw new CodecMediaException("Invalid OGG data: too small");
}
OggPageHeader firstPage = parsePageHeader(data, 0);
if (firstPage == null) {
throw new CodecMediaException("Invalid OGG stream: missing OggS header");
}
int identOffset = firstPage.headerSize();
int firstPayloadSize = firstPage.payloadSize();
if (identOffset + firstPayloadSize > data.length || firstPayloadSize <= 0) {
throw new CodecMediaException("Invalid OGG stream: incomplete first packet payload");
}
AudioIdent ident = parseIdentificationPacket(data, identOffset, firstPayloadSize);
long payloadBits = 0;
int pageCount = 0;
long maxGranule = 0;
int offset = 0;
while (offset + 27 <= data.length) {
OggPageHeader page = parsePageHeader(data, offset);
if (page == null) {
break;
}
payloadBits += (long) page.payloadSize() * 8;
pageCount++;
if (page.granulePosition() > maxGranule) {
maxGranule = page.granulePosition();
}
offset += page.totalPageSize();
}
int granuleRate = ident.granuleRate() > 0 ? ident.granuleRate() : ident.sampleRate();
long durationMillis = (granuleRate > 0 && maxGranule > 0)
? (maxGranule * 1000L) / granuleRate
: 0;
int avgBitrate = durationMillis > 0
? (int) ((payloadBits * 1000L) / durationMillis / 1000L)
: 0;
int nominalKbps = ident.nominalBitrate() > 0 ? (int) (ident.nominalBitrate() / 1000L) : 0;
int bitrateKbps = avgBitrate > 0 ? avgBitrate : nominalKbps;
BitrateMode mode = switch (ident.codec()) {
case "vorbis" -> (ident.nominalBitrate() > 0 || pageCount > 2) ? BitrateMode.VBR : BitrateMode.UNKNOWN;
case "opus" -> BitrateMode.VBR;
default -> BitrateMode.UNKNOWN;
};
return new OggProbeInfo(ident.codec(), ident.sampleRate(), ident.channels(), bitrateKbps, mode, durationMillis);
}
private static AudioIdent parseIdentificationPacket(byte[] data, int identOffset, int payloadSize) throws CodecMediaException {
if (isVorbisIdentification(data, identOffset, payloadSize)) {
if (payloadSize < 30) {
throw new CodecMediaException("Invalid OGG Vorbis stream: incomplete identification packet");
}
ByteArrayReader ident = new ByteArrayReader(data);
ident.position(identOffset + 7);
ident.readU32LE(); // vorbis version
int channels = ident.readU8();
int sampleRate = (int) ident.readU32LE();
long bitrateNominal = ident.readU32LE();
return new AudioIdent("vorbis", sampleRate, channels, bitrateNominal, sampleRate);
}
if (isOpusIdentification(data, identOffset, payloadSize)) {
if (payloadSize < 19) {
throw new CodecMediaException("Invalid OGG Opus stream: incomplete OpusHead packet");
}
ByteArrayReader ident = new ByteArrayReader(data);
ident.position(identOffset + 9); // OpusHead + version
int channels = ident.readU8();
ident.readU16LE(); // pre-skip
int inputSampleRate = (int) ident.readU32LE();
int sampleRate = inputSampleRate > 0 ? inputSampleRate : OPUS_GRANULE_RATE;
return new AudioIdent("opus", sampleRate, channels, 0, OPUS_GRANULE_RATE);
}
throw new CodecMediaException("Unsupported OGG codec: currently Vorbis and Opus are parsed");
}
private static boolean isVorbisIdentification(byte[] data, int offset, int payloadSize) {
return payloadSize >= 7
&& data[offset] == 0x01
&& data[offset + 1] == 'v'
&& data[offset + 2] == 'o'
&& data[offset + 3] == 'r'
&& data[offset + 4] == 'b'
&& data[offset + 5] == 'i'
&& data[offset + 6] == 's';
}
private static boolean isOpusIdentification(byte[] data, int offset, int payloadSize) {
return payloadSize >= 8
&& data[offset] == 'O'
&& data[offset + 1] == 'p'
&& data[offset + 2] == 'u'
&& data[offset + 3] == 's'
&& data[offset + 4] == 'H'
&& data[offset + 5] == 'e'
&& data[offset + 6] == 'a'
&& data[offset + 7] == 'd';
}
private static OggPageHeader parsePageHeader(byte[] data, int offset) {
if (offset < 0 || offset + 27 > data.length) {
return null;
}
if (data[offset] != 'O' || data[offset + 1] != 'g' || data[offset + 2] != 'g' || data[offset + 3] != 'S') {
return null;
}
ByteArrayReader r = new ByteArrayReader(data);
r.position(offset + 4);
int version = r.readU8();
int headerType = r.readU8();
long granulePosition = r.readU64LE();
long serial = r.readU32LE();
long sequence = r.readU32LE();
r.readU32LE(); // checksum
int segmentCount = r.readU8();
if (offset + 27 + segmentCount > data.length) {
return null;
}
int payload = 0;
for (int i = 0; i < segmentCount; i++) {
payload += data[offset + 27 + i] & 0xFF;
}
int headerSize = 27 + segmentCount;
int total = headerSize + payload;
if (offset + total > data.length) {
return null;
}
return new OggPageHeader(version, headerType, granulePosition, serial, sequence, segmentCount, payload, total, headerSize);
}
private record AudioIdent(
String codec,
int sampleRate,
int channels,
long nominalBitrate,
int granuleRate
) {
}
}