taglib-wasm 0.1.0 → 0.2.4

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.
package/src/wasm.ts CHANGED
@@ -30,7 +30,19 @@ export interface TagLibModule {
30
30
  setValue: (ptr: number, value: number, type: string) => void;
31
31
  addFunction: (func: Function, signature: string) => number;
32
32
  removeFunction: (funcPtr: number) => void;
33
-
33
+
34
+ // Memory allocation functions
35
+ _malloc: (size: number) => number;
36
+ _free: (ptr: number) => void;
37
+ allocate: (array: Uint8Array, type: number) => number;
38
+
39
+ // Allocation types
40
+ ALLOC_NORMAL: number;
41
+ ALLOC_STACK: number;
42
+ ALLOC_STATIC: number;
43
+ ALLOC_DYNAMIC: number;
44
+ ALLOC_NONE: number;
45
+
34
46
  // File operations
35
47
  _taglib_file_new_from_buffer: (data: number, size: number) => number;
36
48
  _taglib_file_delete: (fileId: number) => void;
@@ -68,9 +80,7 @@ export interface TagLibModule {
68
80
  _taglib_string_delete: (str: number) => void;
69
81
  _taglib_string_to_cstring: (str: number) => number;
70
82
 
71
- // Memory management
72
- _malloc: (size: number) => number;
73
- _free: (ptr: number) => void;
83
+ // Memory management functions already defined above
74
84
  }
75
85
 
76
86
  /**
@@ -93,8 +103,8 @@ export async function loadTagLibModule(
93
103
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
94
104
 
95
105
  // Detect runtime environment
96
- const isNode = typeof process !== "undefined" && process.versions?.node;
97
- const isDeno = typeof Deno !== "undefined";
106
+ const isNode = typeof (globalThis as any).process !== "undefined" && (globalThis as any).process.versions?.node;
107
+ const isDeno = typeof (globalThis as any).Deno !== "undefined";
98
108
 
99
109
  let wasmPath: string;
100
110
 
@@ -113,7 +123,7 @@ export async function loadTagLibModule(
113
123
  let wasmBinary: Uint8Array;
114
124
 
115
125
  if (isDeno) {
116
- wasmBinary = await Deno.readFile(wasmPath);
126
+ wasmBinary = await (globalThis as any).Deno.readFile(wasmPath);
117
127
  } else if (isNode) {
118
128
  const fs = await import("node:fs");
119
129
  wasmBinary = await fs.promises.readFile(wasmPath);
@@ -143,11 +153,13 @@ export async function loadTagLibModule(
143
153
  try {
144
154
  // For Deno, we need to handle the CommonJS-style export
145
155
  let TagLibWASM: any;
146
-
156
+
147
157
  if (isDeno) {
148
158
  // In Deno, read and evaluate the JS file
149
- const jsContent = await Deno.readTextFile(new URL("../build/taglib.js", import.meta.url).pathname);
150
-
159
+ const jsContent = await (globalThis as any).Deno.readTextFile(
160
+ new URL("../build/taglib.js", import.meta.url).pathname,
161
+ );
162
+
151
163
  // Create a minimal CommonJS environment
152
164
  const exports = {} as any;
153
165
  const module = { exports } as any;
@@ -155,38 +167,45 @@ export async function loadTagLibModule(
155
167
  const require = (name: string) => {
156
168
  if (name === "fs") {
157
169
  return {
158
- readFileSync: (path: string) => Deno.readFileSync(path),
170
+ readFileSync: (path: string) => (globalThis as any).Deno.readFileSync(path),
159
171
  };
160
172
  }
161
173
  throw new Error(`Module ${name} not found`);
162
174
  };
163
-
175
+
164
176
  // Add a minimal process object for Node.js compatibility
165
177
  const process = {
166
178
  versions: {},
167
179
  argv: [],
168
180
  type: "deno",
169
181
  };
170
-
182
+
171
183
  // Execute the WASM JS with the proper context
172
184
  const func = new Function(
173
- 'exports', 'module', 'define', 'require', 'process', '__dirname', '__filename',
174
- jsContent + '\nreturn typeof TagLibWASM !== "undefined" ? TagLibWASM : module.exports;'
185
+ "exports",
186
+ "module",
187
+ "define",
188
+ "require",
189
+ "process",
190
+ "__dirname",
191
+ "__filename",
192
+ jsContent +
193
+ '\nreturn typeof TagLibWASM !== "undefined" ? TagLibWASM : module.exports;',
175
194
  );
176
-
195
+
177
196
  TagLibWASM = func(exports, module, define, require, process, "", "");
178
197
  } else {
179
198
  // For Node.js and browsers, use normal import
180
199
  const wasmModule = await import("../build/taglib.js");
181
200
  TagLibWASM = wasmModule.default || wasmModule;
182
201
  }
183
-
184
- if (typeof TagLibWASM !== 'function') {
185
- throw new Error('Failed to load TagLib WASM module');
202
+
203
+ if (typeof TagLibWASM !== "function") {
204
+ throw new Error("Failed to load TagLib WASM module");
186
205
  }
187
-
206
+
188
207
  const wasmInstance = await TagLibWASM(moduleConfig);
189
-
208
+
190
209
  // Ensure proper memory arrays are set up
191
210
  if (!wasmInstance.HEAPU8) {
192
211
  // Manual setup if not automatically created
@@ -202,7 +221,7 @@ export async function loadTagLibModule(
202
221
  wasmInstance.HEAPF64 = new Float64Array(buffer);
203
222
  }
204
223
  }
205
-
224
+
206
225
  return wasmInstance as TagLibModule;
207
226
  } catch (error) {
208
227
  throw new Error(`Failed to load TagLib WASM: ${(error as Error).message}`);
package/src/workers.ts ADDED
@@ -0,0 +1,345 @@
1
+ /**
2
+ * @fileoverview Cloudflare Workers-specific TagLib API
3
+ */
4
+
5
+ import type {
6
+ AudioFormat,
7
+ AudioProperties,
8
+ ExtendedTag,
9
+ Tag,
10
+ TagLibConfig,
11
+ } from "./types.ts";
12
+ import {
13
+ cStringToJS,
14
+ jsToCString,
15
+ loadTagLibModuleForWorkers,
16
+ type TagLibModule,
17
+ } from "./wasm-workers.ts";
18
+
19
+ /**
20
+ * Represents an audio file with metadata and properties (Workers-compatible)
21
+ */
22
+ export class AudioFileWorkers {
23
+ private module: TagLibModule;
24
+ private fileId: number;
25
+ private tagPtr: number;
26
+ private propsPtr: number;
27
+
28
+ constructor(module: TagLibModule, fileId: number) {
29
+ this.module = module;
30
+ this.fileId = fileId;
31
+ this.tagPtr = module._taglib_file_tag(fileId);
32
+ this.propsPtr = module._taglib_file_audioproperties(fileId);
33
+ }
34
+
35
+ /**
36
+ * Check if the file is valid and was loaded successfully
37
+ */
38
+ isValid(): boolean {
39
+ return this.module._taglib_file_is_valid(this.fileId) !== 0;
40
+ }
41
+
42
+ /**
43
+ * Get the file format
44
+ */
45
+ format(): AudioFormat {
46
+ const formatPtr = this.module._taglib_file_format(this.fileId);
47
+ if (formatPtr === 0) return "MP3"; // fallback
48
+ const formatStr = cStringToJS(this.module, formatPtr);
49
+ return formatStr as AudioFormat;
50
+ }
51
+
52
+ /**
53
+ * Get basic tag information
54
+ */
55
+ tag(): Tag {
56
+ if (this.tagPtr === 0) return {};
57
+
58
+ const title = this.module._taglib_tag_title(this.tagPtr);
59
+ const artist = this.module._taglib_tag_artist(this.tagPtr);
60
+ const album = this.module._taglib_tag_album(this.tagPtr);
61
+ const comment = this.module._taglib_tag_comment(this.tagPtr);
62
+ const genre = this.module._taglib_tag_genre(this.tagPtr);
63
+ const year = this.module._taglib_tag_year(this.tagPtr);
64
+ const track = this.module._taglib_tag_track(this.tagPtr);
65
+
66
+ return {
67
+ title: title ? cStringToJS(this.module, title) : undefined,
68
+ artist: artist ? cStringToJS(this.module, artist) : undefined,
69
+ album: album ? cStringToJS(this.module, album) : undefined,
70
+ comment: comment ? cStringToJS(this.module, comment) : undefined,
71
+ genre: genre ? cStringToJS(this.module, genre) : undefined,
72
+ year: year || undefined,
73
+ track: track || undefined,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Get audio properties (duration, bitrate, etc.)
79
+ */
80
+ audioProperties(): AudioProperties | null {
81
+ if (this.propsPtr === 0) return null;
82
+
83
+ const length = this.module._taglib_audioproperties_length(this.propsPtr);
84
+ const bitrate = this.module._taglib_audioproperties_bitrate(this.propsPtr);
85
+ const sampleRate = this.module._taglib_audioproperties_samplerate(
86
+ this.propsPtr,
87
+ );
88
+ const channels = this.module._taglib_audioproperties_channels(
89
+ this.propsPtr,
90
+ );
91
+
92
+ return {
93
+ length,
94
+ bitrate,
95
+ sampleRate,
96
+ channels,
97
+ format: this.format(),
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Set the title tag
103
+ */
104
+ setTitle(title: string): void {
105
+ if (this.tagPtr === 0) return;
106
+ const titlePtr = jsToCString(this.module, title);
107
+ this.module._taglib_tag_set_title(this.tagPtr, titlePtr);
108
+ this.module._free(titlePtr);
109
+ }
110
+
111
+ /**
112
+ * Set the artist tag
113
+ */
114
+ setArtist(artist: string): void {
115
+ if (this.tagPtr === 0) return;
116
+ const artistPtr = jsToCString(this.module, artist);
117
+ this.module._taglib_tag_set_artist(this.tagPtr, artistPtr);
118
+ this.module._free(artistPtr);
119
+ }
120
+
121
+ /**
122
+ * Set the album tag
123
+ */
124
+ setAlbum(album: string): void {
125
+ if (this.tagPtr === 0) return;
126
+ const albumPtr = jsToCString(this.module, album);
127
+ this.module._taglib_tag_set_album(this.tagPtr, albumPtr);
128
+ this.module._free(albumPtr);
129
+ }
130
+
131
+ /**
132
+ * Set the comment tag
133
+ */
134
+ setComment(comment: string): void {
135
+ if (this.tagPtr === 0) return;
136
+ const commentPtr = jsToCString(this.module, comment);
137
+ this.module._taglib_tag_set_comment(this.tagPtr, commentPtr);
138
+ this.module._free(commentPtr);
139
+ }
140
+
141
+ /**
142
+ * Set the genre tag
143
+ */
144
+ setGenre(genre: string): void {
145
+ if (this.tagPtr === 0) return;
146
+ const genrePtr = jsToCString(this.module, genre);
147
+ this.module._taglib_tag_set_genre(this.tagPtr, genrePtr);
148
+ this.module._free(genrePtr);
149
+ }
150
+
151
+ /**
152
+ * Set the year tag
153
+ */
154
+ setYear(year: number): void {
155
+ if (this.tagPtr === 0) return;
156
+ this.module._taglib_tag_set_year(this.tagPtr, year);
157
+ }
158
+
159
+ /**
160
+ * Set the track number tag
161
+ */
162
+ setTrack(track: number): void {
163
+ if (this.tagPtr === 0) return;
164
+ this.module._taglib_tag_set_track(this.tagPtr, track);
165
+ }
166
+
167
+ /**
168
+ * Save changes to the file (read-only in Workers context)
169
+ * Note: This would require returning modified buffer data
170
+ */
171
+ save(): boolean {
172
+ console.warn("save(): File saving not implemented in Workers context");
173
+ return false;
174
+ }
175
+
176
+ /**
177
+ * Get extended metadata with format-agnostic field names
178
+ */
179
+ extendedTag(): ExtendedTag {
180
+ const basicTag = this.tag();
181
+
182
+ return {
183
+ ...basicTag,
184
+ // Advanced fields placeholder - would be populated by PropertyMap reading
185
+ acoustidFingerprint: undefined,
186
+ acoustidId: undefined,
187
+ musicbrainzTrackId: undefined,
188
+ musicbrainzReleaseId: undefined,
189
+ musicbrainzArtistId: undefined,
190
+ musicbrainzReleaseGroupId: undefined,
191
+ albumArtist: undefined,
192
+ composer: undefined,
193
+ discNumber: undefined,
194
+ totalTracks: undefined,
195
+ totalDiscs: undefined,
196
+ bpm: undefined,
197
+ compilation: undefined,
198
+ titleSort: undefined,
199
+ artistSort: undefined,
200
+ albumSort: undefined,
201
+ replayGainTrackGain: undefined,
202
+ replayGainTrackPeak: undefined,
203
+ replayGainAlbumGain: undefined,
204
+ replayGainAlbumPeak: undefined,
205
+ appleSoundCheck: undefined,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Set extended metadata using format-agnostic field names
211
+ */
212
+ setExtendedTag(tag: Partial<ExtendedTag>): void {
213
+ if (tag.title !== undefined) this.setTitle(tag.title);
214
+ if (tag.artist !== undefined) this.setArtist(tag.artist);
215
+ if (tag.album !== undefined) this.setAlbum(tag.album);
216
+ if (tag.comment !== undefined) this.setComment(tag.comment);
217
+ if (tag.genre !== undefined) this.setGenre(tag.genre);
218
+ if (tag.year !== undefined) this.setYear(tag.year);
219
+ if (tag.track !== undefined) this.setTrack(tag.track);
220
+ }
221
+
222
+ /**
223
+ * Clean up resources
224
+ */
225
+ dispose(): void {
226
+ if (this.fileId !== 0) {
227
+ this.module._taglib_file_delete(this.fileId);
228
+ this.fileId = 0;
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Main TagLib class for Cloudflare Workers
235
+ */
236
+ export class TagLibWorkers {
237
+ private module: TagLibModule;
238
+
239
+ private constructor(module: TagLibModule) {
240
+ this.module = module;
241
+ }
242
+
243
+ /**
244
+ * Initialize TagLib for Workers with WASM binary
245
+ *
246
+ * @param wasmBinary - The WebAssembly binary as Uint8Array
247
+ * @param config - Optional configuration for the WASM module
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * // In a Cloudflare Worker
252
+ * import wasmBinary from "../build/taglib.wasm";
253
+ *
254
+ * const taglib = await TagLibWorkers.initialize(wasmBinary);
255
+ * const file = taglib.openFile(audioBuffer);
256
+ * const metadata = file.tag();
257
+ * ```
258
+ */
259
+ static async initialize(
260
+ wasmBinary: Uint8Array,
261
+ config?: TagLibConfig,
262
+ ): Promise<TagLibWorkers> {
263
+ const module = await loadTagLibModuleForWorkers(wasmBinary, config);
264
+ return new TagLibWorkers(module);
265
+ }
266
+
267
+ /**
268
+ * Open an audio file from a buffer
269
+ */
270
+ openFile(buffer: Uint8Array): AudioFileWorkers {
271
+ if (!this.module.HEAPU8) {
272
+ throw new Error("WASM module not properly initialized - missing HEAPU8");
273
+ }
274
+
275
+ // Use Emscripten's allocate function for proper memory management
276
+ const dataPtr = this.module.allocate(buffer, this.module.ALLOC_NORMAL);
277
+
278
+ const fileId = this.module._taglib_file_new_from_buffer(
279
+ dataPtr,
280
+ buffer.length,
281
+ );
282
+
283
+ if (fileId === 0) {
284
+ console.log(
285
+ `DEBUG: File creation failed, not freeing memory at ${dataPtr}`,
286
+ );
287
+ throw new Error(
288
+ "Failed to open audio file - invalid format or corrupted data",
289
+ );
290
+ }
291
+
292
+ // Free the temporary buffer copy (TagLib has made its own copy in ByteVector)
293
+ this.module._free(dataPtr);
294
+
295
+ return new AudioFileWorkers(this.module, fileId);
296
+ }
297
+
298
+ /**
299
+ * Get the underlying WASM module (for advanced usage)
300
+ */
301
+ getModule(): TagLibModule {
302
+ return this.module;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Utility function to process audio metadata in a Cloudflare Worker
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * export default {
312
+ * async fetch(request: Request): Promise<Response> {
313
+ * if (request.method === "POST") {
314
+ * const audioData = new Uint8Array(await request.arrayBuffer());
315
+ * const metadata = await processAudioMetadata(wasmBinary, audioData);
316
+ * return Response.json(metadata);
317
+ * }
318
+ * return new Response("Method not allowed", { status: 405 });
319
+ * }
320
+ * };
321
+ * ```
322
+ */
323
+ export async function processAudioMetadata(
324
+ wasmBinary: Uint8Array,
325
+ audioData: Uint8Array,
326
+ config?: TagLibConfig,
327
+ ): Promise<
328
+ { tag: Tag; properties: AudioProperties | null; format: AudioFormat }
329
+ > {
330
+ const taglib = await TagLibWorkers.initialize(wasmBinary, config);
331
+ const file = taglib.openFile(audioData);
332
+
333
+ try {
334
+ const tag = file.tag();
335
+ const properties = file.audioProperties();
336
+ const format = file.format();
337
+
338
+ return { tag, properties, format };
339
+ } finally {
340
+ file.dispose();
341
+ }
342
+ }
343
+
344
+ // Export types for convenience
345
+ export type { AudioFormat, AudioProperties, ExtendedTag, Tag, TagLibConfig };