ts-flp 1.0.2 → 1.0.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.
@@ -1,313 +0,0 @@
1
- /**
2
- * FL Studio .flp file parser and serializer
3
- * Conservative patch approach: preserves all unknown data byte-for-byte
4
- */
5
- import { DWORD, EVENT_ID, TEXT, WORD, getEventKind } from "../generated/events.generated.js";
6
- import { BinaryReader } from "../io/BinaryReader.js";
7
- import { BinaryWriter } from "../io/BinaryWriter.js";
8
- /**
9
- * Header structure constants
10
- */
11
- const FLP_HEADER_MAGIC = "FLhd";
12
- const FLP_DATA_MAGIC = "FLdt";
13
- const FLP_HEADER_SIZE = 6; // Expected header chunk size
14
- /**
15
- * Parses an FL Studio project file
16
- *
17
- * @param buffer - Raw file data
18
- * @returns Parsed FLP structure
19
- * @throws Error if file is corrupted or invalid
20
- */
21
- export function parseFlp(buffer) {
22
- const reader = new BinaryReader(buffer);
23
- // Read FLhd header chunk
24
- const headerMagic = reader.readBytes(4).toString("ascii");
25
- if (headerMagic !== FLP_HEADER_MAGIC) {
26
- throw new Error(`Invalid header magic: expected 'FLhd', got '${headerMagic}'`);
27
- }
28
- const headerSize = reader.readU32LE();
29
- if (headerSize !== FLP_HEADER_SIZE) {
30
- throw new Error(`Invalid header size: expected ${FLP_HEADER_SIZE}, got ${headerSize}`);
31
- }
32
- const format = reader.readI16LE();
33
- const channelCount = reader.readU16LE();
34
- const ppq = reader.readU16LE();
35
- // Validate format (0 = normal project)
36
- if (format < -1 || format > 0x50) {
37
- throw new Error(`Invalid file format: ${format}`);
38
- }
39
- // Store complete header chunk (14 bytes)
40
- const headerChunkBytes = buffer.subarray(0, 14);
41
- // Read FLdt data chunk header
42
- const dataMagic = reader.readBytes(4).toString("ascii");
43
- if (dataMagic !== FLP_DATA_MAGIC) {
44
- throw new Error(`Invalid data magic: expected 'FLdt', got '${dataMagic}'`);
45
- }
46
- const eventsSize = reader.readU32LE();
47
- // Validate events size
48
- const expectedFileSize = 22 + eventsSize; // 14 (FLhd) + 8 (FLdt header) + events
49
- if (buffer.length !== expectedFileSize) {
50
- throw new Error(`Data chunk size mismatch: expected file size ${expectedFileSize}, got ${buffer.length}`);
51
- }
52
- // Store FLdt header (8 bytes)
53
- const fldtHeaderBytes = buffer.subarray(14, 22);
54
- // Parse events
55
- const events = [];
56
- let flVersion = "0.0.0";
57
- let useUnicode = false;
58
- const eventsStart = reader.tell();
59
- const eventsEnd = eventsStart + eventsSize;
60
- while (reader.tell() < eventsEnd) {
61
- const eventStart = reader.tell();
62
- const eventId = reader.readU8();
63
- let payloadSize;
64
- let headerSize;
65
- // Determine payload size based on event ID range
66
- if (eventId < WORD) {
67
- // BYTE range: 1 byte payload
68
- payloadSize = 1;
69
- headerSize = 1; // Just the ID
70
- }
71
- else if (eventId < DWORD) {
72
- // WORD range: 2 bytes payload
73
- payloadSize = 2;
74
- headerSize = 1;
75
- }
76
- else if (eventId < TEXT) {
77
- // DWORD range: 4 bytes payload
78
- payloadSize = 4;
79
- headerSize = 1;
80
- }
81
- else {
82
- // TEXT/DATA range: VarInt-encoded size
83
- const sizeStart = reader.tell();
84
- payloadSize = reader.readVarInt();
85
- headerSize = 1 + (reader.tell() - sizeStart); // ID + VarInt bytes
86
- }
87
- // Read payload
88
- const payload = reader.readBytes(payloadSize);
89
- // Extract header from original buffer for byte-perfect round-trip
90
- const header = buffer.subarray(eventStart, eventStart + headerSize);
91
- // Determine event kind
92
- const kind = getEventKind(eventId);
93
- const event = {
94
- id: eventId,
95
- kind,
96
- header: Buffer.from(header),
97
- payload: Buffer.from(payload),
98
- };
99
- events.push(event);
100
- // Detect FL version from FLVersion event
101
- if (eventId === EVENT_ID.PROJECT_FL_VERSION) {
102
- flVersion = payload.toString("ascii").replace(/\0/g, "");
103
- const parts = flVersion.split(".").map((p) => parseInt(p, 10));
104
- const major = parts[0] ?? 0;
105
- const minor = parts[1] ?? 0;
106
- useUnicode = parts.length >= 2 && (major > 11 || (major === 11 && minor >= 5));
107
- }
108
- }
109
- // Check for trailing bytes
110
- let trailingBytes;
111
- if (reader.remaining() > 0) {
112
- trailingBytes = reader.readBytes(reader.remaining());
113
- }
114
- return {
115
- headerChunkBytes: Buffer.from(headerChunkBytes),
116
- fldtHeaderBytes: Buffer.from(fldtHeaderBytes),
117
- events,
118
- trailingBytes,
119
- flVersion,
120
- useUnicode,
121
- };
122
- }
123
- /**
124
- * Serializes a parsed FLP back to a buffer
125
- * Recalculates FLdt chunk size if events have changed
126
- *
127
- * @param parsed - Parsed FLP structure
128
- * @returns Serialized buffer
129
- */
130
- export function serializeFlp(parsed) {
131
- const writer = new BinaryWriter();
132
- // Write FLhd header chunk (preserved byte-for-byte)
133
- writer.writeBytes(parsed.headerChunkBytes);
134
- // Calculate total events size
135
- let eventsSize = 0;
136
- for (const event of parsed.events) {
137
- eventsSize += serializeEvent(event).length;
138
- }
139
- // Add trailing bytes if present
140
- if (parsed.trailingBytes) {
141
- eventsSize += parsed.trailingBytes.length;
142
- }
143
- // Write FLdt header with updated size
144
- writer.writeBytes(Buffer.from(FLP_DATA_MAGIC, "ascii"));
145
- writer.writeU32LE(eventsSize);
146
- // Write all events
147
- for (const event of parsed.events) {
148
- writer.writeBytes(serializeEvent(event));
149
- }
150
- // Write trailing bytes if present
151
- if (parsed.trailingBytes) {
152
- writer.writeBytes(parsed.trailingBytes);
153
- }
154
- return writer.toBuffer();
155
- }
156
- /**
157
- * Serializes a single event to bytes
158
- * Uses original header for unchanged events, recalculates for modified ones
159
- */
160
- function serializeEvent(event) {
161
- const writer = new BinaryWriter();
162
- // Write event ID
163
- writer.writeU8(event.id);
164
- // For TEXT/DATA events, we need to write the VarInt size
165
- if (event.id >= TEXT) {
166
- writer.writeVarInt(event.payload.length);
167
- }
168
- // Write payload
169
- writer.writeBytes(event.payload);
170
- return writer.toBuffer();
171
- }
172
- /**
173
- * Creates a new event with the given ID and payload
174
- */
175
- export function createEvent(id, payload) {
176
- const kind = getEventKind(id);
177
- // Build header
178
- const headerWriter = new BinaryWriter();
179
- headerWriter.writeU8(id);
180
- if (id >= TEXT) {
181
- headerWriter.writeVarInt(payload.length);
182
- }
183
- return {
184
- id,
185
- kind,
186
- header: headerWriter.toBuffer(),
187
- payload: Buffer.from(payload),
188
- };
189
- }
190
- /**
191
- * Applies a patcher function to all events
192
- * Returns a new ParsedFlp with patched events
193
- *
194
- * @param parsed - Original parsed FLP
195
- * @param patcher - Function that transforms each event
196
- * @returns New ParsedFlp with patched events
197
- */
198
- export function patchEvents(parsed, patcher) {
199
- const patchedEvents = parsed.events.map((event, index) => {
200
- const patched = patcher(event, index);
201
- // If payload changed, recalculate header for TEXT/DATA events
202
- if (patched.payload !== event.payload && patched.id >= TEXT) {
203
- const headerWriter = new BinaryWriter();
204
- headerWriter.writeU8(patched.id);
205
- headerWriter.writeVarInt(patched.payload.length);
206
- return {
207
- ...patched,
208
- header: headerWriter.toBuffer(),
209
- };
210
- }
211
- return patched;
212
- });
213
- return {
214
- ...parsed,
215
- events: patchedEvents,
216
- };
217
- }
218
- /**
219
- * Finds all events with a specific ID
220
- */
221
- export function findEvents(parsed, eventId) {
222
- return parsed.events.filter((e) => e.id === eventId);
223
- }
224
- /**
225
- * Finds the first event with a specific ID
226
- */
227
- export function findFirstEvent(parsed, eventId) {
228
- return parsed.events.find((e) => e.id === eventId);
229
- }
230
- /**
231
- * Gets event payload as a string (handles encoding)
232
- */
233
- export function getEventString(event, useUnicode) {
234
- if (event.kind !== "text") {
235
- throw new Error(`Event ${event.id} is not a text event`);
236
- }
237
- if (useUnicode) {
238
- const str = event.payload.toString("utf16le");
239
- const nullPos = str.indexOf("\0");
240
- return nullPos === -1 ? str : str.substring(0, nullPos);
241
- }
242
- else {
243
- const str = event.payload.toString("ascii");
244
- const nullPos = str.indexOf("\0");
245
- return nullPos === -1 ? str : str.substring(0, nullPos);
246
- }
247
- }
248
- /**
249
- * Creates a text payload with proper encoding
250
- */
251
- export function createTextPayload(text, useUnicode) {
252
- if (useUnicode) {
253
- return Buffer.from(text + "\0", "utf16le");
254
- }
255
- else {
256
- return Buffer.from(text + "\0", "ascii");
257
- }
258
- }
259
- /**
260
- * Gets event payload as a number
261
- */
262
- export function getEventNumber(event) {
263
- const reader = new BinaryReader(event.payload);
264
- switch (event.kind) {
265
- case "u8":
266
- return reader.readU8();
267
- case "i8":
268
- return reader.readI8();
269
- case "u16":
270
- return reader.readU16LE();
271
- case "i16":
272
- return reader.readI16LE();
273
- case "u32":
274
- return reader.readU32LE();
275
- case "i32":
276
- return reader.readI32LE();
277
- case "f32":
278
- return reader.readF32LE();
279
- default:
280
- throw new Error(`Cannot get number from ${event.kind} event`);
281
- }
282
- }
283
- /**
284
- * Creates a numeric payload
285
- */
286
- export function createNumberPayload(value, kind) {
287
- const writer = new BinaryWriter();
288
- switch (kind) {
289
- case "u8":
290
- writer.writeU8(value);
291
- break;
292
- case "i8":
293
- writer.writeI8(value);
294
- break;
295
- case "u16":
296
- writer.writeU16LE(value);
297
- break;
298
- case "i16":
299
- writer.writeI16LE(value);
300
- break;
301
- case "u32":
302
- writer.writeU32LE(value);
303
- break;
304
- case "i32":
305
- writer.writeI32LE(value);
306
- break;
307
- case "f32":
308
- writer.writeF32LE(value);
309
- break;
310
- }
311
- return writer.toBuffer();
312
- }
313
- //# sourceMappingURL=FlpParser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FlpParser.js","sourceRoot":"","sources":["../../../src/parser/FlpParser.ts"],"names":[],"mappings":"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"}
@@ -1,345 +0,0 @@
1
- /**
2
- * FLP Parser unit tests
3
- * Converted from flparser_unitest.py
4
- */
5
- import * as fs from "fs";
6
- import * as path from "path";
7
- import { beforeAll, describe, expect, it } from "vitest";
8
- import { getFlVersion, listPlugins, listSamples, parseFlp, readProjectMeta, readProjectTimeInfo, rewriteSamplePaths, serializeFlp, writeProjectMeta, writeProjectTimeInfo } from "../index.js";
9
- // Test configuration
10
- const TEST_PROJS_DIR = path.join(import.meta.dirname, "..", "test_projs");
11
- const EXPECTED_FL_VERSION = "24"; // Update this to match your FL Studio version
12
- /**
13
- * Helper function to get test FLP files
14
- */
15
- function getTestableProjects() {
16
- if (!fs.existsSync(TEST_PROJS_DIR)) {
17
- return [];
18
- }
19
- return fs.readdirSync(TEST_PROJS_DIR).filter((f) => f.endsWith(".flp"));
20
- }
21
- /**
22
- * Helper function to load and parse a test FLP file
23
- */
24
- function loadTestProject(filename) {
25
- const filepath = path.join(TEST_PROJS_DIR, filename);
26
- const buffer = fs.readFileSync(filepath);
27
- return parseFlp(buffer);
28
- }
29
- function randomString(length) {
30
- let result = "";
31
- while (result.length < length) {
32
- result += Math.random().toString(36).substring(2);
33
- }
34
- return result.substring(0, length);
35
- }
36
- function randomBPM() {
37
- // return a random BPM between 50 and 190
38
- return Math.floor(Math.random() * 140) + 50;
39
- }
40
- function randomDecimalBPM() {
41
- // Random BPM with decimals between 60 and 200
42
- return Math.floor(Math.random() * 140000 + 60000) / 1000;
43
- }
44
- function randomTimestamp() {
45
- // Random date between 2020 and 2025
46
- const start = new Date(2020, 0, 1).getTime();
47
- const end = new Date(2025, 11, 31).getTime();
48
- return new Date(start + Math.random() * (end - start));
49
- }
50
- function randomWorkTime() {
51
- // Random work time between 0 and 1000000 seconds (~277 hours)
52
- return Math.floor(Math.random() * 1000000);
53
- }
54
- function randomEmoji() {
55
- const emojis = ["🎵", "🎹", "🎸", "🥁", "🎤", "🔊", "🎧", "🎼", "🤓", "👨‍🍳", "🔥", "💯", "✨", "🚀"];
56
- return emojis[Math.floor(Math.random() * emojis.length)];
57
- }
58
- describe("FLP Parser", () => {
59
- const testProjects = getTestableProjects();
60
- beforeAll(() => {
61
- if (testProjects.length === 0) {
62
- console.warn(`No test projects found in ${TEST_PROJS_DIR}`);
63
- console.warn("Create a 'test_projs' folder with .flp files to run tests");
64
- }
65
- });
66
- describe("Basic Parsing", () => {
67
- it.skipIf(testProjects.length === 0)("should parse FLP file without errors", () => {
68
- const project = loadTestProject(testProjects[0]);
69
- expect(project).toBeDefined();
70
- expect(project.events).toBeDefined();
71
- expect(project.events.length).toBeGreaterThan(0);
72
- });
73
- it.skipIf(testProjects.length === 0)("should detect FL version", () => {
74
- const project = loadTestProject(testProjects[0]);
75
- const version = getFlVersion(project);
76
- expect(version).toBeDefined();
77
- expect(version.length).toBeGreaterThan(0);
78
- console.log(`Detected FL version: ${version}`);
79
- });
80
- it.skipIf(testProjects.length === 0)(`should be FL Studio version ${EXPECTED_FL_VERSION}.x`, () => {
81
- const project = loadTestProject(testProjects[0]);
82
- const version = getFlVersion(project);
83
- expect(version.startsWith(EXPECTED_FL_VERSION)).toBe(true);
84
- });
85
- });
86
- describe("Round-trip Stability", () => {
87
- it.skipIf(testProjects.length === 0)("should produce identical output when parsing and re-serializing without changes", () => {
88
- const filepath = path.join(TEST_PROJS_DIR, testProjects[0]);
89
- const originalBuffer = fs.readFileSync(filepath);
90
- const parsed = parseFlp(originalBuffer);
91
- const reserialized = serializeFlp(parsed);
92
- expect(reserialized.length).toBe(originalBuffer.length);
93
- expect(reserialized.equals(originalBuffer)).toBe(true);
94
- });
95
- });
96
- describe("Case 1: Basic metadata (title, artist, description, genre)", () => {
97
- it.skipIf(testProjects.length === 0)("should read and write basic metadata", () => {
98
- const project = loadTestProject(testProjects[0]);
99
- const name = randomString(10);
100
- const artist = randomString(10);
101
- const description = randomString(100);
102
- const genre = randomString(15);
103
- const modified = writeProjectMeta(project, {
104
- name: name,
105
- artist: artist,
106
- description: description,
107
- genre: genre,
108
- });
109
- const meta = readProjectMeta(modified);
110
- expect(meta.name).toBe(name);
111
- expect(meta.artist).toBe(artist);
112
- expect(meta.description).toContain(description);
113
- expect(meta.genre).toBe(genre);
114
- });
115
- });
116
- describe("Case 2: Metadata with genre and tempo", () => {
117
- it.skipIf(testProjects.length === 0)("should read and write metadata with genre and tempo", () => {
118
- const project = loadTestProject(testProjects[0]);
119
- const name = randomString(10);
120
- const artist = randomString(10);
121
- const description = randomString(100);
122
- const genre = randomString(15);
123
- const bpm = randomBPM();
124
- const modified = writeProjectMeta(project, {
125
- name: name,
126
- artist: artist,
127
- description: description,
128
- genre: genre,
129
- bpm: bpm,
130
- });
131
- const meta = readProjectMeta(modified);
132
- expect(meta.name).toBe(name);
133
- expect(meta.artist).toBe(artist);
134
- expect(meta.description).toContain(description);
135
- expect(meta.genre).toBe(genre);
136
- expect(meta.bpm).toBe(bpm);
137
- });
138
- });
139
- describe("Case 3: Decimal tempo", () => {
140
- it.skipIf(testProjects.length === 0)("should handle decimal tempo values", () => {
141
- const project = loadTestProject(testProjects[0]);
142
- const name = randomString(10);
143
- const bpm = randomBPM();
144
- const modified = writeProjectMeta(project, {
145
- name: name,
146
- bpm: bpm,
147
- });
148
- const meta = readProjectMeta(modified);
149
- // BPM is stored as integer * 1000, so we get 3 decimal places precision
150
- expect(meta.bpm).toBeCloseTo(bpm, 3);
151
- });
152
- });
153
- describe("Case 4: Type coercion", () => {
154
- it.skipIf(testProjects.length === 0)("should handle numeric values as strings", () => {
155
- const project = loadTestProject(testProjects[0]);
156
- const bpm = randomBPM();
157
- // In TypeScript we enforce types, but we test that numeric strings work
158
- const modified = writeProjectMeta(project, {
159
- name: String(89465),
160
- description: String(10231658784241),
161
- bpm: bpm,
162
- });
163
- const meta = readProjectMeta(modified);
164
- expect(meta.name).toBe("89465");
165
- expect(meta.description).toBe("10231658784241");
166
- expect(meta.bpm).toBeCloseTo(bpm, 3);
167
- });
168
- });
169
- describe("Case 5: Samples", () => {
170
- it.skipIf(testProjects.length === 0)("should list samples", () => {
171
- const project = loadTestProject(testProjects[0]);
172
- const samples = listSamples(project);
173
- // Just verify we can list samples without error
174
- expect(Array.isArray(samples)).toBe(true);
175
- console.log(`Found ${samples.length} samples`);
176
- if (samples.length > 0) {
177
- expect(samples[0]).toHaveProperty("path");
178
- expect(samples[0]).toHaveProperty("eventIndex");
179
- }
180
- });
181
- it.skipIf(testProjects.length === 0)("should rewrite sample paths with random prefix", () => {
182
- const project = loadTestProject(testProjects[0]);
183
- const originalSamples = listSamples(project);
184
- if (originalSamples.length > 0) {
185
- const randomPrefix = randomString(8);
186
- const modified = rewriteSamplePaths(project, (oldPath) => oldPath.replace("C:\\", `${randomPrefix}:\\`));
187
- const newSamples = listSamples(modified);
188
- expect(newSamples.length).toBe(originalSamples.length);
189
- // Verify at least one path was modified
190
- const modifiedPaths = newSamples.filter((s) => s.path.includes(randomPrefix));
191
- if (originalSamples.some((s) => s.path.includes("C:\\"))) {
192
- expect(modifiedPaths.length).toBeGreaterThan(0);
193
- }
194
- }
195
- });
196
- });
197
- describe("Case 6: Unicode/Emoji support", () => {
198
- it.skipIf(testProjects.length === 0)("should handle emojis in metadata", () => {
199
- const project = loadTestProject(testProjects[0]);
200
- const emoji1 = randomEmoji();
201
- const emoji2 = randomEmoji();
202
- const baseName = randomString(10);
203
- const artist = randomString(10);
204
- const description = randomString(50);
205
- const nameWithEmoji = `${baseName} ${emoji1}${emoji2}`;
206
- const descWithEmoji = `${description} ${emoji1}`;
207
- const modified = writeProjectMeta(project, {
208
- name: nameWithEmoji,
209
- artist: artist,
210
- description: descWithEmoji,
211
- });
212
- const meta = readProjectMeta(modified);
213
- expect(meta.name).toBe(nameWithEmoji);
214
- expect(meta.artist).toBe(artist);
215
- expect(meta.description).toBe(descWithEmoji);
216
- });
217
- });
218
- describe("Case 7: createdAt timestamp", () => {
219
- it.skipIf(testProjects.length === 0)("should read and write creation date", () => {
220
- const project = loadTestProject(testProjects[0]);
221
- const testDate = randomTimestamp();
222
- const modified = writeProjectTimeInfo(project, {
223
- creationDate: testDate,
224
- });
225
- const timeInfo = readProjectTimeInfo(modified);
226
- expect(timeInfo.creationDate).toBeDefined();
227
- if (timeInfo.creationDate) {
228
- // Allow 1 second tolerance due to float precision
229
- const diff = Math.abs(timeInfo.creationDate.getTime() - testDate.getTime());
230
- expect(diff).toBeLessThan(1000);
231
- }
232
- });
233
- });
234
- describe("Case 8: workTime = 0", () => {
235
- it.skipIf(testProjects.length === 0)("should handle zero work time", () => {
236
- const project = loadTestProject(testProjects[0]);
237
- const modified = writeProjectTimeInfo(project, {
238
- workTimeSeconds: 0,
239
- });
240
- const timeInfo = readProjectTimeInfo(modified);
241
- expect(timeInfo.workTimeSeconds).toBe(0);
242
- });
243
- });
244
- describe("Case 9: workTime with value", () => {
245
- it.skipIf(testProjects.length === 0)("should read and write work time", () => {
246
- const project = loadTestProject(testProjects[0]);
247
- const workTime = randomWorkTime();
248
- const modified = writeProjectTimeInfo(project, {
249
- workTimeSeconds: workTime,
250
- });
251
- const timeInfo = readProjectTimeInfo(modified);
252
- expect(timeInfo.workTimeSeconds).toBeCloseTo(workTime, 1);
253
- });
254
- });
255
- describe("Plugins", () => {
256
- it.skipIf(testProjects.length === 0)("should list plugins", () => {
257
- const project = loadTestProject(testProjects[0]);
258
- const plugins = listPlugins(project);
259
- expect(Array.isArray(plugins)).toBe(true);
260
- console.log(`Found ${plugins.length} plugins`);
261
- for (const plugin of plugins) {
262
- console.log(` - ${plugin.name} (${plugin.vendor ?? "N/A"})`);
263
- }
264
- });
265
- });
266
- describe("Combined metadata and time info", () => {
267
- it.skipIf(testProjects.length === 0)("should handle all modifications together", () => {
268
- const project = loadTestProject(testProjects[0]);
269
- // Generate random test data
270
- const emoji = randomEmoji();
271
- const name = `${randomString(10)} ${emoji}`;
272
- const artist = randomString(10);
273
- const description = randomString(100);
274
- const genre = randomString(15);
275
- const bpm = randomDecimalBPM();
276
- const testDate = randomTimestamp();
277
- const workTime = randomWorkTime();
278
- // Apply metadata changes
279
- let modified = writeProjectMeta(project, {
280
- name: name,
281
- artist: artist,
282
- description: description,
283
- genre: genre,
284
- bpm: bpm,
285
- });
286
- // Apply time info changes
287
- modified = writeProjectTimeInfo(modified, {
288
- creationDate: testDate,
289
- workTimeSeconds: workTime,
290
- });
291
- // Verify all changes
292
- const meta = readProjectMeta(modified);
293
- expect(meta.name).toBe(name);
294
- expect(meta.artist).toBe(artist);
295
- expect(meta.description).toBe(description);
296
- expect(meta.genre).toBe(genre);
297
- expect(meta.bpm).toBeCloseTo(bpm, 3);
298
- const timeInfo = readProjectTimeInfo(modified);
299
- expect(timeInfo.workTimeSeconds).toBeCloseTo(workTime, 1);
300
- // Verify round-trip works
301
- const serialized = serializeFlp(modified);
302
- const reparsed = parseFlp(serialized);
303
- const reMeta = readProjectMeta(reparsed);
304
- expect(reMeta.name).toBe(name);
305
- expect(reMeta.artist).toBe(artist);
306
- expect(reMeta.genre).toBe(genre);
307
- expect(reMeta.bpm).toBeCloseTo(bpm, 3);
308
- // Save the modified FLP to demo folder
309
- const demoDir = path.join(TEST_PROJS_DIR, "demo");
310
- if (!fs.existsSync(demoDir)) {
311
- fs.mkdirSync(demoDir, { recursive: true });
312
- }
313
- const outputPath = path.join(demoDir, `modified_${testProjects[0]}`);
314
- fs.writeFileSync(outputPath, serialized);
315
- console.log(`Saved modified FLP to: ${outputPath}`);
316
- });
317
- });
318
- });
319
- // Run all tests on each available test project
320
- describe.skipIf(getTestableProjects().length === 0)("All test projects", () => {
321
- const projects = getTestableProjects();
322
- for (const projectFile of projects) {
323
- describe(`Project: ${projectFile}`, () => {
324
- it("should parse without errors", () => {
325
- const project = loadTestProject(projectFile);
326
- expect(project).toBeDefined();
327
- expect(project.events.length).toBeGreaterThan(0);
328
- });
329
- it("should round-trip without data loss", () => {
330
- const filepath = path.join(TEST_PROJS_DIR, projectFile);
331
- const original = fs.readFileSync(filepath);
332
- const parsed = parseFlp(original);
333
- const reserialized = serializeFlp(parsed);
334
- expect(reserialized.equals(original)).toBe(true);
335
- });
336
- it("should read metadata", () => {
337
- const project = loadTestProject(projectFile);
338
- const meta = readProjectMeta(project);
339
- expect(meta).toBeDefined();
340
- console.log(` ${projectFile}: "${meta.name}" @ ${meta.bpm} BPM`);
341
- });
342
- });
343
- }
344
- });
345
- //# sourceMappingURL=flparser.test.js.map