Skip to content

Commit c8098a9

Browse files
committed
Add meta
1 parent 2d5a047 commit c8098a9

9 files changed

Lines changed: 417 additions & 0 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,30 @@ Works with any async iterable source.
8181
| `application` | `'audio'`, `'voip'`, or `'lowdelay'` | opus |
8282

8383

84+
### Metadata
85+
86+
Splice tags, pictures, markers and regions into encoded bytes. Available for `wav`, `mp3`, `flac`.
87+
88+
```js
89+
import encode from 'encode-audio'
90+
import { wav } from 'encode-audio/meta'
91+
92+
let bytes = await encode.wav(channelData, { sampleRate: 44100 })
93+
let out = wav(bytes, {
94+
meta: { title: 'Hare Krishna', artist: 'Prabhupada', year: '1966' },
95+
markers: [{ sample: 44100, label: 'verse' }],
96+
regions: [{ sample: 88200, length: 44100, label: 'chorus' }]
97+
})
98+
```
99+
100+
Each codec sub-package also exposes its writer directly:
101+
102+
```js
103+
import { writeMeta } from '@audio/encode-mp3/meta'
104+
let tagged = writeMeta(mp3Bytes, { meta: { title: 'foo' } })
105+
```
106+
107+
84108
## See also
85109

86110
* [audio-decode](https://github.com/audiojs/audio-decode) – decode any audio format to raw samples.

meta.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Meta writers — re-export from codec packages.
3+
* @module encode-audio/meta
4+
*
5+
* import { wav, mp3, flac } from 'encode-audio/meta'
6+
* let out = wav(bytes, { meta, markers, regions })
7+
*/
8+
9+
export { writeMeta as wav } from '@audio/encode-wav/meta'
10+
export { writeMeta as mp3 } from '@audio/encode-mp3/meta'
11+
export { writeMeta as flac } from '@audio/encode-flac/meta'

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
"default": "./audio-encode.js"
1414
},
1515
"./stream": "./stream.js",
16+
"./meta": "./meta.js",
1617
"./package.json": "./package.json"
1718
},
1819
"files": [
1920
"audio-encode.js",
2021
"audio-encode.d.ts",
2122
"stream.js",
2223
"stream.d.ts",
24+
"meta.js",
2325
"LICENSE"
2426
],
2527
"workspaces": [

packages/encode-flac/meta.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* FLAC metadata writer — Vorbis comments, pictures.
3+
* @module @audio/encode-flac/meta
4+
*/
5+
6+
const TE = new TextEncoder()
7+
8+
// ── Constants ───────────────────────────────────────────────────────────
9+
10+
const STREAMINFO = 0, VORBIS_COMMENT = 4, PICTURE = 6, PADDING = 1
11+
12+
const VORBIS_MAP_REV = {
13+
title: 'TITLE', artist: 'ARTIST', album: 'ALBUM', albumartist: 'ALBUMARTIST',
14+
composer: 'COMPOSER', genre: 'GENRE', year: 'DATE', track: 'TRACKNUMBER',
15+
disc: 'DISCNUMBER', bpm: 'BPM', key: 'KEY', comment: 'COMMENT',
16+
copyright: 'COPYRIGHT', isrc: 'ISRC', publisher: 'PUBLISHER', software: 'ENCODER',
17+
lyrics: 'LYRICS'
18+
}
19+
20+
// ── Binary helpers ──────────────────────────────────────────────────────
21+
22+
function u24be(b, o) { return (b[o] << 16) | (b[o + 1] << 8) | b[o + 2] }
23+
function wu32le(b, o, v) { b[o] = v; b[o + 1] = v >>> 8; b[o + 2] = v >>> 16; b[o + 3] = v >>> 24 }
24+
function wu32be(b, o, v) { b[o] = (v >>> 24) & 0xff; b[o + 1] = (v >>> 16) & 0xff; b[o + 2] = (v >>> 8) & 0xff; b[o + 3] = v & 0xff }
25+
function fourcc(b, o) { return String.fromCharCode(b[o], b[o + 1], b[o + 2], b[o + 3]) }
26+
27+
// ── Block builders ──────────────────────────────────────────────────────
28+
29+
function buildVorbisComment(meta) {
30+
let vendor = TE.encode('audio')
31+
let entries = []
32+
for (let k in VORBIS_MAP_REV) {
33+
let v = meta[k]
34+
if (v == null || v === '') continue
35+
entries.push(TE.encode(`${VORBIS_MAP_REV[k]}=${v}`))
36+
}
37+
let size = 4 + vendor.length + 4
38+
for (let e of entries) size += 4 + e.length
39+
let out = new Uint8Array(size)
40+
let pos = 0
41+
wu32le(out, pos, vendor.length); pos += 4
42+
out.set(vendor, pos); pos += vendor.length
43+
wu32le(out, pos, entries.length); pos += 4
44+
for (let e of entries) { wu32le(out, pos, e.length); pos += 4; out.set(e, pos); pos += e.length }
45+
return out
46+
}
47+
48+
function buildFlacPicture(p) {
49+
let mimeBytes = TE.encode(p.mime || 'image/jpeg')
50+
let descBytes = TE.encode(p.description || '')
51+
let size = 4 + 4 + mimeBytes.length + 4 + descBytes.length + 16 + 4 + p.data.length
52+
let out = new Uint8Array(size)
53+
let pos = 0
54+
wu32be(out, pos, p.type ?? 3); pos += 4
55+
wu32be(out, pos, mimeBytes.length); pos += 4
56+
out.set(mimeBytes, pos); pos += mimeBytes.length
57+
wu32be(out, pos, descBytes.length); pos += 4
58+
out.set(descBytes, pos); pos += descBytes.length
59+
pos += 16 // width/height/depth/colors = 0
60+
wu32be(out, pos, p.data.length); pos += 4
61+
out.set(p.data, pos)
62+
return out
63+
}
64+
65+
function buildFlacBlock(type, body, last) {
66+
let out = new Uint8Array(4 + body.length)
67+
out[0] = (last ? 0x80 : 0) | (type & 0x7f)
68+
out[1] = (body.length >> 16) & 0xff
69+
out[2] = (body.length >> 8) & 0xff
70+
out[3] = body.length & 0xff
71+
out.set(body, 4)
72+
return out
73+
}
74+
75+
/** Splice meta into FLAC bytes. Returns new Uint8Array. */
76+
export function writeMeta(bytes, { meta = {} } = {}) {
77+
if (bytes.length < 4 || fourcc(bytes, 0) !== 'fLaC') return bytes
78+
let off = 4, streamInfo = null, others = []
79+
while (off + 4 <= bytes.length) {
80+
let hdr = bytes[off]
81+
let last = !!(hdr & 0x80), type = hdr & 0x7f
82+
let size = u24be(bytes, off + 1)
83+
let body = bytes.subarray(off + 4, off + 4 + size)
84+
if (type === STREAMINFO) streamInfo = body
85+
else if (type !== VORBIS_COMMENT && type !== PICTURE && type !== PADDING) others.push({ type, body })
86+
off += 4 + size
87+
if (last) break
88+
}
89+
let audioStart = off
90+
if (!streamInfo) return bytes
91+
92+
let blocks = []
93+
blocks.push({ type: STREAMINFO, body: streamInfo })
94+
for (let o of others) blocks.push(o)
95+
blocks.push({ type: VORBIS_COMMENT, body: buildVorbisComment(meta) })
96+
if (meta.pictures) for (let p of meta.pictures) blocks.push({ type: PICTURE, body: buildFlacPicture(p) })
97+
98+
let headerSize = 4
99+
let encoded = []
100+
for (let i = 0; i < blocks.length; i++) {
101+
let b = buildFlacBlock(blocks[i].type, blocks[i].body, i === blocks.length - 1)
102+
encoded.push(b)
103+
headerSize += b.length
104+
}
105+
let audioPart = bytes.subarray(audioStart)
106+
let out = new Uint8Array(headerSize + audioPart.length)
107+
out.set(TE.encode('fLaC'), 0)
108+
let pos = 4
109+
for (let e of encoded) { out.set(e, pos); pos += e.length }
110+
out.set(audioPart, pos)
111+
return out
112+
}

packages/encode-flac/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
"types": "flac-encode.d.ts",
88
"exports": {
99
".": "./flac-encode.js",
10+
"./meta": "./meta.js",
1011
"./package.json": "./package.json"
1112
},
1213
"files": [
1314
"flac-encode.js",
1415
"flac-encode.d.ts",
16+
"meta.js",
1517
"LICENSE"
1618
],
1719
"keywords": [

packages/encode-mp3/meta.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* ID3v2 metadata writer for MP3 files.
3+
* @module @audio/encode-mp3/meta
4+
*/
5+
6+
const TE = new TextEncoder()
7+
8+
// ── Constants ───────────────────────────────────────────────────────────
9+
10+
const ID3_MAP_REV = {
11+
title: 'TIT2', artist: 'TPE1', album: 'TALB', albumartist: 'TPE2',
12+
composer: 'TCOM', genre: 'TCON', year: 'TDRC', track: 'TRCK',
13+
disc: 'TPOS', bpm: 'TBPM', key: 'TKEY', copyright: 'TCOP',
14+
isrc: 'TSRC', publisher: 'TPUB', software: 'TENC',
15+
comment: 'COMM', lyrics: 'USLT'
16+
}
17+
18+
// ── Binary helpers ──────────────────────────────────────────────────────
19+
20+
function synchsafe(b, o) { return (b[o] << 21) | (b[o + 1] << 14) | (b[o + 2] << 7) | b[o + 3] }
21+
function wSynchsafe(b, o, v) { b[o] = (v >>> 21) & 0x7f; b[o + 1] = (v >>> 14) & 0x7f; b[o + 2] = (v >>> 7) & 0x7f; b[o + 3] = v & 0x7f }
22+
23+
// ── ID3v2 builder ───────────────────────────────────────────────────────
24+
25+
function buildId3Frame(id, body) {
26+
let out = new Uint8Array(10 + body.length)
27+
out.set(TE.encode(id), 0)
28+
wSynchsafe(out, 4, body.length)
29+
out.set(body, 10)
30+
return out
31+
}
32+
33+
function buildId3v2(meta) {
34+
let frames = []
35+
for (let k in ID3_MAP_REV) {
36+
let v = meta[k]
37+
if (v == null || v === '') continue
38+
let id = ID3_MAP_REV[k]
39+
let body
40+
if (id === 'COMM' || id === 'USLT') {
41+
let txt = TE.encode(String(v))
42+
body = new Uint8Array(1 + 3 + 1 + txt.length + 1)
43+
body[0] = 3
44+
body.set(TE.encode('eng'), 1)
45+
body[4] = 0
46+
body.set(txt, 5)
47+
body[body.length - 1] = 0
48+
} else {
49+
let enc = TE.encode(String(v))
50+
body = new Uint8Array(1 + enc.length)
51+
body[0] = 3
52+
body.set(enc, 1)
53+
}
54+
frames.push(buildId3Frame(id, body))
55+
}
56+
if (meta.pictures) {
57+
for (let p of meta.pictures) {
58+
let mime = TE.encode((p.mime || 'image/jpeg') + '\0')
59+
let desc = TE.encode((p.description || '') + '\0')
60+
let body = new Uint8Array(1 + mime.length + 1 + desc.length + p.data.length)
61+
body[0] = 3
62+
let pos = 1
63+
body.set(mime, pos); pos += mime.length
64+
body[pos++] = p.type ?? 3
65+
body.set(desc, pos); pos += desc.length
66+
body.set(p.data, pos)
67+
frames.push(buildId3Frame('APIC', body))
68+
}
69+
}
70+
71+
if (!frames.length) return null
72+
let totalFrameSize = frames.reduce((n, f) => n + f.length, 0)
73+
let out = new Uint8Array(10 + totalFrameSize)
74+
out[0] = 0x49; out[1] = 0x44; out[2] = 0x33
75+
out[3] = 4; out[4] = 0; out[5] = 0
76+
wSynchsafe(out, 6, totalFrameSize)
77+
let pos = 10
78+
for (let f of frames) { out.set(f, pos); pos += f.length }
79+
return out
80+
}
81+
82+
function stripMp3Tags(bytes) {
83+
let start = 0, end = bytes.length
84+
if (bytes.length >= 10 && bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) {
85+
start = 10 + synchsafe(bytes, 6)
86+
}
87+
if (bytes.length >= 128 && bytes[end - 128] === 0x54 && bytes[end - 127] === 0x41 && bytes[end - 126] === 0x47) {
88+
end -= 128
89+
}
90+
return bytes.subarray(start, end)
91+
}
92+
93+
/** Splice ID3v2 tag into MP3 bytes. Returns new Uint8Array. */
94+
export function writeMeta(bytes, { meta = {} } = {}) {
95+
let audio = stripMp3Tags(bytes)
96+
let tag = buildId3v2(meta)
97+
if (!tag) return audio
98+
let out = new Uint8Array(tag.length + audio.length)
99+
out.set(tag, 0)
100+
out.set(audio, tag.length)
101+
return out
102+
}

packages/encode-mp3/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
"types": "mp3-encode.d.ts",
88
"exports": {
99
".": "./mp3-encode.js",
10+
"./meta": "./meta.js",
1011
"./package.json": "./package.json"
1112
},
1213
"files": [
1314
"mp3-encode.js",
1415
"mp3-encode.d.ts",
16+
"meta.js",
1517
"LICENSE"
1618
],
1719
"keywords": [

0 commit comments

Comments
 (0)