Skip to content

Commit 8277dbe

Browse files
authored
Merge pull request #32 from eshaz/ogg-sample-count
Fix samples counts
2 parents 3ad0384 + 7834ca1 commit 8277dbe

File tree

49 files changed

+56923
-51020
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+56923
-51020
lines changed

package-lock.json

Lines changed: 688 additions & 691 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codec-parser",
3-
"version": "2.4.3",
3+
"version": "2.5.0",
44
"description": "Library that parses raw data from audio codecs into frames containing data, header values, duration, and other information.",
55
"main": "index.js",
66
"types": "index.d.ts",
@@ -37,8 +37,8 @@
3737
"sideEffects": false,
3838
"homepage": "https://github.com/eshaz/codec-parser#readme",
3939
"devDependencies": {
40-
"@types/jest": "^29.5.3",
41-
"jest": "^29.6.2",
42-
"prettier": "^3.0.1"
40+
"@types/jest": "^29.5.13",
41+
"jest": "^29.7.0",
42+
"prettier": "^3.3.3"
4343
}
4444
}

src/CodecParser.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
parseFrame,
4343
checkCodecUpdate,
4444
reset,
45+
isLastPage,
4546
} from "./constants.js";
4647
import HeaderCache from "./codecs/HeaderCache.js";
4748
import MPEGParser from "./codecs/mpeg/MPEGParser.js";
@@ -229,12 +230,37 @@ export default class CodecParser {
229230
[mapFrameStats](frame) {
230231
if (frame[codecFrames]) {
231232
// Ogg container
232-
frame[codecFrames].forEach((codecFrame) => {
233-
frame[duration] += codecFrame[duration];
234-
frame[samples] += codecFrame[samples];
235-
this[mapCodecFrameStats](codecFrame);
236-
});
233+
if (frame[isLastPage]) {
234+
// cut any excess samples that fall outside of the absolute granule position
235+
// some streams put invalid data in absolute granule position, so only do this
236+
// for the end of the stream
237+
let absoluteGranulePositionSamples = frame[samples];
238+
239+
frame[codecFrames].forEach((codecFrame) => {
240+
const untrimmedCodecSamples = codecFrame[samples];
241+
242+
if (absoluteGranulePositionSamples < untrimmedCodecSamples) {
243+
codecFrame[samples] =
244+
absoluteGranulePositionSamples > 0
245+
? absoluteGranulePositionSamples
246+
: 0;
247+
codecFrame[duration] =
248+
(codecFrame[samples] / codecFrame[header][sampleRate]) * 1000;
249+
}
250+
251+
absoluteGranulePositionSamples -= untrimmedCodecSamples;
252+
253+
this[mapCodecFrameStats](codecFrame);
254+
});
255+
} else {
256+
frame[samples] = 0;
257+
frame[codecFrames].forEach((codecFrame) => {
258+
frame[samples] += codecFrame[samples];
259+
this[mapCodecFrameStats](codecFrame);
260+
});
261+
}
237262

263+
frame[duration] = (frame[samples] / this._sampleRate) * 1000 || 0;
238264
frame[totalSamples] = this._totalSamples;
239265
frame[totalDuration] =
240266
(this._totalSamples / this._sampleRate) * 1000 || 0;

src/codecs/flac/FLACParser.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,8 @@ export default class FLACParser extends Parser {
100100
))
101101
) {
102102
// found a valid next frame header
103-
let frameData = yield* this._codecParser[readRawData](
104-
nextHeaderOffset,
105-
);
103+
let frameData =
104+
yield* this._codecParser[readRawData](nextHeaderOffset);
106105

107106
if (!this._codecParser._flushing)
108107
frameData = frameData[subarray](0, nextHeaderOffset);

src/codecs/opus/OpusFrame.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,10 @@
1616
along with this program. If not, see <https://www.gnu.org/licenses/>
1717
*/
1818

19-
import { sampleRate, frameCount, frameSize } from "../../constants.js";
2019
import CodecFrame from "../CodecFrame.js";
2120

2221
export default class OpusFrame extends CodecFrame {
23-
constructor(data, header) {
24-
super(
25-
header,
26-
data,
27-
((header[frameSize] * header[frameCount]) / 1000) * header[sampleRate],
28-
);
22+
constructor(data, header, samples) {
23+
super(header, data, samples);
2924
}
3025
}

src/codecs/opus/OpusParser.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ import {
2727
parseOggPage,
2828
enable,
2929
getHeaderFromUint8Array,
30+
preSkip,
31+
frameSize,
32+
frameCount,
33+
sampleRate,
3034
} from "../../constants.js";
3135
import Parser from "../Parser.js";
3236
import OpusFrame from "./OpusFrame.js";
@@ -40,6 +44,7 @@ export default class OpusParser extends Parser {
4044

4145
onCodec(this[codec]);
4246
this._identificationHeader = null;
47+
this._preSkipRemaining = null;
4348
}
4449

4550
get [codec]() {
@@ -67,7 +72,22 @@ export default class OpusParser extends Parser {
6772
this._headerCache,
6873
);
6974

70-
if (header) return new OpusFrame(segment, header);
75+
if (header) {
76+
if (this._preSkipRemaining === null)
77+
this._preSkipRemaining = header[preSkip];
78+
79+
let samples =
80+
((header[frameSize] * header[frameCount]) / 1000) *
81+
header[sampleRate];
82+
83+
if (this._preSkipRemaining > 0) {
84+
this._preSkipRemaining -= samples;
85+
samples =
86+
this._preSkipRemaining < 0 ? -this._preSkipRemaining : 0;
87+
}
88+
89+
return new OpusFrame(segment, header, samples);
90+
}
7191

7292
this._codecParser[logError](
7393
"Failed to parse Ogg Opus Header",

src/containers/ogg/OggPageHeader.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
uint8Array,
7171
dataView,
7272
} from "../../constants.js";
73+
import { readInt64le } from "../../utilities.js";
7374

7475
export default class OggPageHeader {
7576
static *[getHeader](codecParser, headerCache, readOffset) {
@@ -111,13 +112,7 @@ export default class OggPageHeader {
111112
// Byte (7-14 of 28)
112113
// * `FFFFFFFF|FFFFFFFF|FFFFFFFF|FFFFFFFF|FFFFFFFF|FFFFFFFF|FFFFFFFF|FFFFFFFF`
113114
// * Absolute Granule Position
114-
115-
/**
116-
* @todo Safari does not support getBigInt64, but it also doesn't support Ogg
117-
*/
118-
try {
119-
header[absoluteGranulePosition] = view.getBigInt64(6, true);
120-
} catch {}
115+
header[absoluteGranulePosition] = readInt64le(view, 6);
121116

122117
// Byte (15-18 of 28)
123118
// * `GGGGGGGG|GGGGGGGG|GGGGGGGG|GGGGGGGG`

src/containers/ogg/OggParser.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
codec,
2727
data,
2828
length,
29+
samples,
2930
segments,
3031
subarray,
3132
vorbis,
@@ -38,6 +39,7 @@ import {
3839
uint8Array,
3940
isLastPage,
4041
streamSerialNumber,
42+
absoluteGranulePosition,
4143
} from "../../constants.js";
4244

4345
import Parser from "../../codecs/Parser.js";
@@ -57,6 +59,7 @@ class OggStream {
5759
this._continuedPacket = new uint8Array();
5860
this._codec = null;
5961
this._isSupported = null;
62+
this._previousAbsoluteGranulePosition = null;
6063
}
6164

6265
get [codec]() {
@@ -149,6 +152,16 @@ class OggStream {
149152
);
150153
}
151154

155+
// set total samples in this ogg page
156+
if (this._previousAbsoluteGranulePosition !== null) {
157+
oggPage[samples] = Number(
158+
oggPage[absoluteGranulePosition] -
159+
this._previousAbsoluteGranulePosition,
160+
);
161+
}
162+
163+
this._previousAbsoluteGranulePosition = oggPage[absoluteGranulePosition];
164+
152165
if (this._isSupported) {
153166
const frame = this._parser[parseOggPage](oggPage);
154167
this._codecParser[mapFrameStats](frame);

src/utilities.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,37 @@ class BitReader {
185185
}
186186
}
187187

188+
/**
189+
* @todo Old versions of Safari do not support BigInt
190+
*/
191+
const readInt64le = (view, offset) => {
192+
try {
193+
return view.getBigInt64(offset, true);
194+
} catch {
195+
const sign = view.getUint8(offset + 7) & 0x80 ? -1 : 1;
196+
let firstPart = view.getUint32(offset, true);
197+
let secondPart = view.getUint32(offset + 4, true);
198+
199+
if (sign === -1) {
200+
firstPart = ~firstPart + 1;
201+
secondPart = ~secondPart + 1;
202+
}
203+
204+
if (secondPart > 0x000fffff) {
205+
console.warn("This platform does not support BigInt");
206+
}
207+
208+
return sign * (firstPart + secondPart * 2 ** 32);
209+
}
210+
};
211+
188212
export {
189213
crc8,
190214
flacCrc16,
191215
crc32Function,
192216
reverse,
193217
concatBuffers,
194218
bytesToString,
219+
readInt64le,
195220
BitReader,
196221
};

test/CodecParser.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ describe("CodecParser", () => {
311311
testParser("ogg.opus.framesize_60", mimeType, "opus", 251, 0);
312312
testParser("ogg.opus.surround", mimeType, "opus", 737, 0);
313313
testParser("ogg.opus.channel_family_255", mimeType, "opus", 284, 0);
314+
testParser("ogg.opus.last_page_trimming", mimeType, "opus", 51, 0);
314315
});
315316

316317
describe("Ogg Vorbis", () => {
@@ -322,7 +323,7 @@ describe("CodecParser", () => {
322323
"ogg.vorbis.setup_packets_separate_pages",
323324
mimeType,
324325
"vorbis",
325-
118,
326+
119,
326327
);
327328
testParser("metronome2.vorbis", mimeType, "vorbis", 3);
328329
});

0 commit comments

Comments
 (0)