@quran-lab/tajweedformer 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,294 +1,325 @@
1
- // Ogg Opus for shared sessions (RFC 7845), the same wire format as the Swift SDK's OggOpusWriter
2
- // (tajweedformer-sdk-swift Sharing/OggOpus.swift) and every other SDK 2.0.0 (PLATFORM_CLIENT.md
3
- // section 10): mono, coded at 48 kHz, OpusHead with the true input rate, OpusTags, pages of about a
4
- // second of packets (50 at 20 ms, fewer when 255 lacing values would not hold them), cut into
5
- // segments at page boundaries (10, 20, 40, then 60 s), the last page of a finished session with the
6
- // end-of-stream flag and the encoder's final granule position. Plain JS, no dependencies.
7
-
8
- /** The sharing bitrate (bit/s): the reference's 192 kbit/s (about 86 MB an hour) until Quran Lab decides;
9
- * the one constant of this SDK. Apps may pass another (createSharingSession({ bitrate })). */
10
- export const SHARING_BITRATE_DEFAULT = 192000;
11
-
12
- /** The Opus settings sessions are shared with (Swift OpusSettings.shared, the opus-bench choice). */
13
- export const OPUS_SETTINGS = Object.freeze({
14
- application: 2049, // OPUS_APPLICATION_AUDIO
15
- bitrate: SHARING_BITRATE_DEFAULT,
16
- complexity: 10,
17
- vbr: true,
18
- constrainedVbr: false,
19
- bandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
20
- signal: -1000, // OPUS_AUTO
21
- frameMs: 20,
22
- });
23
-
24
- /** The rate Opus codes at; the sharing path always encodes at it. */
25
- export const CODED_RATE = 48000;
26
-
27
- /** Samples per packet at 48 kHz, and packets on a full page (about one second). */
28
- export const packetFrames = (s = OPUS_SETTINGS) => Math.trunc(48 * s.frameMs);
29
- export const pagePackets = (s = OPUS_SETTINGS) => Math.max(1, Math.round(1000 / s.frameMs));
30
-
31
- /** Seconds per segment: the first ones short, then a minute each; the last entry repeats. */
32
- export const SEGMENT_PLAN = Object.freeze([10, 20, 40, 60]);
33
- export const segmentSeconds = (n, plan = SEGMENT_PLAN) => plan[Math.min(n, plan.length) - 1];
34
-
35
- /** "seg-00001" for segment 1 (the R2 key and the local name, without ".opus"). */
36
- export const segmentName = (n) => `seg-${String(n).padStart(5, '0')}`;
37
-
38
- const CRC_TABLE = (() => {
39
- const t = new Uint32Array(256);
40
- for (let i = 0; i < 256; i++) {
41
- let r = i << 24;
42
- for (let k = 0; k < 8; k++) r = r & 0x80000000 ? (r << 1) ^ 0x04c11db7 : r << 1;
43
- t[i] = r >>> 0;
44
- }
45
- return t;
46
- })();
47
-
48
- /** The Ogg CRC-32: polynomial 0x04c11db7, no reflection, initial value 0. */
49
- export function oggCrc(bytes) {
50
- let c = 0;
51
- for (let i = 0; i < bytes.length; i++) c = ((c << 8) ^ CRC_TABLE[((c >>> 24) & 0xff) ^ bytes[i]]) >>> 0;
52
- return c >>> 0;
53
- }
54
-
55
- const utf8 = (s) => new TextEncoder().encode(s);
56
- function concat(parts) {
57
- const n = parts.reduce((k, p) => k + p.length, 0);
58
- const out = new Uint8Array(n);
59
- let at = 0;
60
- for (const p of parts) { out.set(p, at); at += p.length; }
61
- return out;
62
- }
63
-
64
- /** RFC 7845 identification header; inputRate is the capture rate (informational). */
65
- export function opusHead(preSkip, inputRate) {
66
- const d = new Uint8Array(19);
67
- d.set(utf8('OpusHead'), 0);
68
- d[8] = 1; d[9] = 1;
69
- const v = new DataView(d.buffer);
70
- v.setUint16(10, preSkip, true);
71
- v.setUint32(12, inputRate, true);
72
- v.setUint16(16, 0, true);
73
- d[18] = 0;
74
- return d;
75
- }
76
-
77
- /** RFC 7845 comment header with the vendor string and no comments. */
78
- export function opusTags(vendor) {
79
- const vb = utf8(vendor);
80
- const d = new Uint8Array(8 + 4 + vb.length + 4);
81
- d.set(utf8('OpusTags'), 0);
82
- const v = new DataView(d.buffer);
83
- v.setUint32(8, vb.length, true);
84
- d.set(vb, 12);
85
- v.setUint32(12 + vb.length, 0, true);
86
- return d;
87
- }
88
-
89
- /** One Ogg page holding whole packets. */
90
- export function oggPage(packets, { granule, serial, sequence, flags }) {
91
- const lacing = [];
92
- for (const p of packets) {
93
- let n = p.length;
94
- while (n >= 255) { lacing.push(255); n -= 255; }
95
- lacing.push(n);
96
- }
97
- if (lacing.length > 255) throw new RangeError('an Ogg page holds at most 255 segments');
98
- const head = new Uint8Array(27 + lacing.length);
99
- head.set(utf8('OggS'), 0);
100
- head[4] = 0;
101
- head[5] = flags;
102
- const v = new DataView(head.buffer);
103
- v.setBigUint64(6, BigInt.asUintN(64, BigInt(granule)), true);
104
- v.setUint32(14, serial >>> 0, true);
105
- v.setUint32(18, sequence >>> 0, true);
106
- v.setUint32(22, 0, true);
107
- head[26] = lacing.length;
108
- head.set(lacing, 27);
109
- const page = concat([head, ...packets]);
110
- new DataView(page.buffer).setUint32(22, oggCrc(page), true);
111
- return page;
112
- }
113
-
114
- /**
115
- * Every page of an Ogg stream, each checked (capture pattern, CRC); throws on a bad page.
116
- * @returns {{flags: number, granule: number, serial: number, sequence: number, packets: Uint8Array[]}[]}
117
- */
118
- export function readPages(bytes) {
119
- const out = [];
120
- let at = 0;
121
- while (at < bytes.length) {
122
- if (!(bytes[at] === 0x4f && bytes[at + 1] === 0x67 && bytes[at + 2] === 0x67 && bytes[at + 3] === 0x53)) throw new Error(`no OggS at ${at}`);
123
- const n = bytes[at + 26];
124
- const lacing = bytes.subarray(at + 27, at + 27 + n);
125
- const len = 27 + n + lacing.reduce((k, x) => k + x, 0);
126
- if (at + len > bytes.length) throw new Error(`page at ${at} is cut`);
127
- const page = bytes.slice(at, at + len);
128
- const v = new DataView(page.buffer);
129
- const crc = v.getUint32(22, true);
130
- v.setUint32(22, 0, true);
131
- if (oggCrc(page) !== crc) throw new Error(`page CRC at ${at}`);
132
- const packets = [];
133
- let parts = [], o = at + 27 + n;
134
- for (const l of lacing) {
135
- parts.push(bytes.subarray(o, o + l));
136
- o += l;
137
- if (l < 255) { packets.push(concat(parts)); parts = []; }
138
- }
139
- out.push({ flags: bytes[at + 5], granule: Number(v.getBigInt64(6, true)), serial: v.getUint32(14, true), sequence: v.getUint32(18, true), packets });
140
- at += len;
141
- }
142
- return out;
143
- }
144
-
145
- /** The length of the whole pages at the start of `bytes` (each with a valid CRC) and the last one's
146
- * granule position (Swift ShareFiles.wholePages): what survives a torn write. */
147
- export function wholePages(bytes) {
148
- let at = 0, granule = 0;
149
- while (at + 27 <= bytes.length && bytes[at] === 0x4f && bytes[at + 1] === 0x67 && bytes[at + 2] === 0x67 && bytes[at + 3] === 0x53) {
150
- const n = bytes[at + 26];
151
- if (at + 27 + n > bytes.length) break;
152
- let len = 0;
153
- for (let i = 0; i < n; i++) len += bytes[at + 27 + i];
154
- const end = at + 27 + n + len;
155
- if (end > bytes.length) break;
156
- const page = bytes.slice(at, end);
157
- const v = new DataView(page.buffer);
158
- const crc = v.getUint32(22, true);
159
- v.setUint32(22, 0, true);
160
- if (oggCrc(page) !== crc) break;
161
- granule = Number(v.getBigInt64(6, true));
162
- at = end;
163
- }
164
- return { keep: at, granule };
165
- }
166
-
167
- /**
168
- * Writes one session's audio as Ogg Opus pages while the session runs (Swift OggOpusWriter).
169
- * Synchronous: each finished page goes to `onPage(n, bytes)` (n = its segment), each closed segment
170
- * to `onSegment({ n, granule, last })`; the caller persists them. `plan` empty: one segment.
171
- *
172
- * @param {object} o
173
- * @param {{ preSkip: number, encode(x: Float32Array): Uint8Array[], flush(): Uint8Array[], finalGranulePosition: number }} o.encoder
174
- * an encoder taking 48 kHz input (encoder.js)
175
- * @param {{ process(x: Float32Array, sink: Function): void, flush(sink: Function): void } | null} o.resampler
176
- * input rate to 48 kHz (resampler.js), null when the input is 48 kHz
177
- * @param {number} o.inputRate the capture rate (8000 to 384000, whole Hz)
178
- */
179
- export class OggOpusWriter {
180
- constructor({ encoder, resampler = null, inputRate, plan = SEGMENT_PLAN, settings = OPUS_SETTINGS, serial, vendor, onPage, onSegment = () => {} }) {
181
- if (!(inputRate >= 8000 && inputRate <= 384000 && Number.isInteger(inputRate))) throw new RangeError(`unsupported input rate ${inputRate} Hz`);
182
- if (inputRate !== CODED_RATE && !resampler) throw new TypeError('a resampler is needed for input that is not 48 kHz');
183
- this.encoder = encoder;
184
- this.resampler = inputRate === CODED_RATE ? null : resampler;
185
- this.inputRate = inputRate;
186
- this.plan = plan;
187
- this.settings = settings;
188
- this.serial = serial === undefined ? (Math.random() * 0x100000000) >>> 0 : serial >>> 0;
189
- this.onPage = onPage;
190
- this.onSegment = onSegment;
191
- this.packetFrames = packetFrames(settings);
192
- this.pagePackets = pagePackets(settings);
193
- this.segment = 0;
194
- this.open = false;
195
- this.segmentPackets = 0;
196
- this.sequence = 0;
197
- this.inputFrames = 0;
198
- this.nonFiniteSamples = 0;
199
- this.ended = false;
200
- this.packets = [];
201
- this.paged = 0;
202
- this.lastGranule = 0;
203
- this._clean = new Float32Array(0);
204
- this._sink = (x) => { for (const p of this.encoder.encode(x)) this.packets.push(p); };
205
- this._page([opusHead(encoder.preSkip, inputRate)], 0, 0x02, false);
206
- this._page([opusTags(vendor || 'QuranLab SDK')], 0, 0, false);
207
- }
208
-
209
- get preSkip() { return this.encoder.preSkip; }
210
- /** Seconds of audio written so far (at the input rate). */
211
- get duration() { return this.inputFrames / this.inputRate; }
212
-
213
- /** Mono samples at the input rate; non-finite samples are written as silence. */
214
- append(samples) {
215
- if (!samples.length || this.ended) return;
216
- this.inputFrames += samples.length;
217
- if (this._clean.length < samples.length) this._clean = new Float32Array(samples.length);
218
- const c = this._clean.subarray(0, samples.length);
219
- let bad = 0;
220
- for (let i = 0; i < samples.length; i++) {
221
- const v = samples[i];
222
- if (Number.isFinite(v)) c[i] = v; else { c[i] = 0; bad++; }
223
- }
224
- this.nonFiniteSamples += bad;
225
- if (this.resampler) this.resampler.process(c, this._sink); else this._sink(c);
226
- this._writePages(false);
227
- }
228
-
229
- /** Flushes the resampler and the encoder, writes the last pages, closes the last segment. Returns
230
- * the duration in ms. */
231
- finish() {
232
- if (!this.ended) {
233
- this.ended = true;
234
- if (this.resampler) this.resampler.flush(this._sink);
235
- for (const p of this.encoder.flush()) this.packets.push(p);
236
- this._writePages(true);
237
- this._close(true);
238
- }
239
- return Math.round(this.duration * 1000);
240
- }
241
-
242
- /** Stops without finishing (the caller removes what was written). */
243
- abandon() { this.ended = true; this.open = false; }
244
-
245
- _segmentLimit(n) {
246
- return this.plan.length ? Math.round(segmentSeconds(n, this.plan) * 1000 / this.settings.frameMs) : Infinity;
247
- }
248
-
249
- _writePages(final) {
250
- while (this.packets.length) {
251
- const next = this.open ? this.segment : this.segment + 1;
252
- const used = this.open ? this.segmentPackets : 0;
253
- const room = this._segmentLimit(next) - used;
254
- const want = Math.min(this.pagePackets, room);
255
- let take = 0, lacing = 0;
256
- while (take < Math.min(want, this.packets.length)) {
257
- const l = Math.floor(this.packets[take].length / 255) + 1;
258
- if (lacing + l > 255) break;
259
- lacing += l;
260
- take++;
261
- }
262
- const full = take === want || take < this.packets.length;
263
- if (!(full || final)) return;
264
- const last = final && take === this.packets.length;
265
- const chunk = this.packets.splice(0, take);
266
- let granule = (this.paged + take) * this.packetFrames;
267
- if (last) granule = Math.max(Math.min(this.encoder.finalGranulePosition, granule), this.paged * this.packetFrames);
268
- this._page(chunk, granule, last ? 0x04 : 0, true);
269
- }
270
- }
271
-
272
- _close(last) {
273
- if (!this.open) return;
274
- this.open = false;
275
- this.onSegment({ n: this.segment, granule: this.lastGranule, last });
276
- }
277
-
278
- _page(packets, granule, flags, audio) {
279
- const bytes = oggPage(packets, { granule, serial: this.serial, sequence: this.sequence, flags });
280
- if (!this.open) {
281
- this.segment += 1;
282
- this.segmentPackets = 0;
283
- this.open = true;
284
- }
285
- this.onPage(this.segment, bytes);
286
- this.sequence += 1;
287
- this.lastGranule = granule;
288
- if (audio) {
289
- this.paged += packets.length;
290
- this.segmentPackets += packets.length;
291
- if (!(flags & 0x04) && this.segmentPackets >= this._segmentLimit(this.segment)) this._close(false);
292
- }
293
- }
294
- }
1
+ // Ogg Opus for shared sessions (RFC 7845), the same wire format as the Swift SDK's OggOpusWriter
2
+ // (tajweedformer-sdk-swift Sharing/OggOpus.swift) and every other SDK 2.0.0 (PLATFORM_CLIENT.md
3
+ // section 10): mono, coded at 48 kHz, OpusHead with the true input rate, OpusTags, pages of about a
4
+ // second of packets (50 at 20 ms, fewer when 255 lacing values would not hold them), cut into
5
+ // segments at page boundaries (10, 20, 40, then 60 s), the last page of a finished session with the
6
+ // end-of-stream flag and the encoder's final granule position. Plain JS, no dependencies.
7
+
8
+ /** The sharing bitrate (bit/s): the reference's 192 kbit/s (about 86 MB an hour) until Quran Lab decides;
9
+ * the one constant of this SDK. Apps may pass another (createSharingSession({ bitrate })). */
10
+ export const SHARING_BITRATE_DEFAULT = 192000;
11
+
12
+ /** The Opus settings sessions are shared with (Swift OpusSettings.shared, the opus-bench choice). */
13
+ export const OPUS_SETTINGS = Object.freeze({
14
+ application: 2049, // OPUS_APPLICATION_AUDIO
15
+ bitrate: SHARING_BITRATE_DEFAULT,
16
+ complexity: 10,
17
+ vbr: true,
18
+ constrainedVbr: false,
19
+ bandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
20
+ signal: -1000, // OPUS_AUTO
21
+ frameMs: 20,
22
+ });
23
+
24
+ /** The rate Opus codes at; the sharing path always encodes at it. */
25
+ export const CODED_RATE = 48000;
26
+
27
+ /** Samples per packet at 48 kHz, and packets on a full page (about one second). */
28
+ export const packetFrames = (s = OPUS_SETTINGS) => Math.trunc(48 * s.frameMs);
29
+ export const pagePackets = (s = OPUS_SETTINGS) => Math.max(1, Math.round(1000 / s.frameMs));
30
+
31
+ /** Seconds per segment: the first ones short, then a minute each; the last entry repeats. */
32
+ export const SEGMENT_PLAN = Object.freeze([10, 20, 40, 60]);
33
+ export const segmentSeconds = (n, plan = SEGMENT_PLAN) => plan[Math.min(n, plan.length) - 1];
34
+
35
+ /** "seg-00001" for segment 1 (the R2 key and the local name, without ".opus"). */
36
+ export const segmentName = (n) => `seg-${String(n).padStart(5, '0')}`;
37
+
38
+ const CRC_TABLE = (() => {
39
+ const t = new Uint32Array(256);
40
+ for (let i = 0; i < 256; i++) {
41
+ let r = i << 24;
42
+ for (let k = 0; k < 8; k++) r = r & 0x80000000 ? (r << 1) ^ 0x04c11db7 : r << 1;
43
+ t[i] = r >>> 0;
44
+ }
45
+ return t;
46
+ })();
47
+
48
+ /** The Ogg CRC-32: polynomial 0x04c11db7, no reflection, initial value 0. */
49
+ export function oggCrc(bytes) {
50
+ let c = 0;
51
+ for (let i = 0; i < bytes.length; i++) c = ((c << 8) ^ CRC_TABLE[((c >>> 24) & 0xff) ^ bytes[i]]) >>> 0;
52
+ return c >>> 0;
53
+ }
54
+
55
+ const utf8 = (s) => new TextEncoder().encode(s);
56
+ function concat(parts) {
57
+ const n = parts.reduce((k, p) => k + p.length, 0);
58
+ const out = new Uint8Array(n);
59
+ let at = 0;
60
+ for (const p of parts) { out.set(p, at); at += p.length; }
61
+ return out;
62
+ }
63
+
64
+ /** RFC 7845 identification header; inputRate is the capture rate (informational). */
65
+ export function opusHead(preSkip, inputRate) {
66
+ const d = new Uint8Array(19);
67
+ d.set(utf8('OpusHead'), 0);
68
+ d[8] = 1; d[9] = 1;
69
+ const v = new DataView(d.buffer);
70
+ v.setUint16(10, preSkip, true);
71
+ v.setUint32(12, inputRate, true);
72
+ v.setUint16(16, 0, true);
73
+ d[18] = 0;
74
+ return d;
75
+ }
76
+
77
+ /** RFC 7845 comment header with the vendor string and no comments. */
78
+ export function opusTags(vendor) {
79
+ const vb = utf8(vendor);
80
+ const d = new Uint8Array(8 + 4 + vb.length + 4);
81
+ d.set(utf8('OpusTags'), 0);
82
+ const v = new DataView(d.buffer);
83
+ v.setUint32(8, vb.length, true);
84
+ d.set(vb, 12);
85
+ v.setUint32(12 + vb.length, 0, true);
86
+ return d;
87
+ }
88
+
89
+ /** One Ogg page holding whole packets. */
90
+ export function oggPage(packets, { granule, serial, sequence, flags }) {
91
+ const lacing = [];
92
+ for (const p of packets) {
93
+ let n = p.length;
94
+ while (n >= 255) { lacing.push(255); n -= 255; }
95
+ lacing.push(n);
96
+ }
97
+ if (lacing.length > 255) throw new RangeError('an Ogg page holds at most 255 segments');
98
+ const head = new Uint8Array(27 + lacing.length);
99
+ head.set(utf8('OggS'), 0);
100
+ head[4] = 0;
101
+ head[5] = flags;
102
+ const v = new DataView(head.buffer);
103
+ v.setBigUint64(6, BigInt.asUintN(64, BigInt(granule)), true);
104
+ v.setUint32(14, serial >>> 0, true);
105
+ v.setUint32(18, sequence >>> 0, true);
106
+ v.setUint32(22, 0, true);
107
+ head[26] = lacing.length;
108
+ head.set(lacing, 27);
109
+ const page = concat([head, ...packets]);
110
+ new DataView(page.buffer).setUint32(22, oggCrc(page), true);
111
+ return page;
112
+ }
113
+
114
+ /**
115
+ * Every page of an Ogg stream, each checked (capture pattern, CRC); throws on a bad page.
116
+ * @returns {{flags: number, granule: number, serial: number, sequence: number, packets: Uint8Array[]}[]}
117
+ */
118
+ export function readPages(bytes) {
119
+ const out = [];
120
+ let at = 0;
121
+ while (at < bytes.length) {
122
+ if (!(bytes[at] === 0x4f && bytes[at + 1] === 0x67 && bytes[at + 2] === 0x67 && bytes[at + 3] === 0x53)) throw new Error(`no OggS at ${at}`);
123
+ const n = bytes[at + 26];
124
+ const lacing = bytes.subarray(at + 27, at + 27 + n);
125
+ const len = 27 + n + lacing.reduce((k, x) => k + x, 0);
126
+ if (at + len > bytes.length) throw new Error(`page at ${at} is cut`);
127
+ const page = bytes.slice(at, at + len);
128
+ const v = new DataView(page.buffer);
129
+ const crc = v.getUint32(22, true);
130
+ v.setUint32(22, 0, true);
131
+ if (oggCrc(page) !== crc) throw new Error(`page CRC at ${at}`);
132
+ const packets = [];
133
+ let parts = [], o = at + 27 + n;
134
+ for (const l of lacing) {
135
+ parts.push(bytes.subarray(o, o + l));
136
+ o += l;
137
+ if (l < 255) { packets.push(concat(parts)); parts = []; }
138
+ }
139
+ out.push({ flags: bytes[at + 5], granule: Number(v.getBigInt64(6, true)), serial: v.getUint32(14, true), sequence: v.getUint32(18, true), packets });
140
+ at += len;
141
+ }
142
+ return out;
143
+ }
144
+
145
+ /** The length of the whole pages at the start of `bytes` (each with a valid CRC) and the last one's
146
+ * granule position (Swift ShareFiles.wholePages): what survives a torn write. */
147
+ export function wholePages(bytes) {
148
+ let at = 0, granule = 0;
149
+ while (at + 27 <= bytes.length && bytes[at] === 0x4f && bytes[at + 1] === 0x67 && bytes[at + 2] === 0x67 && bytes[at + 3] === 0x53) {
150
+ const n = bytes[at + 26];
151
+ if (at + 27 + n > bytes.length) break;
152
+ let len = 0;
153
+ for (let i = 0; i < n; i++) len += bytes[at + 27 + i];
154
+ const end = at + 27 + n + len;
155
+ if (end > bytes.length) break;
156
+ const page = bytes.slice(at, end);
157
+ const v = new DataView(page.buffer);
158
+ const crc = v.getUint32(22, true);
159
+ v.setUint32(22, 0, true);
160
+ if (oggCrc(page) !== crc) break;
161
+ granule = Number(v.getBigInt64(6, true));
162
+ at = end;
163
+ }
164
+ return { keep: at, granule };
165
+ }
166
+
167
+ /**
168
+ * Writes one session's audio as Ogg Opus pages while the session runs (Swift OggOpusWriter).
169
+ * Synchronous: each finished page goes to `onPage(n, bytes)` (n = its segment), each closed segment
170
+ * to `onSegment({ n, granule, last })`; the caller persists them. `plan` empty: one segment.
171
+ *
172
+ * @param {object} o
173
+ * @param {{ preSkip: number, encode(x: Float32Array): Uint8Array[], flush(): Uint8Array[], finalGranulePosition: number }} o.encoder
174
+ * an encoder taking 48 kHz input (encoder.js)
175
+ * @param {{ process(x: Float32Array, sink: Function): void, flush(sink: Function): void } | null} o.resampler
176
+ * input rate to 48 kHz (resampler.js), null when the input is 48 kHz
177
+ * @param {number} o.inputRate the capture rate (8000 to 384000, whole Hz)
178
+ * @param {(n: number) => number|null} [o.bitrate] the bit rate of segment n, asked when the segment opens
179
+ * (just before its first packet is coded; the previous segment is closed by then); null keeps the
180
+ * current one. Needs an encoder with setBitrate and beforeFrame (encoder.js).
181
+ * @param {(s: {n: number, bitrate: number|null}) => void} [o.onSegmentOpen] a segment opened (its first packet)
182
+ */
183
+ export class OggOpusWriter {
184
+ constructor({ encoder, resampler = null, inputRate, plan = SEGMENT_PLAN, settings = OPUS_SETTINGS, serial, vendor, onPage, onSegment = () => {},
185
+ bitrate = null, onSegmentOpen = () => {} }) {
186
+ if (!(inputRate >= 8000 && inputRate <= 384000 && Number.isInteger(inputRate))) throw new RangeError(`unsupported input rate ${inputRate} Hz`);
187
+ if (inputRate !== CODED_RATE && !resampler) throw new TypeError('a resampler is needed for input that is not 48 kHz');
188
+ this.encoder = encoder;
189
+ this.resampler = inputRate === CODED_RATE ? null : resampler;
190
+ this.inputRate = inputRate;
191
+ this.plan = plan;
192
+ this.settings = settings;
193
+ this.serial = serial === undefined ? (Math.random() * 0x100000000) >>> 0 : serial >>> 0;
194
+ this.onPage = onPage;
195
+ this.onSegment = onSegment;
196
+ this.packetFrames = packetFrames(settings);
197
+ this.pagePackets = pagePackets(settings);
198
+ this.segment = 0;
199
+ this.open = false;
200
+ this.segmentPackets = 0;
201
+ this.sequence = 0;
202
+ this.inputFrames = 0;
203
+ this.nonFiniteSamples = 0;
204
+ this.ended = false;
205
+ this.packets = [];
206
+ this.paged = 0;
207
+ this.lastGranule = 0;
208
+ this._clean = new Float32Array(0);
209
+ this.bitrateFor = bitrate;
210
+ this.onSegmentOpen = onSegmentOpen;
211
+ /** segment number -> the bit rate its packets were coded at */
212
+ this.segmentBitrates = new Map();
213
+ this._openN = 1; // the next segment to open
214
+ this._openAt = 0; // the index of its first packet
215
+ // packets reach the writer one by one as they are coded, so a segment can be closed and the next one's
216
+ // bitrate set between two packets (an encoder seam without the hook hands them over per call)
217
+ this._push = (p) => { this.packets.push(p); };
218
+ this._sink = (x) => { const r = this.encoder.encode(x, this._push); if (r) for (const p of r) this.packets.push(p); };
219
+ if (encoder && 'beforeFrame' in encoder) encoder.beforeFrame = () => this._beforePacket(this.paged + this.packets.length);
220
+ this._page([opusHead(encoder.preSkip, inputRate)], 0, 0x02, false);
221
+ this._page([opusTags(vendor || 'QuranLab SDK')], 0, 0, false);
222
+ }
223
+
224
+ get preSkip() { return this.encoder.preSkip; }
225
+ /** Seconds of audio written so far (at the input rate). */
226
+ get duration() { return this.inputFrames / this.inputRate; }
227
+
228
+ /** Mono samples at the input rate; non-finite samples are written as silence. */
229
+ append(samples) {
230
+ if (!samples.length || this.ended) return;
231
+ this.inputFrames += samples.length;
232
+ if (this._clean.length < samples.length) this._clean = new Float32Array(samples.length);
233
+ const c = this._clean.subarray(0, samples.length);
234
+ let bad = 0;
235
+ for (let i = 0; i < samples.length; i++) {
236
+ const v = samples[i];
237
+ if (Number.isFinite(v)) c[i] = v; else { c[i] = 0; bad++; }
238
+ }
239
+ this.nonFiniteSamples += bad;
240
+ if (this.resampler) this.resampler.process(c, this._sink); else this._sink(c);
241
+ this._writePages(false);
242
+ }
243
+
244
+ /** Flushes the resampler and the encoder, writes the last pages, closes the last segment. Returns
245
+ * the duration in ms. */
246
+ finish() {
247
+ if (!this.ended) {
248
+ this.ended = true;
249
+ if (this.resampler) this.resampler.flush(this._sink);
250
+ const r = this.encoder.flush(this._push);
251
+ if (r) for (const p of r) this.packets.push(p);
252
+ this._writePages(true);
253
+ this._close(true);
254
+ }
255
+ return Math.round(this.duration * 1000);
256
+ }
257
+
258
+ /** Stops without finishing (the caller removes what was written). */
259
+ abandon() { this.ended = true; this.open = false; }
260
+
261
+ // Before packet k is coded: at the first packet of a segment, close the previous one (all its packets are
262
+ // coded, so its last page can be written now) and set the new segment's bitrate.
263
+ _beforePacket(k) {
264
+ if (k !== this._openAt) return;
265
+ const n = this._openN;
266
+ if (n > 1) this._writePages(false);
267
+ const want = this.bitrateFor ? this.bitrateFor(n) : null;
268
+ if (want && typeof this.encoder.setBitrate === 'function') this.encoder.setBitrate(want);
269
+ const used = this.encoder.bitrate ?? this.settings.bitrate ?? null;
270
+ this.segmentBitrates.set(n, used);
271
+ this._openN = n + 1;
272
+ this._openAt += this._segmentLimit(n);
273
+ this.onSegmentOpen({ n, bitrate: used });
274
+ }
275
+
276
+ _segmentLimit(n) {
277
+ return this.plan.length ? Math.round(segmentSeconds(n, this.plan) * 1000 / this.settings.frameMs) : Infinity;
278
+ }
279
+
280
+ _writePages(final) {
281
+ while (this.packets.length) {
282
+ const next = this.open ? this.segment : this.segment + 1;
283
+ const used = this.open ? this.segmentPackets : 0;
284
+ const room = this._segmentLimit(next) - used;
285
+ const want = Math.min(this.pagePackets, room);
286
+ let take = 0, lacing = 0;
287
+ while (take < Math.min(want, this.packets.length)) {
288
+ const l = Math.floor(this.packets[take].length / 255) + 1;
289
+ if (lacing + l > 255) break;
290
+ lacing += l;
291
+ take++;
292
+ }
293
+ const full = take === want || take < this.packets.length;
294
+ if (!(full || final)) return;
295
+ const last = final && take === this.packets.length;
296
+ const chunk = this.packets.splice(0, take);
297
+ let granule = (this.paged + take) * this.packetFrames;
298
+ if (last) granule = Math.max(Math.min(this.encoder.finalGranulePosition, granule), this.paged * this.packetFrames);
299
+ this._page(chunk, granule, last ? 0x04 : 0, true);
300
+ }
301
+ }
302
+
303
+ _close(last) {
304
+ if (!this.open) return;
305
+ this.open = false;
306
+ this.onSegment({ n: this.segment, granule: this.lastGranule, last, bitrate: this.segmentBitrates.get(this.segment) ?? null });
307
+ }
308
+
309
+ _page(packets, granule, flags, audio) {
310
+ const bytes = oggPage(packets, { granule, serial: this.serial, sequence: this.sequence, flags });
311
+ if (!this.open) {
312
+ this.segment += 1;
313
+ this.segmentPackets = 0;
314
+ this.open = true;
315
+ }
316
+ this.onPage(this.segment, bytes);
317
+ this.sequence += 1;
318
+ this.lastGranule = granule;
319
+ if (audio) {
320
+ this.paged += packets.length;
321
+ this.segmentPackets += packets.length;
322
+ if (!(flags & 0x04) && this.segmentPackets >= this._segmentLimit(this.segment)) this._close(false);
323
+ }
324
+ }
325
+ }