taglib-wasm 2.2.3 → 2.3.0

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.
Files changed (60) hide show
  1. package/README.md +192 -13
  2. package/dist/index.browser.d.ts +24 -4
  3. package/dist/index.browser.d.ts.map +1 -1
  4. package/dist/index.browser.js +1123 -208
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/simple.browser.js +527 -202
  9. package/dist/src/errors/base.d.ts +17 -2
  10. package/dist/src/errors/base.d.ts.map +1 -1
  11. package/dist/src/errors/base.js +18 -0
  12. package/dist/src/runtime/wasi-adapter/adapter.d.ts +13 -1
  13. package/dist/src/runtime/wasi-adapter/adapter.d.ts.map +1 -1
  14. package/dist/src/runtime/wasi-adapter/adapter.js +17 -0
  15. package/dist/src/runtime/wasi-adapter/audio-properties.d.ts +4 -1
  16. package/dist/src/runtime/wasi-adapter/audio-properties.d.ts.map +1 -1
  17. package/dist/src/runtime/wasi-adapter/audio-properties.js +23 -6
  18. package/dist/src/runtime/wasi-adapter/path-open.d.ts +68 -0
  19. package/dist/src/runtime/wasi-adapter/path-open.d.ts.map +1 -0
  20. package/dist/src/runtime/wasi-adapter/path-open.js +76 -0
  21. package/dist/src/runtime/wasi-adapter/wasm-io.d.ts.map +1 -1
  22. package/dist/src/runtime/wasi-adapter/wasm-io.js +36 -28
  23. package/dist/src/runtime/wasi-host-loader.js +8 -3
  24. package/dist/src/runtime/wasmer-sdk-loader/types.d.ts +13 -0
  25. package/dist/src/runtime/wasmer-sdk-loader/types.d.ts.map +1 -1
  26. package/dist/src/simple/checksum-operations.d.ts +25 -0
  27. package/dist/src/simple/checksum-operations.d.ts.map +1 -0
  28. package/dist/src/simple/checksum-operations.js +7 -0
  29. package/dist/src/simple/index.d.ts +2 -0
  30. package/dist/src/simple/index.d.ts.map +1 -1
  31. package/dist/src/simple/index.js +2 -0
  32. package/dist/src/taglib/audio-file-checksum.d.ts +47 -0
  33. package/dist/src/taglib/audio-file-checksum.d.ts.map +1 -0
  34. package/dist/src/taglib/audio-file-checksum.js +77 -0
  35. package/dist/src/taglib/audio-file-impl.d.ts +4 -0
  36. package/dist/src/taglib/audio-file-impl.d.ts.map +1 -1
  37. package/dist/src/taglib/audio-file-impl.js +32 -0
  38. package/dist/src/taglib/audio-file-interface.d.ts +17 -0
  39. package/dist/src/taglib/audio-file-interface.d.ts.map +1 -1
  40. package/dist/src/taglib/media-ranges.d.ts +85 -0
  41. package/dist/src/taglib/media-ranges.d.ts.map +1 -0
  42. package/dist/src/taglib/media-ranges.js +192 -0
  43. package/dist/src/taglib/metadata-extent.d.ts +55 -0
  44. package/dist/src/taglib/metadata-extent.d.ts.map +1 -1
  45. package/dist/src/taglib/metadata-extent.js +24 -6
  46. package/dist/src/taglib/taglib-class.d.ts.map +1 -1
  47. package/dist/src/taglib/taglib-class.js +13 -24
  48. package/dist/src/utils/path.d.ts +12 -0
  49. package/dist/src/utils/path.d.ts.map +1 -1
  50. package/dist/src/utils/path.js +19 -0
  51. package/dist/src/version.d.ts +1 -1
  52. package/dist/src/version.js +1 -1
  53. package/dist/src/wasm.d.ts +9 -0
  54. package/dist/src/wasm.d.ts.map +1 -1
  55. package/dist/taglib-wasi.wasm +0 -0
  56. package/dist/taglib-web.wasm +0 -0
  57. package/dist/web.browser.d.ts +13 -0
  58. package/dist/web.browser.d.ts.map +1 -0
  59. package/dist/web.browser.js +3630 -0
  60. package/package.json +12 -4
