|
| 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 | +} |
0 commit comments