Skip to content

Commit 1d81161

Browse files
committed
frames: the exact length upfront makes the streamed header exact (canonical WAV, pipes get true totals); WAV pads odd data
1 parent 6b9641e commit 1d81161

31 files changed

Lines changed: 140 additions & 90 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ await file.close()
116116
| `chapters` | `[{ time, title }]` (seconds): ID3 CHAP/CTOC, iTunes chpl | mp3, m4a |
117117
| `profile` | `'lc'`, `'he'` (HE-AAC), `'hev2'` (HE-AACv2, stereo) | aac, m4a |
118118
| `stream` | Emit as it encodes; `head()` after (see above) | all |
119+
| `frames` | With `stream`: the exact length, when known upfront, so the header goes out exact (a canonical WAV; a pipe gets true totals) | wav, aiff, caf, qoa, flac, mp3 |
119120

120121

121122
### Metadata

‎audio-encode.d.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export interface EncodeOptions {
4141
* totals the header can't know yet read "unknown" (WAV/AIFF 0xFFFFFFFF, CAF -1, QOA 0,
4242
* FLAC STREAMINFO 0) until `head()`; m4a/mp4 become fragmented. */
4343
stream?: boolean;
44+
/** With stream: the exact length in sample frames, when known upfront. The header goes out exact
45+
* (a canonical WAV, no RF64 reserve under 4 GB): a pipe gets the true totals, head() has less to do. */
46+
frames?: number;
4447
[key: string]: any;
4548
}
4649

‎package-lock.json‎

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

