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/README.md +143 -80
- package/index.ts +21 -0
- package/package.json +21 -10
- package/src/enhanced-api.ts +66 -44
- package/src/taglib-jsr.ts +394 -0
- package/src/taglib.ts +30 -14
- package/src/types.ts +20 -16
- package/src/wasm-jsr.ts +179 -0
- package/src/wasm-workers.ts +159 -0
- package/src/wasm.ts +41 -22
- package/src/workers.ts +345 -0
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
|
|
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
|
-
|
|
174
|
-
|
|
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 !==
|
|
185
|
-
throw new Error(
|
|
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 };
|