ts-flp 1.0.5 → 1.1.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.
- package/LICENSE +21 -674
- package/README.md +49 -1
- package/dist/index.cjs +300 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +489 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +299 -150
- package/dist/index.js.map +1 -1
- package/dist/src/api/ProjectApi.d.ts.map +1 -1
- package/dist/src/api/inspectSamples.d.ts +4 -0
- package/dist/src/api/inspectSamples.d.ts.map +1 -0
- package/dist/src/generated/events.generated.d.ts.map +1 -1
- package/dist/src/parser/FlpParser.d.ts +2 -6
- package/dist/src/parser/FlpParser.d.ts.map +1 -1
- package/dist/src/samples/inspectReferences.d.ts +40 -0
- package/dist/src/samples/inspectReferences.d.ts.map +1 -0
- package/package.json +16 -8
- package/dist/test/dump-all-info.d.ts +0 -2
- package/dist/test/dump-all-info.d.ts.map +0 -1
- package/dist/test/flparser.test.d.ts +0 -6
- package/dist/test/flparser.test.d.ts.map +0 -1
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -160,6 +160,14 @@ interface PluginRef {
|
|
|
160
160
|
| `getFlVersion(parsed)` | Get the FL Studio version string |
|
|
161
161
|
| `getPPQ(parsed)` | Get the PPQ (Pulses Per Quarter note) |
|
|
162
162
|
|
|
163
|
+
### Write behavior
|
|
164
|
+
|
|
165
|
+
- `undefined` leaves the corresponding value unchanged.
|
|
166
|
+
- `null` clears text metadata, leaves BPM unchanged, and resets time values to zero.
|
|
167
|
+
- Missing metadata and timestamp events are created in the project preamble.
|
|
168
|
+
- BPM values are validated against the limits supported by the detected FL Studio version.
|
|
169
|
+
- Invalid dates, negative work times, and non-finite numeric values throw an error.
|
|
170
|
+
|
|
163
171
|
## Constraints and limitations
|
|
164
172
|
|
|
165
173
|
### What this library does
|
|
@@ -197,6 +205,9 @@ src/
|
|
|
197
205
|
# Install dependencies
|
|
198
206
|
pnpm install
|
|
199
207
|
|
|
208
|
+
# Add at least one private .flp file directly to test_projs/
|
|
209
|
+
# These fixtures are ignored by Git and are required by the integration tests.
|
|
210
|
+
|
|
200
211
|
# Type checking
|
|
201
212
|
pnpm run typecheck
|
|
202
213
|
|
|
@@ -215,7 +226,7 @@ pnpm run build
|
|
|
215
226
|
|
|
216
227
|
## License
|
|
217
228
|
|
|
218
|
-
[
|
|
229
|
+
[MIT](LICENSE)
|
|
219
230
|
|
|
220
231
|
---
|
|
221
232
|
|
|
@@ -224,3 +235,40 @@ pnpm run build
|
|
|
224
235
|
A huge thank you to **[@demberto](https://github.com/demberto)** ([PyFLP](https://github.com/demberto/PyFLP)), **[@monadgroup](https://github.com/monadgroup)** ([FLParser](https://github.com/monadgroup/FLParser)), and **FLPEdit** for their incredible reverse-engineering work on the FL Studio `.flp` format.
|
|
225
236
|
|
|
226
237
|
This project would never have been possible without them.
|
|
238
|
+
|
|
239
|
+
## Optional sample inspection
|
|
240
|
+
|
|
241
|
+
Existing `listSamples(parsed)` remains synchronous and returns the same
|
|
242
|
+
`{ eventIndex, path }` entries without accessing disk. Use the additive API:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { inspectSamples } from 'ts-flp';
|
|
246
|
+
const samples = await inspectSamples(parsed, {
|
|
247
|
+
mode: 'check', // default: 'resolve', which does not access the filesystem
|
|
248
|
+
projectPath: '/music/Project/Song.flp',
|
|
249
|
+
searchRoots: [],
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Results preserve occurrences (`referenceIndex` is the original event index) and
|
|
254
|
+
include `recordedPath`, `pathType`, `relativePath`, `candidates`, `resolvedPath`,
|
|
255
|
+
`status`, and `reason`. Status is `unchecked`, `found`, `missing`, `unresolved`,
|
|
256
|
+
`ambiguous`, or `inaccessible`. Found means a regular file, not validated audio.
|
|
257
|
+
Missing means absent at the checked locations, not absent everywhere.
|
|
258
|
+
|
|
259
|
+
Relative paths use explicit `projectRoot`, or the parent of `projectPath`.
|
|
260
|
+
Context paths must be native absolute paths; no implicit cwd resolution.
|
|
261
|
+
Foreign Windows paths on POSIX and unexpanded DAW placeholders remain unresolved.
|
|
262
|
+
`searchRoots` opt into recursive discovery in check mode only; name-only matches
|
|
263
|
+
remain suggestions (`confidence: 'name-only'`) and never resolve automatically.
|
|
264
|
+
Multiple existing exact candidates also remain ambiguous.
|
|
265
|
+
|
|
266
|
+
Checks are asynchronous with `concurrency` (default 8, maximum 64), optional
|
|
267
|
+
`signal` cancellation, per-call caching and `maxSearchEntries` (default 100,000).
|
|
268
|
+
Directory symlinks encountered during search are not traversed. No project or
|
|
269
|
+
sample files are modified. Dependency files hidden inside plugin state are not
|
|
270
|
+
inventoried by this API. The result is a filesystem snapshot and can become stale.
|
|
271
|
+
|
|
272
|
+
Release preparation: after building, run `pnpm run check:package` to inspect a
|
|
273
|
+
real npm archive, install it in isolation and verify ESM/CommonJS imports.
|
|
274
|
+
This does not publish a package or change its version.
|
package/dist/index.cjs
CHANGED
|
@@ -46,6 +46,7 @@ __export(index_exports, {
|
|
|
46
46
|
getEventString: () => getEventString,
|
|
47
47
|
getFlVersion: () => getFlVersion,
|
|
48
48
|
getPPQ: () => getPPQ,
|
|
49
|
+
inspectSamples: () => inspectSamples,
|
|
49
50
|
listPlugins: () => listPlugins,
|
|
50
51
|
listSamples: () => listSamples,
|
|
51
52
|
parseFlp: () => parseFlp,
|
|
@@ -65,6 +66,9 @@ var WORD = 64;
|
|
|
65
66
|
var DWORD = 128;
|
|
66
67
|
var TEXT = 192;
|
|
67
68
|
var DATA = 208;
|
|
69
|
+
var FIXED_SIZE_OVERRIDES = {
|
|
70
|
+
172: 3
|
|
71
|
+
};
|
|
68
72
|
var NEW_TEXT_IDS = [
|
|
69
73
|
TEXT + 49,
|
|
70
74
|
// ArrangementID.Name
|
|
@@ -211,6 +215,7 @@ var EVENT_ID = {
|
|
|
211
215
|
// 236
|
|
212
216
|
};
|
|
213
217
|
var EVENT_KIND = {
|
|
218
|
+
[172]: "unknown",
|
|
214
219
|
// Project events
|
|
215
220
|
[EVENT_ID.PROJECT_LOOP_ACTIVE]: "u8",
|
|
216
221
|
[EVENT_ID.PROJECT_SHOW_INFO]: "u8",
|
|
@@ -319,6 +324,10 @@ function getEventKind(eventId) {
|
|
|
319
324
|
}
|
|
320
325
|
}
|
|
321
326
|
function getEventFixedSize(eventId) {
|
|
327
|
+
const override = FIXED_SIZE_OVERRIDES[eventId];
|
|
328
|
+
if (override !== void 0) {
|
|
329
|
+
return override;
|
|
330
|
+
}
|
|
322
331
|
if (eventId < WORD) {
|
|
323
332
|
return 1;
|
|
324
333
|
} else if (eventId < DWORD) {
|
|
@@ -595,62 +604,9 @@ function varIntSize(value) {
|
|
|
595
604
|
}
|
|
596
605
|
|
|
597
606
|
// src/parser/FlpParser.ts
|
|
598
|
-
var KNOWN_DWORD_IDS = new Set(
|
|
599
|
-
Object.keys(EVENT_KIND).map(Number).filter((id) => id >= DWORD && id < TEXT)
|
|
600
|
-
);
|
|
601
607
|
var FLP_HEADER_MAGIC = "FLhd";
|
|
602
608
|
var FLP_DATA_MAGIC = "FLdt";
|
|
603
609
|
var FLP_HEADER_SIZE = 6;
|
|
604
|
-
var LOOKAHEAD_BYTES = 200;
|
|
605
|
-
var LOOKAHEAD_VARINT_MAX = 1e5;
|
|
606
|
-
function readVarIntRaw(buf, off, end) {
|
|
607
|
-
let shift = 0;
|
|
608
|
-
let size = 0;
|
|
609
|
-
const start = off;
|
|
610
|
-
while (true) {
|
|
611
|
-
if (off >= end) return { size: -1, bytesRead: 0 };
|
|
612
|
-
const byte = buf[off];
|
|
613
|
-
off++;
|
|
614
|
-
size |= (byte & 127) << shift;
|
|
615
|
-
shift += 7;
|
|
616
|
-
if (!(byte & 128)) break;
|
|
617
|
-
}
|
|
618
|
-
return { size, bytesRead: off - start };
|
|
619
|
-
}
|
|
620
|
-
function scoreAlignment(buf, off, end, bytesToCheck) {
|
|
621
|
-
const limit = Math.min(off + bytesToCheck, end);
|
|
622
|
-
let textDataCount = 0;
|
|
623
|
-
let consecutiveSmallByte = 0;
|
|
624
|
-
let maxConsecutiveSmallByte = 0;
|
|
625
|
-
while (off < limit) {
|
|
626
|
-
const id = buf[off];
|
|
627
|
-
if (id < WORD) {
|
|
628
|
-
off += 2;
|
|
629
|
-
if (id < 32 && !KNOWN_DWORD_IDS.has(id)) {
|
|
630
|
-
consecutiveSmallByte++;
|
|
631
|
-
maxConsecutiveSmallByte = Math.max(maxConsecutiveSmallByte, consecutiveSmallByte);
|
|
632
|
-
} else {
|
|
633
|
-
consecutiveSmallByte = 0;
|
|
634
|
-
}
|
|
635
|
-
} else if (id < DWORD) {
|
|
636
|
-
off += 3;
|
|
637
|
-
consecutiveSmallByte = 0;
|
|
638
|
-
} else if (id < TEXT) {
|
|
639
|
-
off += 5;
|
|
640
|
-
consecutiveSmallByte = 0;
|
|
641
|
-
} else {
|
|
642
|
-
const vi = readVarIntRaw(buf, off + 1, end);
|
|
643
|
-
if (vi.size >= 0 && vi.size < LOOKAHEAD_VARINT_MAX && off + 1 + vi.bytesRead + vi.size <= end) {
|
|
644
|
-
textDataCount++;
|
|
645
|
-
off = off + 1 + vi.bytesRead + vi.size;
|
|
646
|
-
} else {
|
|
647
|
-
return -100;
|
|
648
|
-
}
|
|
649
|
-
consecutiveSmallByte = 0;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
return textDataCount * 10 - maxConsecutiveSmallByte * 3;
|
|
653
|
-
}
|
|
654
610
|
function parseFlp(buffer) {
|
|
655
611
|
const reader = new BinaryReader(buffer);
|
|
656
612
|
const headerMagic = reader.readBytes(4).toString("ascii");
|
|
@@ -662,8 +618,8 @@ function parseFlp(buffer) {
|
|
|
662
618
|
throw new Error(`Invalid header size: expected ${FLP_HEADER_SIZE}, got ${headerSize}`);
|
|
663
619
|
}
|
|
664
620
|
const format = reader.readI16LE();
|
|
665
|
-
|
|
666
|
-
|
|
621
|
+
reader.readU16LE();
|
|
622
|
+
reader.readU16LE();
|
|
667
623
|
if (format < -1 || format > 80) {
|
|
668
624
|
throw new Error(`Invalid file format: ${format}`);
|
|
669
625
|
}
|
|
@@ -690,39 +646,10 @@ function parseFlp(buffer) {
|
|
|
690
646
|
const eventId = reader.readU8();
|
|
691
647
|
let payloadSize;
|
|
692
648
|
let headerSize2;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
} else if (eventId < DWORD) {
|
|
697
|
-
payloadSize = 2;
|
|
649
|
+
const fixedSize = getEventFixedSize(eventId);
|
|
650
|
+
if (fixedSize >= 0) {
|
|
651
|
+
payloadSize = fixedSize;
|
|
698
652
|
headerSize2 = 1;
|
|
699
|
-
} else if (eventId < TEXT) {
|
|
700
|
-
if (KNOWN_DWORD_IDS.has(eventId)) {
|
|
701
|
-
payloadSize = 4;
|
|
702
|
-
headerSize2 = 1;
|
|
703
|
-
} else {
|
|
704
|
-
const afterDword = eventStart + 1 + 4;
|
|
705
|
-
const vi = readVarIntRaw(buffer, eventStart + 1, eventsEnd);
|
|
706
|
-
if (vi.size < 0 || vi.size > LOOKAHEAD_VARINT_MAX || eventStart + 1 + vi.bytesRead + vi.size > eventsEnd) {
|
|
707
|
-
payloadSize = 4;
|
|
708
|
-
headerSize2 = 1;
|
|
709
|
-
} else if (vi.size === 3) {
|
|
710
|
-
payloadSize = 4;
|
|
711
|
-
headerSize2 = 1;
|
|
712
|
-
} else {
|
|
713
|
-
const afterVarInt = eventStart + 1 + vi.bytesRead + vi.size;
|
|
714
|
-
const dwordScore = scoreAlignment(buffer, afterDword, eventsEnd, LOOKAHEAD_BYTES);
|
|
715
|
-
const varIntScore = scoreAlignment(buffer, afterVarInt, eventsEnd, LOOKAHEAD_BYTES);
|
|
716
|
-
if (varIntScore > dwordScore + 2) {
|
|
717
|
-
payloadSize = vi.size;
|
|
718
|
-
reader.seek(eventStart + 1 + vi.bytesRead);
|
|
719
|
-
headerSize2 = 1 + vi.bytesRead;
|
|
720
|
-
} else {
|
|
721
|
-
payloadSize = 4;
|
|
722
|
-
headerSize2 = 1;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
653
|
} else {
|
|
727
654
|
const sizeStart = reader.tell();
|
|
728
655
|
payloadSize = reader.readVarInt();
|
|
@@ -905,6 +832,46 @@ function createNumberPayload(value, kind) {
|
|
|
905
832
|
}
|
|
906
833
|
|
|
907
834
|
// src/api/ProjectApi.ts
|
|
835
|
+
var PROJECT_EVENT_ORDER = [
|
|
836
|
+
EVENT_ID.PROJECT_TEMPO,
|
|
837
|
+
EVENT_ID.PROJECT_TITLE,
|
|
838
|
+
EVENT_ID.PROJECT_GENRE,
|
|
839
|
+
EVENT_ID.PROJECT_ARTISTS,
|
|
840
|
+
EVENT_ID.PROJECT_COMMENTS,
|
|
841
|
+
EVENT_ID.PROJECT_TIMESTAMP
|
|
842
|
+
];
|
|
843
|
+
function insertProjectEvent(parsed, event) {
|
|
844
|
+
const orderIndex = PROJECT_EVENT_ORDER.indexOf(event.id);
|
|
845
|
+
let insertionIndex = -1;
|
|
846
|
+
if (orderIndex >= 0) {
|
|
847
|
+
const laterIds = new Set(PROJECT_EVENT_ORDER.slice(orderIndex + 1));
|
|
848
|
+
insertionIndex = parsed.events.findIndex((candidate) => laterIds.has(candidate.id));
|
|
849
|
+
if (insertionIndex < 0) {
|
|
850
|
+
const previousIds = new Set(PROJECT_EVENT_ORDER.slice(0, orderIndex + 1));
|
|
851
|
+
for (let index = parsed.events.length - 1; index >= 0; index--) {
|
|
852
|
+
if (previousIds.has(parsed.events[index].id)) {
|
|
853
|
+
insertionIndex = index + 1;
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (insertionIndex < 0) {
|
|
860
|
+
insertionIndex = parsed.events.findIndex((candidate) => candidate.id === EVENT_ID.CHANNEL_NEW);
|
|
861
|
+
}
|
|
862
|
+
if (insertionIndex < 0) {
|
|
863
|
+
const versionIndex = parsed.events.findIndex(
|
|
864
|
+
(candidate) => candidate.id === EVENT_ID.PROJECT_FL_VERSION
|
|
865
|
+
);
|
|
866
|
+
insertionIndex = versionIndex >= 0 ? versionIndex + 1 : parsed.events.length;
|
|
867
|
+
}
|
|
868
|
+
const events = [...parsed.events];
|
|
869
|
+
events.splice(insertionIndex, 0, event);
|
|
870
|
+
return { ...parsed, events };
|
|
871
|
+
}
|
|
872
|
+
function replaceEventPayload(parsed, target, payload) {
|
|
873
|
+
return patchEvents(parsed, (event) => event === target ? { ...event, payload } : event);
|
|
874
|
+
}
|
|
908
875
|
function readBpm(parsed) {
|
|
909
876
|
const tempoEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
|
|
910
877
|
if (tempoEvent) {
|
|
@@ -934,48 +901,97 @@ function readProjectMeta(parsed) {
|
|
|
934
901
|
bpm: readBpm(parsed)
|
|
935
902
|
};
|
|
936
903
|
}
|
|
904
|
+
function compareVersion(version, expected) {
|
|
905
|
+
const actual = version.split(".").map((part) => Number.parseInt(part, 10));
|
|
906
|
+
const length = Math.max(actual.length, expected.length);
|
|
907
|
+
for (let index = 0; index < length; index++) {
|
|
908
|
+
const difference = (actual[index] ?? 0) - (expected[index] ?? 0);
|
|
909
|
+
if (difference !== 0) return Math.sign(difference);
|
|
910
|
+
}
|
|
911
|
+
return 0;
|
|
912
|
+
}
|
|
913
|
+
function validateBpm(parsed, bpm) {
|
|
914
|
+
if (!Number.isFinite(bpm)) {
|
|
915
|
+
throw new RangeError("BPM must be a finite number");
|
|
916
|
+
}
|
|
917
|
+
const legacyHighTempo = compareVersion(parsed.flVersion, [1, 4, 2]) >= 0 && compareVersion(parsed.flVersion, [11]) < 0;
|
|
918
|
+
const maximum = legacyHighTempo ? 999 : 522;
|
|
919
|
+
if (bpm < 10 || bpm > maximum) {
|
|
920
|
+
throw new RangeError(`BPM must be between 10 and ${maximum} for FL Studio ${parsed.flVersion}`);
|
|
921
|
+
}
|
|
922
|
+
if (parsed.flVersion !== "0.0.0" && compareVersion(parsed.flVersion, [3, 4]) < 0 && !Number.isInteger(bpm)) {
|
|
923
|
+
throw new TypeError(`FL Studio ${parsed.flVersion} does not support decimal BPM values`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function writeTextProjectEvent(parsed, eventId, value) {
|
|
927
|
+
if (value === void 0) return parsed;
|
|
928
|
+
const payload = createTextPayload(value ?? "", parsed.useUnicode);
|
|
929
|
+
const existing = findFirstEvent(parsed, eventId);
|
|
930
|
+
return existing ? replaceEventPayload(parsed, existing, payload) : insertProjectEvent(parsed, createEvent(eventId, payload));
|
|
931
|
+
}
|
|
932
|
+
function writeProjectBpm(parsed, bpm) {
|
|
933
|
+
if (bpm === void 0 || bpm === null) return parsed;
|
|
934
|
+
validateBpm(parsed, bpm);
|
|
935
|
+
const scaledBpm = Math.round(bpm * 1e3);
|
|
936
|
+
const coarseBpm = Math.floor(scaledBpm / 1e3);
|
|
937
|
+
const fineBpm = scaledBpm % 1e3;
|
|
938
|
+
const modernEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
|
|
939
|
+
const coarseEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_COARSE);
|
|
940
|
+
const fineEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_FINE);
|
|
941
|
+
let result = parsed;
|
|
942
|
+
if (modernEvent) {
|
|
943
|
+
result = replaceEventPayload(result, modernEvent, createNumberPayload(scaledBpm, "u32"));
|
|
944
|
+
}
|
|
945
|
+
if (coarseEvent) {
|
|
946
|
+
result = replaceEventPayload(result, coarseEvent, createNumberPayload(coarseBpm, "u16"));
|
|
947
|
+
}
|
|
948
|
+
if (fineEvent) {
|
|
949
|
+
result = replaceEventPayload(result, fineEvent, createNumberPayload(fineBpm, "u16"));
|
|
950
|
+
}
|
|
951
|
+
if (!modernEvent && !coarseEvent && !fineEvent) {
|
|
952
|
+
result = insertProjectEvent(
|
|
953
|
+
result,
|
|
954
|
+
createEvent(EVENT_ID.PROJECT_TEMPO, createNumberPayload(scaledBpm, "u32"))
|
|
955
|
+
);
|
|
956
|
+
} else if (!modernEvent && coarseEvent && !fineEvent && fineBpm > 0) {
|
|
957
|
+
const updatedCoarse = findFirstEvent(result, EVENT_ID.PROJECT__TEMPO_COARSE);
|
|
958
|
+
const index = result.events.indexOf(updatedCoarse) + 1;
|
|
959
|
+
const events = [...result.events];
|
|
960
|
+
events.splice(
|
|
961
|
+
index,
|
|
962
|
+
0,
|
|
963
|
+
createEvent(EVENT_ID.PROJECT__TEMPO_FINE, createNumberPayload(fineBpm, "u16"))
|
|
964
|
+
);
|
|
965
|
+
result = { ...result, events };
|
|
966
|
+
} else if (!modernEvent && !coarseEvent && fineEvent) {
|
|
967
|
+
const updatedFine = findFirstEvent(result, EVENT_ID.PROJECT__TEMPO_FINE);
|
|
968
|
+
const index = result.events.indexOf(updatedFine);
|
|
969
|
+
const events = [...result.events];
|
|
970
|
+
events.splice(
|
|
971
|
+
index,
|
|
972
|
+
0,
|
|
973
|
+
createEvent(EVENT_ID.PROJECT__TEMPO_COARSE, createNumberPayload(coarseBpm, "u16"))
|
|
974
|
+
);
|
|
975
|
+
result = { ...result, events };
|
|
976
|
+
}
|
|
977
|
+
return result;
|
|
978
|
+
}
|
|
937
979
|
function writeProjectMeta(parsed, meta) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
if (meta.description !== void 0 && event.id === EVENT_ID.PROJECT_COMMENTS) {
|
|
946
|
-
return {
|
|
947
|
-
...event,
|
|
948
|
-
payload: createTextPayload(meta.description ?? "", parsed.useUnicode)
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
if (meta.artist !== void 0 && event.id === EVENT_ID.PROJECT_ARTISTS) {
|
|
952
|
-
return {
|
|
953
|
-
...event,
|
|
954
|
-
payload: createTextPayload(meta.artist ?? "", parsed.useUnicode)
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
if (meta.genre !== void 0 && event.id === EVENT_ID.PROJECT_GENRE) {
|
|
958
|
-
return {
|
|
959
|
-
...event,
|
|
960
|
-
payload: createTextPayload(meta.genre ?? "", parsed.useUnicode)
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
if (meta.bpm && Math.abs(meta.bpm) > 0 && event.id === EVENT_ID.PROJECT_TEMPO) {
|
|
964
|
-
return {
|
|
965
|
-
...event,
|
|
966
|
-
payload: createNumberPayload(Math.round(Math.abs(meta.bpm) * 1e3), "u32")
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
return event;
|
|
970
|
-
});
|
|
980
|
+
let result = parsed;
|
|
981
|
+
result = writeProjectBpm(result, meta.bpm);
|
|
982
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_TITLE, meta.name);
|
|
983
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_GENRE, meta.genre);
|
|
984
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_ARTISTS, meta.artist);
|
|
985
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_COMMENTS, meta.description);
|
|
986
|
+
return result;
|
|
971
987
|
}
|
|
972
|
-
var
|
|
988
|
+
var DELPHI_EPOCH_MS = Date.UTC(1899, 11, 30);
|
|
973
989
|
function delphiToDate(days) {
|
|
974
990
|
const ms = days * 24 * 60 * 60 * 1e3;
|
|
975
|
-
return new Date(
|
|
991
|
+
return new Date(DELPHI_EPOCH_MS + ms);
|
|
976
992
|
}
|
|
977
993
|
function dateToDelphiDays(date) {
|
|
978
|
-
const ms = date.getTime() -
|
|
994
|
+
const ms = date.getTime() - DELPHI_EPOCH_MS;
|
|
979
995
|
return ms / (24 * 60 * 60 * 1e3);
|
|
980
996
|
}
|
|
981
997
|
function readProjectTimeInfo(parsed) {
|
|
@@ -996,41 +1012,33 @@ function readProjectTimeInfo(parsed) {
|
|
|
996
1012
|
};
|
|
997
1013
|
}
|
|
998
1014
|
function writeProjectTimeInfo(parsed, info) {
|
|
1015
|
+
if (info.creationDate === void 0 && info.workTimeSeconds === void 0) {
|
|
1016
|
+
return parsed;
|
|
1017
|
+
}
|
|
999
1018
|
const current = readProjectTimeInfo(parsed);
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
writer.writeF64LE(info.workTimeSeconds / (24 * 60 * 60));
|
|
1014
|
-
} else if (current.workTimeSeconds !== null) {
|
|
1015
|
-
writer.writeF64LE(current.workTimeSeconds / (24 * 60 * 60));
|
|
1016
|
-
} else {
|
|
1017
|
-
writer.writeF64LE(0);
|
|
1018
|
-
}
|
|
1019
|
-
return {
|
|
1020
|
-
...event,
|
|
1021
|
-
payload: writer.toBuffer()
|
|
1022
|
-
};
|
|
1023
|
-
});
|
|
1019
|
+
if (info.creationDate !== void 0 && info.creationDate !== null && Number.isNaN(info.creationDate.getTime())) {
|
|
1020
|
+
throw new RangeError("creationDate must be a valid Date");
|
|
1021
|
+
}
|
|
1022
|
+
if (info.workTimeSeconds !== void 0 && info.workTimeSeconds !== null && (!Number.isFinite(info.workTimeSeconds) || info.workTimeSeconds < 0)) {
|
|
1023
|
+
throw new RangeError("workTimeSeconds must be a finite, non-negative number");
|
|
1024
|
+
}
|
|
1025
|
+
const creationDateDays = info.creationDate === null ? 0 : info.creationDate !== void 0 ? dateToDelphiDays(info.creationDate) : current.creationDate !== null ? dateToDelphiDays(current.creationDate) : 0;
|
|
1026
|
+
const workTimeDays = info.workTimeSeconds === null ? 0 : info.workTimeSeconds !== void 0 ? info.workTimeSeconds / (24 * 60 * 60) : current.workTimeSeconds !== null ? current.workTimeSeconds / (24 * 60 * 60) : 0;
|
|
1027
|
+
const writer = new BinaryWriter();
|
|
1028
|
+
writer.writeF64LE(creationDateDays);
|
|
1029
|
+
writer.writeF64LE(workTimeDays);
|
|
1030
|
+
const timestampEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TIMESTAMP);
|
|
1031
|
+
return timestampEvent ? replaceEventPayload(parsed, timestampEvent, writer.toBuffer()) : insertProjectEvent(parsed, createEvent(EVENT_ID.PROJECT_TIMESTAMP, writer.toBuffer()));
|
|
1024
1032
|
}
|
|
1025
1033
|
function listSamples(parsed) {
|
|
1026
1034
|
const samples = [];
|
|
1027
1035
|
parsed.events.forEach((event, index) => {
|
|
1028
1036
|
if (event.id === EVENT_ID.CHANNEL_SAMPLE_PATH) {
|
|
1029
|
-
const
|
|
1030
|
-
if (
|
|
1037
|
+
const path2 = getEventString(event, parsed.useUnicode);
|
|
1038
|
+
if (path2 && path2.length > 0) {
|
|
1031
1039
|
samples.push({
|
|
1032
1040
|
eventIndex: index,
|
|
1033
|
-
path
|
|
1041
|
+
path: path2
|
|
1034
1042
|
});
|
|
1035
1043
|
}
|
|
1036
1044
|
}
|
|
@@ -1158,6 +1166,147 @@ function getPPQ(parsed) {
|
|
|
1158
1166
|
reader.seek(12);
|
|
1159
1167
|
return reader.readU16LE();
|
|
1160
1168
|
}
|
|
1169
|
+
|
|
1170
|
+
// src/samples/inspectReferences.ts
|
|
1171
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1172
|
+
var fs = __toESM(require("fs/promises"), 1);
|
|
1173
|
+
function pathType(p) {
|
|
1174
|
+
if (!p || Array.from(p).some((c) => c.charCodeAt(0) < 32)) return "unknown";
|
|
1175
|
+
if (/^[A-Za-z]:/.test(p) || p.startsWith("\\")) return "windows";
|
|
1176
|
+
if (p.startsWith("/")) return "posix";
|
|
1177
|
+
if (/^[A-Za-z][\w+.-]*:/.test(p) || /[%$]/.test(p)) return "daw";
|
|
1178
|
+
return "relative";
|
|
1179
|
+
}
|
|
1180
|
+
function nativeAbsolute(p) {
|
|
1181
|
+
const kind = pathType(p);
|
|
1182
|
+
return process.platform === "win32" ? kind === "windows" && import_node_path.default.win32.isAbsolute(p) && !/^[\\/][^\\/]/.test(p) : kind === "posix";
|
|
1183
|
+
}
|
|
1184
|
+
function localRelative(p) {
|
|
1185
|
+
return pathType(p) === "relative";
|
|
1186
|
+
}
|
|
1187
|
+
function abort(signal) {
|
|
1188
|
+
if (signal?.aborted) {
|
|
1189
|
+
const e = new Error("Sample inspection aborted");
|
|
1190
|
+
e.name = "AbortError";
|
|
1191
|
+
throw e;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
async function inspectReferences(refs, options = {}) {
|
|
1195
|
+
const mode = options.mode ?? "resolve";
|
|
1196
|
+
if (mode !== "resolve" && mode !== "check") throw new TypeError("Unknown sample inspection mode");
|
|
1197
|
+
const concurrency = options.concurrency ?? 8;
|
|
1198
|
+
const maxEntries = options.maxSearchEntries ?? 1e5;
|
|
1199
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 64) throw new RangeError("Concurrency must be between 1 and 64");
|
|
1200
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) throw new RangeError("Search limit must be a positive safe integer");
|
|
1201
|
+
for (const p of [options.projectPath, options.projectRoot, ...Object.values(options.relativeRoots ?? {}), ...options.searchRoots ?? []]) {
|
|
1202
|
+
if (p !== void 0 && !nativeAbsolute(p)) throw new TypeError("Inspection context paths must be native absolute paths");
|
|
1203
|
+
}
|
|
1204
|
+
abort(options.signal);
|
|
1205
|
+
const results = refs.map((ref) => {
|
|
1206
|
+
const kind = pathType(ref.path);
|
|
1207
|
+
const candidates = [];
|
|
1208
|
+
const add = (p, source) => {
|
|
1209
|
+
const normalized = import_node_path.default.normalize(p);
|
|
1210
|
+
if (!candidates.some((c) => c.path === normalized)) candidates.push({ path: normalized, source, confidence: "exact" });
|
|
1211
|
+
};
|
|
1212
|
+
if (ref.supported !== false) {
|
|
1213
|
+
if (nativeAbsolute(ref.path)) add(ref.path, "declared");
|
|
1214
|
+
const base = ref.relativeBase === "project-file" ? options.projectRoot ?? (options.projectPath ? import_node_path.default.dirname(options.projectPath) : void 0) : options.relativeRoots?.[ref.relativeType ?? ""] ?? (ref.relativeType === "3" ? options.projectRoot : void 0);
|
|
1215
|
+
const relative = ref.relativePath || (kind === "relative" ? ref.path : null);
|
|
1216
|
+
if (base && relative && localRelative(relative)) add(import_node_path.default.resolve(base, relative.replace(/\\/g, import_node_path.default.sep)), "relative");
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
referenceIndex: ref.referenceIndex,
|
|
1220
|
+
recordedPath: ref.path,
|
|
1221
|
+
pathType: ref.supported === false ? "unknown" : kind,
|
|
1222
|
+
relativePath: ref.relativePath ?? (kind === "relative" ? ref.path : null),
|
|
1223
|
+
candidates,
|
|
1224
|
+
resolvedPath: null,
|
|
1225
|
+
status: candidates.length ? "unchecked" : "unresolved",
|
|
1226
|
+
reason: candidates.length ? null : "No interpretable path in the supplied context"
|
|
1227
|
+
};
|
|
1228
|
+
});
|
|
1229
|
+
if (mode === "resolve") return results;
|
|
1230
|
+
const checks = /* @__PURE__ */ new Map();
|
|
1231
|
+
const check = (p) => {
|
|
1232
|
+
let pending = checks.get(p);
|
|
1233
|
+
if (!pending) {
|
|
1234
|
+
pending = fs.stat(p).then((s) => s.isFile() ? "file" : "absent", (e) => e.code === "ENOENT" || e.code === "ENOTDIR" ? "absent" : "inaccessible");
|
|
1235
|
+
checks.set(p, pending);
|
|
1236
|
+
}
|
|
1237
|
+
return pending;
|
|
1238
|
+
};
|
|
1239
|
+
let index = 0;
|
|
1240
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, results.length) }, async () => {
|
|
1241
|
+
while (index < results.length) {
|
|
1242
|
+
abort(options.signal);
|
|
1243
|
+
const result = results[index++];
|
|
1244
|
+
const found = [];
|
|
1245
|
+
let inaccessible = false;
|
|
1246
|
+
for (const candidate of result.candidates) {
|
|
1247
|
+
abort(options.signal);
|
|
1248
|
+
const status = await check(candidate.path);
|
|
1249
|
+
if (status === "file") found.push(candidate.path);
|
|
1250
|
+
if (status === "inaccessible") inaccessible = true;
|
|
1251
|
+
}
|
|
1252
|
+
result.status = found.length > 1 ? "ambiguous" : found.length === 1 ? "found" : inaccessible ? "inaccessible" : result.candidates.length ? "missing" : "unresolved";
|
|
1253
|
+
result.resolvedPath = result.status === "found" ? found[0] : null;
|
|
1254
|
+
if (result.status === "inaccessible") result.reason = "A candidate could not be checked";
|
|
1255
|
+
}
|
|
1256
|
+
}));
|
|
1257
|
+
abort(options.signal);
|
|
1258
|
+
const unresolved = results.filter((r) => r.status !== "found");
|
|
1259
|
+
if (unresolved.length && options.searchRoots?.length) {
|
|
1260
|
+
const names = /* @__PURE__ */ new Map();
|
|
1261
|
+
const needed = new Set(unresolved.map((r) => import_node_path.default.posix.basename(r.recordedPath.replace(/\\/g, "/")).normalize("NFC").toLowerCase()));
|
|
1262
|
+
const queue = [...new Set(options.searchRoots.map((r) => import_node_path.default.normalize(r)))];
|
|
1263
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1264
|
+
let entries = 0;
|
|
1265
|
+
let incomplete = false;
|
|
1266
|
+
while (queue.length) {
|
|
1267
|
+
abort(options.signal);
|
|
1268
|
+
const directory = queue.pop();
|
|
1269
|
+
if (visited.has(directory)) continue;
|
|
1270
|
+
visited.add(directory);
|
|
1271
|
+
try {
|
|
1272
|
+
const handle = await fs.opendir(directory);
|
|
1273
|
+
for await (const entry of handle) {
|
|
1274
|
+
abort(options.signal);
|
|
1275
|
+
if (++entries > maxEntries) throw new RangeError("Sample search entry limit exceeded");
|
|
1276
|
+
const p = import_node_path.default.join(directory, entry.name);
|
|
1277
|
+
if (entry.isDirectory()) queue.push(p);
|
|
1278
|
+
else if (entry.isFile()) {
|
|
1279
|
+
const name = entry.name.normalize("NFC").toLowerCase();
|
|
1280
|
+
if (needed.has(name)) names.set(name, [...names.get(name) ?? [], p]);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
} catch (e) {
|
|
1284
|
+
if (e instanceof RangeError || e instanceof Error && e.name === "AbortError") throw e;
|
|
1285
|
+
incomplete = true;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
for (const result of unresolved) {
|
|
1289
|
+
const name = import_node_path.default.posix.basename(result.recordedPath.replace(/\\/g, "/")).normalize("NFC").toLowerCase();
|
|
1290
|
+
const matches = names.get(name) ?? [];
|
|
1291
|
+
for (const p of matches) if (!result.candidates.some((c) => c.path === p)) result.candidates.push({ path: p, source: "search", confidence: "name-only" });
|
|
1292
|
+
if (result.status === "ambiguous") continue;
|
|
1293
|
+
if (matches.length) {
|
|
1294
|
+
result.status = matches.length > 1 ? "ambiguous" : "unresolved";
|
|
1295
|
+
result.reason = "Name-only candidates require caller confirmation";
|
|
1296
|
+
} else if (incomplete && result.status !== "inaccessible") {
|
|
1297
|
+
result.status = "inaccessible";
|
|
1298
|
+
result.reason = "Search could not inspect every requested directory";
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
abort(options.signal);
|
|
1303
|
+
return results;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/api/inspectSamples.ts
|
|
1307
|
+
function inspectSamples(parsed, options = {}) {
|
|
1308
|
+
return inspectReferences(listSamples(parsed).map((s) => ({ referenceIndex: s.eventIndex, path: s.path, relativeBase: "project-file" })), options);
|
|
1309
|
+
}
|
|
1161
1310
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1162
1311
|
0 && (module.exports = {
|
|
1163
1312
|
BinaryReader,
|
|
@@ -1176,6 +1325,7 @@ function getPPQ(parsed) {
|
|
|
1176
1325
|
getEventString,
|
|
1177
1326
|
getFlVersion,
|
|
1178
1327
|
getPPQ,
|
|
1328
|
+
inspectSamples,
|
|
1179
1329
|
listPlugins,
|
|
1180
1330
|
listSamples,
|
|
1181
1331
|
parseFlp,
|