‎package.json‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@audio/encode",
3-
"version": "1.7.0",
3+
"version": "1.8.0",
44
"description": "Encode audio data in node or browser",
55
"type": "module",
66
"sideEffects": false,
@@ -64,17 +64,17 @@
6464
"homepage": "https://github.com/audiojs/encode#readme",
6565
"dependencies": {
6666
"@audio/encode-aac": "^1.2.0",
67-
"@audio/encode-aiff": "^1.3.0",
68-
"@audio/encode-caf": "^1.2.0",
69-
"@audio/encode-flac": "^1.3.0",
70-
"@audio/encode-mp3": "^1.3.0",
67+
"@audio/encode-aiff": "^1.4.0",
68+
"@audio/encode-caf": "^1.3.0",
69+
"@audio/encode-flac": "^1.4.0",
70+
"@audio/encode-mp3": "^1.4.0",
7171
"@audio/encode-ogg": "^1.3.0",
7272
"@audio/encode-opus": "^1.3.0",
73-
"@audio/encode-qoa": "^1.2.0",
74-
"@audio/encode-wav": "^1.4.0",
73+
"@audio/encode-qoa": "^1.3.0",
74+
"@audio/encode-wav": "^1.5.0",
7575
"@audio/encode-webm": "^1.3.0",
7676
"@audio/encode-wavpack": "^1.0.0",
77-
"@audio/encode-mp4": "^1.1.0",
77+
"@audio/encode-mp4": "^1.1.1",
7878
"@audio/encode-alac": "^1.0.1"
7979
},
8080
"devDependencies": {

‎packages/encode-aiff/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Input is `Float32Array[]` — one array per channel. Big-endian interleaving han
3434

3535
### Streaming output
3636

37-
`stream: true`: the header goes out with the first samples, sizes `0xFFFFFFFF` (read to the end), `meta` as an ID3 chunk before SSND. After `flush()`, `head()` returns the exact header to write over the start of a file.
37+
`stream: true`: the header goes out with the first samples, sizes `0xFFFFFFFF` (read to the end), `meta` as an ID3 chunk before SSND. After `flush()`, `head()` returns the exact header to write over the start of a file. With `frames` (the exact length, known upfront) the header goes out exact and `head()` has nothing to add.
3838

3939
## License
4040

‎packages/encode-aiff/aiff-encode.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export interface AiffEncodeOptions {
22
/** Emit bytes as they encode (metadata in the header); head() gives the final header. */
33
stream?: boolean;
4+
/** The exact length in sample frames, when known upfront: the streamed header goes out exact. */
5+
frames?: number;
46
sampleRate: number;
57
bitDepth?: 16 | 24;
68
}

‎packages/encode-aiff/aiff-encode.js‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* @param {number} [opts.bitDepth=16] - 16 or 24
66
* @param {boolean} [opts.stream] - emit the header and samples as they are encoded, sizes unknown
77
* (0xFFFFFFFF, read to the end) until head() gives the exact header; `meta` rides in an ID3 chunk
8+
* @param {number} [opts.frames] - with stream: the exact length, when known upfront: the header goes out exact
89
* @returns {{ encode, flush, free, head }}
910
*/
1011
export default async function aiff(opts) {
@@ -53,28 +54,28 @@ export default async function aiff(opts) {
5354
if (stream) {
5455
if (sent) return buf
5556
sent = true
56-
let h = header(false), out = new Uint8Array(h.length + buf.length)
57+
let h = header(opts.frames), out = new Uint8Array(h.length + buf.length)
5758
out.set(h); out.set(buf, h.length)
5859
return out
5960
}
6061
chunks.push(buf)
6162
return new Uint8Array(0)
6263
}
6364

64-
// FORM, COMM, [ID3], SSND header. `known`: exact sizes, else 0xFFFFFFFF (unknown, read to the end)
65-
function header(known) {
66-
let ch = nCh || opts.channels || 1, x = id3?.length || 0
65+
// FORM, COMM, [ID3], SSND header for `frames` sample frames; null: sizes 0xFFFFFFFF (unknown, read to the end)
66+
function header(frames) {
67+
let ch = nCh || opts.channels || 1, x = id3?.length || 0, bytes = (frames ?? 0) * ch * bytesPerSample
6768
let hdr = new Uint8Array(12 + 26 + x + 16), dv = new DataView(hdr.buffer), p = 0
68-
let ssndSize = totalBytes + 8, formSize = 4 + 26 + x + 8 + ssndSize + (totalBytes & 1)
69-
let fit = known && formSize <= 0xFFFFFFFF
69+
let ssndSize = bytes + 8, formSize = 4 + 26 + x + 8 + ssndSize + (bytes & 1)
70+
let fit = frames != null && formSize <= 0xFFFFFFFF
7071

7172
// FORM
7273
str('FORM'); dv.setUint32(p, fit ? formSize : 0xFFFFFFFF, false); p += 4; str('AIFF')
7374

7475
// COMM
7576
str('COMM'); dv.setUint32(p, 18, false); p += 4
7677
dv.setInt16(p, ch, false); p += 2
77-
dv.setUint32(p, fit ? numFrames : 0xFFFFFFFF, false); p += 4
78+
dv.setUint32(p, fit ? frames : 0xFFFFFFFF, false); p += 4
7879
dv.setInt16(p, depth, false); p += 2
7980
writeF80(dv, p, rate); p += 10
8081

@@ -94,13 +95,13 @@ export default async function aiff(opts) {
9495
function flush() {
9596
if (!nCh) nCh = opts.channels || 1
9697
if (stream) {
97-
exact = header(true)
98-
let h = sent ? null : (sent = true, header(false)), pad = totalBytes & 1 ? new Uint8Array(1) : null
98+
let h = sent ? null : (sent = true, header(opts.frames)), pad = totalBytes & 1 ? new Uint8Array(1) : null
99+
exact = opts.frames === numFrames ? null : header(numFrames) // the declared length held: nothing to patch
99100
if (!h) return pad || new Uint8Array(0)
100101
let out = new Uint8Array(h.length + (pad ? 1 : 0)); out.set(h); return out
101102
}
102103

103-
let hdr = header(true)
104+
let hdr = header(numFrames)
104105
let file = new Uint8Array(hdr.length + totalBytes + (totalBytes & 1)) // chunks pad to even
105106
file.set(hdr)
106107
let off = hdr.length

‎packages/encode-aiff/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@audio/encode-aiff",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"description": "Encode PCM audio samples to AIFF format",
55
"type": "module",
66
"main": "aiff-encode.js",

‎packages/encode-caf/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Input is `Float32Array[]` — one array per channel. Big-endian interleaving han
3636

3737
### Streaming output
3838

39-
`stream: true`: the header goes out with the first samples, the data chunk size `-1` (the CAF spec's "unknown, runs to the end"). After `flush()`, `head()` returns the exact header to write over the start of a file.
39+
`stream: true`: the header goes out with the first samples, the data chunk size `-1` (the CAF spec's "unknown, runs to the end"). After `flush()`, `head()` returns the exact header to write over the start of a file. With `frames` (the exact length, known upfront) the header goes out exact and `head()` has nothing to add.
4040

4141
## License
4242

‎packages/encode-caf/caf-encode.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export interface CafEncodeOptions {
22
/** Emit bytes as they encode (metadata in the header); head() gives the final header. */
33
stream?: boolean;
4+
/** The exact length in sample frames, when known upfront: the streamed header goes out exact. */
5+
frames?: number;
46
sampleRate: number;
57
bitDepth?: 16 | 32;
68
}

0 commit comments

Comments
 (0)