@@ -281,11 +281,29 @@ var init_base = __esm({
281
281
  "use strict";
282
282
  SUPPORTED_FORMATS = [
283
283
  "MP3",
284
+ "AAC",
284
285
  "MP4",
285
286
  "M4A",
286
287
  "FLAC",
287
288
  "OGG",
289
+ "OPUS",
290
+ "OggFLAC",
291
+ "SPEEX",
288
292
  "WAV",
293
+ "AIFF",
294
+ "ASF",
295
+ "APE",
296
+ "DSF",
297
+ "DSDIFF",
298
+ "WV",
299
+ "MPC",
300
+ "TTA",
301
+ "SHN",
302
+ "MOD",
303
+ "S3M",
304
+ "IT",
305
+ "XM",
306
+ "MATROSKA",
289
307
  "MKA"
290
308
  ];
291
309
  TagLibError = class _TagLibError extends Error {
@@ -1875,6 +1893,463 @@ var init_audio_file_base = __esm({
1875
1893
  }
1876
1894
  });
1877
1895
 
1896
+ // src/taglib/metadata-extent.ts
1897
+ function be32(b, offset) {
1898
+ return (b[offset] << 24 | b[offset + 1] << 16 | b[offset + 2] << 8 | b[offset + 3]) >>> 0;
1899
+ }
1900
+ function startsWith(bytes, magic, at = 0) {
1901
+ if (bytes.length < at + magic.length) return false;
1902
+ for (let i = 0; i < magic.length; i++) {
1903
+ if (bytes[at + i] !== magic.charCodeAt(i)) return false;
1904
+ }
1905
+ return true;
1906
+ }
1907
+ function id3v2End(bytes) {
1908
+ if (!startsWith(bytes, "ID3") || bytes.length < 10) return void 0;
1909
+ const size = bytes[6] << 21 | bytes[7] << 14 | bytes[8] << 7 | bytes[9];
1910
+ const hasFooter = (bytes[5] & 16) !== 0;
1911
+ return 10 + size + (hasFooter ? 10 : 0);
1912
+ }
1913
+ function flacEnd(bytes, limit, start) {
1914
+ let offset = start + 4;
1915
+ for (; ; ) {
1916
+ if (offset + 4 > bytes.length) return void 0;
1917
+ const isLast = (bytes[offset] & 128) !== 0;
1918
+ const length = bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3];
1919
+ offset += 4 + length;
1920
+ if (offset > limit) return offset;
1921
+ if (isLast) return offset;
1922
+ }
1923
+ }
1924
+ function flacMarkerOffset(bytes) {
1925
+ const at = startsWith(bytes, "ID3") ? id3v2End(bytes) : 0;
1926
+ if (at === void 0) return void 0;
1927
+ return startsWith(bytes, "fLaC", at) ? at : void 0;
1928
+ }
1929
+ function flacAudioStart(bytes) {
1930
+ const at = flacMarkerOffset(bytes);
1931
+ if (at === void 0) return void 0;
1932
+ return flacEnd(bytes, bytes.length, at);
1933
+ }
1934
+ function mp4MoovEnd(bytes, limit, start) {
1935
+ let offset = start;
1936
+ const readable = Math.min(bytes.length, limit);
1937
+ while (offset + 8 <= readable) {
1938
+ const size = be32(bytes, offset);
1939
+ const type = String.fromCharCode(
1940
+ bytes[offset + 4],
1941
+ bytes[offset + 5],
1942
+ bytes[offset + 6],
1943
+ bytes[offset + 7]
1944
+ );
1945
+ if (size < 8) return void 0;
1946
+ if (type === "moov") return offset + size;
1947
+ offset += size;
1948
+ }
1949
+ return void 0;
1950
+ }
1951
+ function oggMetadataEnd(bytes, limit, start) {
1952
+ let offset = start;
1953
+ let packetsCompleted = 0;
1954
+ let first = true;
1955
+ for (; ; ) {
1956
+ if (offset + 27 > bytes.length || !startsWith(bytes, "OggS", offset)) {
1957
+ return void 0;
1958
+ }
1959
+ const segmentCount = bytes[offset + 26];
1960
+ const tableStart = offset + 27;
1961
+ if (tableStart + segmentCount > bytes.length) return void 0;
1962
+ let payload = 0;
1963
+ for (let i = 0; i < segmentCount; i++) {
1964
+ const segment = bytes[tableStart + i];
1965
+ payload += segment;
1966
+ if (segment < 255) packetsCompleted++;
1967
+ }
1968
+ if (first) {
1969
+ const payloadStart = tableStart + segmentCount;
1970
+ if (payloadStart + 5 > bytes.length) return void 0;
1971
+ if (bytes[payloadStart] === 127 && startsWith(bytes, "FLAC", payloadStart + 1)) {
1972
+ return void 0;
1973
+ }
1974
+ first = false;
1975
+ }
1976
+ offset = tableStart + segmentCount + payload;
1977
+ if (packetsCompleted >= 2 || offset > limit) return offset;
1978
+ }
1979
+ }
1980
+ function mpegFrameLength(bytes, at) {
1981
+ if (bytes.length < at + 4) return 0;
1982
+ if (bytes[at] !== 255 || (bytes[at + 1] & 224) !== 224) return 0;
1983
+ const version = bytes[at + 1] >> 3 & 3;
1984
+ const layer = bytes[at + 1] >> 1 & 3;
1985
+ if (version === 1 || layer === 0) return 0;
1986
+ const bitrateIndex = bytes[at + 2] >> 4 & 15;
1987
+ const sampleRateIndex = bytes[at + 2] >> 2 & 3;
1988
+ if (bitrateIndex === 0 || bitrateIndex === 15 || sampleRateIndex === 3) {
1989
+ return 0;
1990
+ }
1991
+ const mpeg1 = version === 3;
1992
+ const layerIndex = layer ^ 3;
1993
+ const bitrate = MPEG_BITRATES[mpeg1 ? 0 : 1][layerIndex][bitrateIndex];
1994
+ const sampleRateTable = mpeg1 ? 0 : version === 2 ? 1 : 2;
1995
+ const sampleRate = MPEG_SAMPLE_RATES[sampleRateTable][sampleRateIndex];
1996
+ const samplesPerFrame = layerIndex === 2 ? mpeg1 ? 1152 : 576 : layerIndex === 1 ? 1152 : 384;
1997
+ let frameLength = Math.floor(samplesPerFrame * bitrate * 125 / sampleRate);
1998
+ if ((bytes[at + 2] & 2) !== 0) frameLength += layerIndex === 0 ? 4 : 1;
1999
+ return frameLength === 0 ? 0 : frameLength;
2000
+ }
2001
+ function isMpegFrameSync(bytes, at = 0) {
2002
+ const frameLength = mpegFrameLength(bytes, at);
2003
+ if (frameLength === 0) return false;
2004
+ const next = at + frameLength;
2005
+ if (bytes.length < next + 4) return false;
2006
+ const mask = 4294839296;
2007
+ const header = ((bytes[at] << 24 | bytes[at + 1] << 16 | bytes[at + 2] << 8 | bytes[at + 3]) >>> 0 & mask) >>> 0;
2008
+ const nextHeader = ((bytes[next] << 24 | bytes[next + 1] << 16 | bytes[next + 2] << 8 | bytes[next + 3]) >>> 0 & mask) >>> 0;
2009
+ return header === nextHeader;
2010
+ }
2011
+ function containerEnd(bytes, at, limit) {
2012
+ if (at >= bytes.length) return bytes.length < limit ? null : void 0;
2013
+ if (at + 8 > bytes.length) return void 0;
2014
+ if (startsWith(bytes, "fLaC", at)) return flacEnd(bytes, limit, at);
2015
+ if (startsWith(bytes, "ftyp", at + 4)) return mp4MoovEnd(bytes, limit, at);
2016
+ if (startsWith(bytes, "OggS", at)) return oggMetadataEnd(bytes, limit, at);
2017
+ return null;
2018
+ }
2019
+ function metadataFitsInHeader(header, limit) {
2020
+ let end;
2021
+ if (startsWith(header, "ID3")) {
2022
+ const tagEnd = id3v2End(header);
2023
+ if (tagEnd === void 0) return false;
2024
+ end = tagEnd;
2025
+ if (tagEnd <= limit) {
2026
+ const behind = containerEnd(header, tagEnd, limit);
2027
+ if (behind === void 0) return false;
2028
+ if (behind !== null) end = behind;
2029
+ }
2030
+ } else {
2031
+ const container = containerEnd(header, 0, limit);
2032
+ if (container === null) {
2033
+ end = isMpegFrameSync(header) ? 0 : void 0;
2034
+ } else {
2035
+ end = container;
2036
+ }
2037
+ }
2038
+ if (end === void 0 || end > header.length) return false;
2039
+ return end <= limit;
2040
+ }
2041
+ function trailerFitsInFooter(tail, footerSize) {
2042
+ if (tail.length < Math.min(footerSize, 32)) return false;
2043
+ for (const offsetFromEnd of [32, 32 + 128]) {
2044
+ const at = tail.length - offsetFromEnd;
2045
+ if (at < 0 || !startsWith(tail, "APETAGEX", at)) continue;
2046
+ const size = (tail[at + 12] | tail[at + 13] << 8 | tail[at + 14] << 16 | tail[at + 15] << 24) >>> 0;
2047
+ const trailing = offsetFromEnd === 32 ? 0 : 128;
2048
+ if (size + 32 + trailing > footerSize) return false;
2049
+ }
2050
+ return true;
2051
+ }
2052
+ var MPEG_BITRATES, MPEG_SAMPLE_RATES;
2053
+ var init_metadata_extent = __esm({
2054
+ "src/taglib/metadata-extent.ts"() {
2055
+ "use strict";
2056
+ MPEG_BITRATES = [
2057
+ [
2058
+ [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],
2059
+ // Layer I
2060
+ [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],
2061
+ // Layer II
2062
+ [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0]
2063
+ // Layer III
2064
+ ],
2065
+ [
2066
+ [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
2067
+ // Layer I
2068
+ [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
2069
+ // Layer II
2070
+ [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
2071
+ // Layer III
2072
+ ]
2073
+ ];
2074
+ MPEG_SAMPLE_RATES = [
2075
+ [44100, 48e3, 32e3],
2076
+ // MPEG-1
2077
+ [22050, 24e3, 16e3],
2078
+ // MPEG-2
2079
+ [11025, 12e3, 8e3]
2080
+ // MPEG-2.5
2081
+ ];
2082
+ }
2083
+ });
2084
+
2085
+ // src/taglib/media-ranges.ts
2086
+ function adtsFrameLength(bytes, at) {
2087
+ if (bytes.length < at + 7) return 0;
2088
+ if (bytes[at] !== 255 || (bytes[at + 1] & 246) !== 240) return 0;
2089
+ const length = (bytes[at + 3] & 3) << 11 | bytes[at + 4] << 3 | bytes[at + 5] >> 5;
2090
+ return length >= 7 ? length : 0;
2091
+ }
2092
+ function trailingTagStart(bytes, end = bytes.length) {
2093
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2094
+ let at = Math.max(0, Math.min(end, bytes.length));
2095
+ for (; ; ) {
2096
+ if (at >= 128 && text(bytes, at - 128, 3) === "TAG") {
2097
+ at -= 128;
2098
+ continue;
2099
+ }
2100
+ if (at >= 32 && text(bytes, at - 32, 8) === "APETAGEX") {
2101
+ const size = view.getUint32(at - 32 + 12, true);
2102
+ const flags = view.getUint32(at - 32 + 20, true);
2103
+ const start = at - size - ((flags & 2147483648) !== 0 ? 32 : 0);
2104
+ if (start < 0 || start >= at) return at;
2105
+ at = start;
2106
+ continue;
2107
+ }
2108
+ return at;
2109
+ }
2110
+ }
2111
+ function firstFrame(bytes) {
2112
+ const from = id3v2End(bytes) ?? 0;
2113
+ const limit = Math.min(bytes.length - 6, from + MPEG_SCAN_LIMIT);
2114
+ for (let o = from; o < limit; o++) {
2115
+ const len = frameLengthAt(bytes, o);
2116
+ if (len > 0 && frameLengthAt(bytes, o + len) > 0) return o;
2117
+ }
2118
+ return -1;
2119
+ }
2120
+ function forwardLastFrame(bytes, from, end) {
2121
+ let o = from;
2122
+ let start = -1;
2123
+ let length = 0;
2124
+ let frames = 0;
2125
+ while (o + 6 <= end) {
2126
+ const len = frameLengthAt(bytes, o);
2127
+ if (len === 0 || o + len > end) break;
2128
+ start = o;
2129
+ length = len;
2130
+ frames++;
2131
+ o += len;
2132
+ }
2133
+ return { start, length, frames };
2134
+ }
2135
+ function walkMpeg(bytes) {
2136
+ const from = firstFrame(bytes);
2137
+ if (from < 0) return fallback("no valid first frame");
2138
+ const end = trailingTagStart(bytes);
2139
+ const last = forwardLastFrame(bytes, from, end);
2140
+ if (last.start < 0) return fallback("no valid frames");
2141
+ return {
2142
+ kind: "ranges",
2143
+ ranges: [{ offset: from, length: last.start + last.length - from }],
2144
+ detail: `first=${from} last=${last.start}+${last.length} frames=${last.frames}`
2145
+ };
2146
+ }
2147
+ function flacStreamInfoMd5(bytes) {
2148
+ const marker = flacMarkerOffset(bytes);
2149
+ if (marker === void 0) return void 0;
2150
+ const header = marker + 4;
2151
+ if (header + 4 > bytes.length) return void 0;
2152
+ if ((bytes[header] & 127) !== 0) return void 0;
2153
+ const blockLength = bytes[header + 1] << 16 | bytes[header + 2] << 8 | bytes[header + 3];
2154
+ if (blockLength < 34) return void 0;
2155
+ const payload = header + 4;
2156
+ if (payload + 34 > bytes.length) return void 0;
2157
+ let hex = "";
2158
+ for (const byte of bytes.subarray(payload + 18, payload + 34)) {
2159
+ hex += byte.toString(16).padStart(2, "0");
2160
+ }
2161
+ return hex;
2162
+ }
2163
+ function walkFlac(bytes) {
2164
+ const start = flacAudioStart(bytes);
2165
+ if (start === void 0) return fallback("no fLaC marker");
2166
+ const end = trailingTagStart(bytes);
2167
+ if (end <= start) return fallback("no audio bytes");
2168
+ return {
2169
+ kind: "ranges",
2170
+ ranges: [{ offset: start, length: end - start }],
2171
+ detail: `audioStart=${start} end=${end}`
2172
+ };
2173
+ }
2174
+ function atomLength(bytes, view, o) {
2175
+ const size = view.getUint32(o, false);
2176
+ if (size === 0) return { length: bytes.length - o, headerSize: 8 };
2177
+ if (size === 1) {
2178
+ if (o + 16 > bytes.length) return void 0;
2179
+ const big = view.getBigUint64(o + 8, false);
2180
+ if (big > BigInt(Number.MAX_SAFE_INTEGER)) return void 0;
2181
+ return { length: Number(big), headerSize: 16 };
2182
+ }
2183
+ return { length: size, headerSize: 8 };
2184
+ }
2185
+ function walkMp4(bytes) {
2186
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2187
+ const ranges = [];
2188
+ let o = 0;
2189
+ while (o + 8 <= bytes.length) {
2190
+ const type = text(bytes, o + 4, 4);
2191
+ const atom = atomLength(bytes, view, o);
2192
+ if (atom === void 0) return fallback(`malformed ${type} size`);
2193
+ if (atom.length < atom.headerSize) {
2194
+ return fallback(`malformed ${type} size`);
2195
+ }
2196
+ if (o + atom.length > bytes.length) return fallback(`oversized ${type}`);
2197
+ if (type === "mdat" && atom.length > atom.headerSize) {
2198
+ ranges.push({
2199
+ offset: o + atom.headerSize,
2200
+ length: atom.length - atom.headerSize
2201
+ });
2202
+ }
2203
+ o += atom.length;
2204
+ }
2205
+ if (o !== bytes.length) return fallback("trailing bytes after the last atom");
2206
+ if (ranges.length === 0) return fallback("no non-empty mdat");
2207
+ return { kind: "ranges", ranges, detail: `${ranges.length} mdat(s)` };
2208
+ }
2209
+ function walkWav(bytes) {
2210
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2211
+ if (text(bytes, 0, 4) !== "RIFF") return fallback("not RIFF");
2212
+ if (text(bytes, 8, 4) !== "WAVE") return fallback("not WAVE");
2213
+ const ranges = [];
2214
+ let o = 12;
2215
+ while (o + 8 <= bytes.length) {
2216
+ const id = text(bytes, o, 4);
2217
+ const size = view.getUint32(o + 4, true);
2218
+ const dataStart = o + 8;
2219
+ if (dataStart + size > bytes.length) return fallback(`truncated ${id}`);
2220
+ if (id === "data" && size > 0) {
2221
+ ranges.push({ offset: dataStart, length: size });
2222
+ }
2223
+ o = dataStart + size + size % 2;
2224
+ }
2225
+ if (ranges.length === 0) return fallback("no data chunk");
2226
+ return { kind: "ranges", ranges, detail: `${ranges.length} data chunk(s)` };
2227
+ }
2228
+ function mediaRanges(format, bytes) {
2229
+ const walked = (() => {
2230
+ switch (format.toUpperCase()) {
2231
+ case "MP3":
2232
+ case "AAC":
2233
+ return walkMpeg(bytes);
2234
+ case "FLAC":
2235
+ return walkFlac(bytes);
2236
+ case "MP4":
2237
+ case "M4A":
2238
+ return walkMp4(bytes);
2239
+ case "WAV":
2240
+ return walkWav(bytes);
2241
+ default:
2242
+ return { kind: "fallback", ranges: [], detail: "format not covered" };
2243
+ }
2244
+ })();
2245
+ if (walked.kind === "fallback") {
2246
+ return {
2247
+ kind: "fallback",
2248
+ ranges: [{ offset: 0, length: bytes.length }],
2249
+ detail: `${walked.detail} \u2192 whole file`
2250
+ };
2251
+ }
2252
+ return walked;
2253
+ }
2254
+ var decoder, text, fallback, frameLengthAt, MPEG_SCAN_LIMIT;
2255
+ var init_media_ranges = __esm({
2256
+ "src/taglib/media-ranges.ts"() {
2257
+ "use strict";
2258
+ init_metadata_extent();
2259
+ decoder = new TextDecoder();
2260
+ text = (b, o, n) => decoder.decode(b.subarray(o, o + n));
2261
+ fallback = (detail) => ({
2262
+ kind: "fallback",
2263
+ ranges: [],
2264
+ detail
2265
+ });
2266
+ frameLengthAt = (bytes, at) => mpegFrameLength(bytes, at) || adtsFrameLength(bytes, at);
2267
+ MPEG_SCAN_LIMIT = 65536;
2268
+ }
2269
+ });
2270
+
2271
+ // src/taglib/audio-file-checksum.ts
2272
+ async function fileBytes(source, io) {
2273
+ if (source.path) return await io.readFile(source.path);
2274
+ if (source.blob) return new Uint8Array(await source.blob.arrayBuffer());
2275
+ if (!source.partiallyLoaded && source.bytes.length > 0) return source.bytes;
2276
+ throw new MetadataError(
2277
+ "read",
2278
+ "the handle holds no file bytes and has no readable source; a partial handle that has been saved drops its source \u2014 checksum the saved path instead"
2279
+ );
2280
+ }
2281
+ function joinRanges(bytes, ranges) {
2282
+ const total = ranges.reduce((n, r) => n + r.length, 0);
2283
+ const overrun = ranges.find((r) => r.offset + r.length > bytes.length);
2284
+ if (overrun || total > bytes.length) {
2285
+ throw new MetadataError(
2286
+ "read",
2287
+ `range walk overran the buffer (${overrun ? `range ${overrun.offset}+${overrun.length}` : `${total} bytes`} past the ${bytes.length}-byte buffer)`
2288
+ );
2289
+ }
2290
+ if (ranges.length === 1) {
2291
+ const only = ranges[0];
2292
+ const view = bytes.subarray(only.offset, only.offset + only.length);
2293
+ return view.buffer instanceof ArrayBuffer ? view : new Uint8Array(view);
2294
+ }
2295
+ const out = new Uint8Array(total);
2296
+ let at = 0;
2297
+ for (const r of ranges) {
2298
+ out.set(bytes.subarray(r.offset, r.offset + r.length), at);
2299
+ at += r.length;
2300
+ }
2301
+ return out;
2302
+ }
2303
+ async function mediaChecksum(source, format, io, options = {}) {
2304
+ if (options.basis === "pcm") {
2305
+ if (format.toUpperCase() !== "FLAC") {
2306
+ throw new UnsupportedFormatError(format, ["FLAC"], {
2307
+ operation: "media checksum (basis: pcm)"
2308
+ });
2309
+ }
2310
+ const windowed = source.bytes.length > 0 ? source.bytes : source.path && io.readPartial ? await io.readPartial(source.path, STREAMINFO_WINDOW, 0) : void 0;
2311
+ let md5 = windowed === void 0 ? void 0 : flacStreamInfoMd5(windowed);
2312
+ if (md5 === void 0 && windowed !== source.bytes) {
2313
+ md5 = flacStreamInfoMd5(await fileBytes(source, io));
2314
+ }
2315
+ if (md5 === void 0) {
2316
+ throw new MetadataError(
2317
+ "read",
2318
+ "STREAMINFO digest unavailable"
2319
+ );
2320
+ }
2321
+ return {
2322
+ source: "flac-streaminfo-md5",
2323
+ algorithm: "md5",
2324
+ hex: md5,
2325
+ bytesHashed: 16
2326
+ };
2327
+ }
2328
+ const bytes = await fileBytes(source, io);
2329
+ const walk = mediaRanges(format, bytes);
2330
+ const payload = joinRanges(bytes, walk.ranges);
2331
+ const digest = new Uint8Array(
2332
+ await crypto.subtle.digest("SHA-256", payload)
2333
+ );
2334
+ let hex = "";
2335
+ for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
2336
+ return {
2337
+ source: walk.kind === "ranges" ? "audio-payload" : "file",
2338
+ algorithm: "sha256",
2339
+ hex,
2340
+ bytesHashed: payload.length
2341
+ };
2342
+ }
2343
+ var STREAMINFO_WINDOW;
2344
+ var init_audio_file_checksum = __esm({
2345
+ "src/taglib/audio-file-checksum.ts"() {
2346
+ "use strict";
2347
+ init_media_ranges();
2348
+ init_errors2();
2349
+ STREAMINFO_WINDOW = 65536;
2350
+ }
2351
+ });
2352
+
1878
2353
  // src/utils/file.ts
1879
2354
  async function readFileData(file) {
1880
2355
  if (file instanceof Uint8Array) return file;
@@ -1982,8 +2457,8 @@ function wrapEmbindHandle(raw) {
1982
2457
  // Text only — language/description are not surfaced by the PropertyMap.
1983
2458
  getLyrics() {
1984
2459
  const props = raw.getProperties();
1985
- return (props["LYRICS"] ?? []).map((text) => ({
1986
- text,
2460
+ return (props["LYRICS"] ?? []).map((text2) => ({
2461
+ text: text2,
1987
2462
  description: "",
1988
2463
  language: ""
1989
2464
  }));
@@ -2236,6 +2711,8 @@ var init_audio_file_impl = __esm({
2236
2711
  init_rating();
2237
2712
  init_detector();
2238
2713
  init_audio_file_base();
2714
+ init_audio_file_checksum();
2715
+ init_platform_io_browser_stub();
2239
2716
  init_save_reconstruct();
2240
2717
  AudioFileImpl = class extends BaseAudioFileImpl {
2241
2718
  constructor(module, fileHandle, sourcePath, originalSource, isPartiallyLoaded = false, partialLoadOptions) {
@@ -2248,6 +2725,9 @@ var init_audio_file_impl = __esm({
2248
2725
  partialLoadOptions
2249
2726
  );
2250
2727
  __publicField(this, "pathModeBuffer", null);
2728
+ /** How the handle was loaded — `isPartiallyLoaded` is mutated by saveToFile(). */
2729
+ __publicField(this, "partiallyLoadedAtConstruction");
2730
+ this.partiallyLoadedAtConstruction = isPartiallyLoaded;
2251
2731
  }
2252
2732
  save() {
2253
2733
  if (this.isPartiallyLoaded && this.originalSource) {
@@ -2283,6 +2763,31 @@ var init_audio_file_impl = __esm({
2283
2763
  "No file data available: in-memory buffer is empty and no source path is set"
2284
2764
  );
2285
2765
  }
2766
+ mediaChecksum(options) {
2767
+ return mediaChecksum(
2768
+ {
2769
+ // The cheap, no-IO accessor — `getFileBuffer()` would read the whole
2770
+ // file from disk in path mode (and cache it), which the source rule is
2771
+ // about to read for itself.
2772
+ bytes: this.handle.getBuffer(),
2773
+ // empty in path mode: the data is on disk
2774
+ // Both optional fields are conditional: they are `undefined` far more
2775
+ // often than not, and `exactOptionalPropertyTypes` refuses an explicit
2776
+ // `undefined` on an optional property.
2777
+ ...this.sourcePath !== void 0 ? { path: this.sourcePath } : {},
2778
+ // originalSource is the File when the caller passed one. A partial File
2779
+ // has no path, so this is the only way back to the real bytes.
2780
+ ...this.originalSource instanceof Blob ? { blob: this.originalSource } : {},
2781
+ // Captured at construction, NOT this.isPartiallyLoaded: saveToFile() sets
2782
+ // that flag false (and clears originalSource) while the handle still holds
2783
+ // the spliced image, which would make the source rule hash the image.
2784
+ partiallyLoaded: this.partiallyLoadedAtConstruction
2785
+ },
2786
+ this.getFormat(),
2787
+ getPlatformIO(),
2788
+ options
2789
+ );
2790
+ }
2286
2791
  async saveToFile(path) {
2287
2792
  const targetPath = path ?? this.sourcePath;
2288
2793
  if (!targetPath) {
@@ -2480,181 +2985,6 @@ var init_audio_file_impl = __esm({
2480
2985
  }
2481
2986
  });
2482
2987
 
2483
- // src/taglib/metadata-extent.ts
2484
- function be32(b, offset) {
2485
- return (b[offset] << 24 | b[offset + 1] << 16 | b[offset + 2] << 8 | b[offset + 3]) >>> 0;
2486
- }
2487
- function startsWith(bytes, magic, at = 0) {
2488
- if (bytes.length < at + magic.length) return false;
2489
- for (let i = 0; i < magic.length; i++) {
2490
- if (bytes[at + i] !== magic.charCodeAt(i)) return false;
2491
- }
2492
- return true;
2493
- }
2494
- function id3v2End(bytes) {
2495
- if (bytes.length < 10) return void 0;
2496
- const size = bytes[6] << 21 | bytes[7] << 14 | bytes[8] << 7 | bytes[9];
2497
- const hasFooter = (bytes[5] & 16) !== 0;
2498
- return 10 + size + (hasFooter ? 10 : 0);
2499
- }
2500
- function flacEnd(bytes, limit, start) {
2501
- let offset = start + 4;
2502
- for (; ; ) {
2503
- if (offset + 4 > bytes.length) return void 0;
2504
- const isLast = (bytes[offset] & 128) !== 0;
2505
- const length = bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3];
2506
- offset += 4 + length;
2507
- if (offset > limit) return offset;
2508
- if (isLast) return offset;
2509
- }
2510
- }
2511
- function mp4MoovEnd(bytes, limit, start) {
2512
- let offset = start;
2513
- const readable = Math.min(bytes.length, limit);
2514
- while (offset + 8 <= readable) {
2515
- const size = be32(bytes, offset);
2516
- const type = String.fromCharCode(
2517
- bytes[offset + 4],
2518
- bytes[offset + 5],
2519
- bytes[offset + 6],
2520
- bytes[offset + 7]
2521
- );
2522
- if (size < 8) return void 0;
2523
- if (type === "moov") return offset + size;
2524
- offset += size;
2525
- }
2526
- return void 0;
2527
- }
2528
- function oggMetadataEnd(bytes, limit, start) {
2529
- let offset = start;
2530
- let packetsCompleted = 0;
2531
- let first = true;
2532
- for (; ; ) {
2533
- if (offset + 27 > bytes.length || !startsWith(bytes, "OggS", offset)) {
2534
- return void 0;
2535
- }
2536
- const segmentCount = bytes[offset + 26];
2537
- const tableStart = offset + 27;
2538
- if (tableStart + segmentCount > bytes.length) return void 0;
2539
- let payload = 0;
2540
- for (let i = 0; i < segmentCount; i++) {
2541
- const segment = bytes[tableStart + i];
2542
- payload += segment;
2543
- if (segment < 255) packetsCompleted++;
2544
- }
2545
- if (first) {
2546
- const payloadStart = tableStart + segmentCount;
2547
- if (payloadStart + 5 > bytes.length) return void 0;
2548
- if (bytes[payloadStart] === 127 && startsWith(bytes, "FLAC", payloadStart + 1)) {
2549
- return void 0;
2550
- }
2551
- first = false;
2552
- }
2553
- offset = tableStart + segmentCount + payload;
2554
- if (packetsCompleted >= 2 || offset > limit) return offset;
2555
- }
2556
- }
2557
- function isMpegFrameSync(bytes, at = 0) {
2558
- if (bytes.length < at + 4) return false;
2559
- if (bytes[at] !== 255 || (bytes[at + 1] & 224) !== 224) return false;
2560
- const version = bytes[at + 1] >> 3 & 3;
2561
- const layer = bytes[at + 1] >> 1 & 3;
2562
- if (version === 1 || layer === 0) return false;
2563
- const bitrateIndex = bytes[at + 2] >> 4 & 15;
2564
- const sampleRateIndex = bytes[at + 2] >> 2 & 3;
2565
- if (bitrateIndex === 0 || bitrateIndex === 15 || sampleRateIndex === 3) {
2566
- return false;
2567
- }
2568
- const mpeg1 = version === 3;
2569
- const layerIndex = layer ^ 3;
2570
- const bitrate = MPEG_BITRATES[mpeg1 ? 0 : 1][layerIndex][bitrateIndex];
2571
- const sampleRateTable = mpeg1 ? 0 : version === 2 ? 1 : 2;
2572
- const sampleRate = MPEG_SAMPLE_RATES[sampleRateTable][sampleRateIndex];
2573
- const samplesPerFrame = layerIndex === 2 ? mpeg1 ? 1152 : 576 : layerIndex === 1 ? 1152 : 384;
2574
- let frameLength = Math.floor(samplesPerFrame * bitrate * 125 / sampleRate);
2575
- if ((bytes[at + 2] & 2) !== 0) frameLength += layerIndex === 0 ? 4 : 1;
2576
- if (frameLength === 0) return false;
2577
- const next = at + frameLength;
2578
- if (bytes.length < next + 4) return false;
2579
- const mask = 4294839296;
2580
- const header = ((bytes[at] << 24 | bytes[at + 1] << 16 | bytes[at + 2] << 8 | bytes[at + 3]) >>> 0 & mask) >>> 0;
2581
- const nextHeader = ((bytes[next] << 24 | bytes[next + 1] << 16 | bytes[next + 2] << 8 | bytes[next + 3]) >>> 0 & mask) >>> 0;
2582
- return header === nextHeader;
2583
- }
2584
- function containerEnd(bytes, at, limit) {
2585
- if (at >= bytes.length) return bytes.length < limit ? null : void 0;
2586
- if (at + 8 > bytes.length) return void 0;
2587
- if (startsWith(bytes, "fLaC", at)) return flacEnd(bytes, limit, at);
2588
- if (startsWith(bytes, "ftyp", at + 4)) return mp4MoovEnd(bytes, limit, at);
2589
- if (startsWith(bytes, "OggS", at)) return oggMetadataEnd(bytes, limit, at);
2590
- return null;
2591
- }
2592
- function metadataFitsInHeader(header, limit) {
2593
- let end;
2594
- if (startsWith(header, "ID3")) {
2595
- const tagEnd = id3v2End(header);
2596
- if (tagEnd === void 0) return false;
2597
- end = tagEnd;
2598
- if (tagEnd <= limit) {
2599
- const behind = containerEnd(header, tagEnd, limit);
2600
- if (behind === void 0) return false;
2601
- if (behind !== null) end = behind;
2602
- }
2603
- } else {
2604
- const container = containerEnd(header, 0, limit);
2605
- if (container === null) {
2606
- end = isMpegFrameSync(header) ? 0 : void 0;
2607
- } else {
2608
- end = container;
2609
- }
2610
- }
2611
- if (end === void 0 || end > header.length) return false;
2612
- return end <= limit;
2613
- }
2614
- function trailerFitsInFooter(tail, footerSize) {
2615
- if (tail.length < Math.min(footerSize, 32)) return false;
2616
- for (const offsetFromEnd of [32, 32 + 128]) {
2617
- const at = tail.length - offsetFromEnd;
2618
- if (at < 0 || !startsWith(tail, "APETAGEX", at)) continue;
2619
- const size = (tail[at + 12] | tail[at + 13] << 8 | tail[at + 14] << 16 | tail[at + 15] << 24) >>> 0;
2620
- const trailing = offsetFromEnd === 32 ? 0 : 128;
2621
- if (size + 32 + trailing > footerSize) return false;
2622
- }
2623
- return true;
2624
- }
2625
- var MPEG_BITRATES, MPEG_SAMPLE_RATES;
2626
- var init_metadata_extent = __esm({
2627
- "src/taglib/metadata-extent.ts"() {
2628
- "use strict";
2629
- MPEG_BITRATES = [
2630
- [
2631
- [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],
2632
- // Layer I
2633
- [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],
2634
- // Layer II
2635
- [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0]
2636
- // Layer III
2637
- ],
2638
- [
2639
- [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
2640
- // Layer I
2641
- [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
2642
- // Layer II
2643
- [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
2644
- // Layer III
2645
- ]
2646
- ];
2647
- MPEG_SAMPLE_RATES = [
2648
- [44100, 48e3, 32e3],
2649
- // MPEG-1
2650
- [22050, 24e3, 16e3],
2651
- // MPEG-2
2652
- [11025, 12e3, 8e3]
2653
- // MPEG-2.5
2654
- ];
2655
- }
2656
- });
2657
-
2658
2988
  // src/taglib/load-audio-data.ts
2659
2989
  async function loadAudioData(input, opts) {
2660
2990
  if (opts.partial && typeof File !== "undefined" && input instanceof File) {
@@ -2941,7 +3271,7 @@ var VERSION;
2941
3271
  var init_version = __esm({
2942
3272
  "src/version.ts"() {
2943
3273
  "use strict";
2944
- VERSION = "2.2.3";
3274
+ VERSION = "2.3.0";
2945
3275
  }
2946
3276
  });
2947
3277
 
@@ -3063,30 +3393,19 @@ var init_taglib_class = __esm({
3063
3393
  const actualInput = isNamedAudioInput(input) ? input.data : input;
3064
3394
  const sourcePath = typeof actualInput === "string" ? actualInput : void 0;
3065
3395
  if (typeof actualInput === "string" && this.module.isWasi) {
3066
- const fileHandle2 = this.module.createFileHandle();
3067
- try {
3068
- const fh = fileHandle2;
3069
- if (fh.loadFromPath) {
3070
- const wasiPath = toWasiPath(actualInput);
3071
- const success = fh.loadFromPath(wasiPath);
3072
- if (!success) {
3073
- throw new InvalidFormatError(
3074
- `Failed to load audio file. Path: ${actualInput}`
3075
- );
3076
- }
3077
- return new AudioFileImpl(
3078
- this.module,
3079
- fileHandle2,
3080
- sourcePath,
3081
- actualInput,
3082
- false
3083
- );
3084
- }
3085
- } catch (error) {
3086
- if (typeof fileHandle2.destroy === "function") {
3087
- fileHandle2.destroy();
3088
- }
3089
- throw error;
3396
+ const wasiPath = toWasiPath(actualInput);
3397
+ if (this.module.loadAudioPath) {
3398
+ const fileHandle2 = await this.module.loadAudioPath(
3399
+ wasiPath,
3400
+ actualInput
3401
+ );
3402
+ return new AudioFileImpl(
3403
+ this.module,
3404
+ fileHandle2,
3405
+ sourcePath,
3406
+ actualInput,
3407
+ false
3408
+ );
3090
3409
  }
3091
3410
  }
3092
3411
  const opts = {
@@ -3600,6 +3919,11 @@ async function editTagsBatch(files, mutator, options = {}) {
3600
3919
  );
3601
3920
  }
3602
3921
 
3922
+ // src/simple/checksum-operations.ts
3923
+ async function readMediaChecksum(file, options) {
3924
+ return withAudioFile(file, (audioFile) => audioFile.mediaChecksum(options));
3925
+ }
3926
+
3603
3927
  // src/simple/index.ts
3604
3928
  init_types2();
3605
3929
  export {
@@ -3618,6 +3942,7 @@ export {
3618
3942
  isValidAudioFile,
3619
3943
  readCoverArt,
3620
3944
  readFormat,
3945
+ readMediaChecksum,
3621
3946
  readMetadata,
3622
3947
  readMetadataBatch,
3623
3948
  readPictureMetadata,