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/dist/index.js
CHANGED
|
@@ -3,6 +3,9 @@ var WORD = 64;
|
|
|
3
3
|
var DWORD = 128;
|
|
4
4
|
var TEXT = 192;
|
|
5
5
|
var DATA = 208;
|
|
6
|
+
var FIXED_SIZE_OVERRIDES = {
|
|
7
|
+
172: 3
|
|
8
|
+
};
|
|
6
9
|
var NEW_TEXT_IDS = [
|
|
7
10
|
TEXT + 49,
|
|
8
11
|
// ArrangementID.Name
|
|
@@ -149,6 +152,7 @@ var EVENT_ID = {
|
|
|
149
152
|
// 236
|
|
150
153
|
};
|
|
151
154
|
var EVENT_KIND = {
|
|
155
|
+
[172]: "unknown",
|
|
152
156
|
// Project events
|
|
153
157
|
[EVENT_ID.PROJECT_LOOP_ACTIVE]: "u8",
|
|
154
158
|
[EVENT_ID.PROJECT_SHOW_INFO]: "u8",
|
|
@@ -257,6 +261,10 @@ function getEventKind(eventId) {
|
|
|
257
261
|
}
|
|
258
262
|
}
|
|
259
263
|
function getEventFixedSize(eventId) {
|
|
264
|
+
const override = FIXED_SIZE_OVERRIDES[eventId];
|
|
265
|
+
if (override !== void 0) {
|
|
266
|
+
return override;
|
|
267
|
+
}
|
|
260
268
|
if (eventId < WORD) {
|
|
261
269
|
return 1;
|
|
262
270
|
} else if (eventId < DWORD) {
|
|
@@ -533,62 +541,9 @@ function varIntSize(value) {
|
|
|
533
541
|
}
|
|
534
542
|
|
|
535
543
|
// src/parser/FlpParser.ts
|
|
536
|
-
var KNOWN_DWORD_IDS = new Set(
|
|
537
|
-
Object.keys(EVENT_KIND).map(Number).filter((id) => id >= DWORD && id < TEXT)
|
|
538
|
-
);
|
|
539
544
|
var FLP_HEADER_MAGIC = "FLhd";
|
|
540
545
|
var FLP_DATA_MAGIC = "FLdt";
|
|
541
546
|
var FLP_HEADER_SIZE = 6;
|
|
542
|
-
var LOOKAHEAD_BYTES = 200;
|
|
543
|
-
var LOOKAHEAD_VARINT_MAX = 1e5;
|
|
544
|
-
function readVarIntRaw(buf, off, end) {
|
|
545
|
-
let shift = 0;
|
|
546
|
-
let size = 0;
|
|
547
|
-
const start = off;
|
|
548
|
-
while (true) {
|
|
549
|
-
if (off >= end) return { size: -1, bytesRead: 0 };
|
|
550
|
-
const byte = buf[off];
|
|
551
|
-
off++;
|
|
552
|
-
size |= (byte & 127) << shift;
|
|
553
|
-
shift += 7;
|
|
554
|
-
if (!(byte & 128)) break;
|
|
555
|
-
}
|
|
556
|
-
return { size, bytesRead: off - start };
|
|
557
|
-
}
|
|
558
|
-
function scoreAlignment(buf, off, end, bytesToCheck) {
|
|
559
|
-
const limit = Math.min(off + bytesToCheck, end);
|
|
560
|
-
let textDataCount = 0;
|
|
561
|
-
let consecutiveSmallByte = 0;
|
|
562
|
-
let maxConsecutiveSmallByte = 0;
|
|
563
|
-
while (off < limit) {
|
|
564
|
-
const id = buf[off];
|
|
565
|
-
if (id < WORD) {
|
|
566
|
-
off += 2;
|
|
567
|
-
if (id < 32 && !KNOWN_DWORD_IDS.has(id)) {
|
|
568
|
-
consecutiveSmallByte++;
|
|
569
|
-
maxConsecutiveSmallByte = Math.max(maxConsecutiveSmallByte, consecutiveSmallByte);
|
|
570
|
-
} else {
|
|
571
|
-
consecutiveSmallByte = 0;
|
|
572
|
-
}
|
|
573
|
-
} else if (id < DWORD) {
|
|
574
|
-
off += 3;
|
|
575
|
-
consecutiveSmallByte = 0;
|
|
576
|
-
} else if (id < TEXT) {
|
|
577
|
-
off += 5;
|
|
578
|
-
consecutiveSmallByte = 0;
|
|
579
|
-
} else {
|
|
580
|
-
const vi = readVarIntRaw(buf, off + 1, end);
|
|
581
|
-
if (vi.size >= 0 && vi.size < LOOKAHEAD_VARINT_MAX && off + 1 + vi.bytesRead + vi.size <= end) {
|
|
582
|
-
textDataCount++;
|
|
583
|
-
off = off + 1 + vi.bytesRead + vi.size;
|
|
584
|
-
} else {
|
|
585
|
-
return -100;
|
|
586
|
-
}
|
|
587
|
-
consecutiveSmallByte = 0;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
return textDataCount * 10 - maxConsecutiveSmallByte * 3;
|
|
591
|
-
}
|
|
592
547
|
function parseFlp(buffer) {
|
|
593
548
|
const reader = new BinaryReader(buffer);
|
|
594
549
|
const headerMagic = reader.readBytes(4).toString("ascii");
|
|
@@ -600,8 +555,8 @@ function parseFlp(buffer) {
|
|
|
600
555
|
throw new Error(`Invalid header size: expected ${FLP_HEADER_SIZE}, got ${headerSize}`);
|
|
601
556
|
}
|
|
602
557
|
const format = reader.readI16LE();
|
|
603
|
-
|
|
604
|
-
|
|
558
|
+
reader.readU16LE();
|
|
559
|
+
reader.readU16LE();
|
|
605
560
|
if (format < -1 || format > 80) {
|
|
606
561
|
throw new Error(`Invalid file format: ${format}`);
|
|
607
562
|
}
|
|
@@ -628,39 +583,10 @@ function parseFlp(buffer) {
|
|
|
628
583
|
const eventId = reader.readU8();
|
|
629
584
|
let payloadSize;
|
|
630
585
|
let headerSize2;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
} else if (eventId < DWORD) {
|
|
635
|
-
payloadSize = 2;
|
|
586
|
+
const fixedSize = getEventFixedSize(eventId);
|
|
587
|
+
if (fixedSize >= 0) {
|
|
588
|
+
payloadSize = fixedSize;
|
|
636
589
|
headerSize2 = 1;
|
|
637
|
-
} else if (eventId < TEXT) {
|
|
638
|
-
if (KNOWN_DWORD_IDS.has(eventId)) {
|
|
639
|
-
payloadSize = 4;
|
|
640
|
-
headerSize2 = 1;
|
|
641
|
-
} else {
|
|
642
|
-
const afterDword = eventStart + 1 + 4;
|
|
643
|
-
const vi = readVarIntRaw(buffer, eventStart + 1, eventsEnd);
|
|
644
|
-
if (vi.size < 0 || vi.size > LOOKAHEAD_VARINT_MAX || eventStart + 1 + vi.bytesRead + vi.size > eventsEnd) {
|
|
645
|
-
payloadSize = 4;
|
|
646
|
-
headerSize2 = 1;
|
|
647
|
-
} else if (vi.size === 3) {
|
|
648
|
-
payloadSize = 4;
|
|
649
|
-
headerSize2 = 1;
|
|
650
|
-
} else {
|
|
651
|
-
const afterVarInt = eventStart + 1 + vi.bytesRead + vi.size;
|
|
652
|
-
const dwordScore = scoreAlignment(buffer, afterDword, eventsEnd, LOOKAHEAD_BYTES);
|
|
653
|
-
const varIntScore = scoreAlignment(buffer, afterVarInt, eventsEnd, LOOKAHEAD_BYTES);
|
|
654
|
-
if (varIntScore > dwordScore + 2) {
|
|
655
|
-
payloadSize = vi.size;
|
|
656
|
-
reader.seek(eventStart + 1 + vi.bytesRead);
|
|
657
|
-
headerSize2 = 1 + vi.bytesRead;
|
|
658
|
-
} else {
|
|
659
|
-
payloadSize = 4;
|
|
660
|
-
headerSize2 = 1;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
590
|
} else {
|
|
665
591
|
const sizeStart = reader.tell();
|
|
666
592
|
payloadSize = reader.readVarInt();
|
|
@@ -843,6 +769,46 @@ function createNumberPayload(value, kind) {
|
|
|
843
769
|
}
|
|
844
770
|
|
|
845
771
|
// src/api/ProjectApi.ts
|
|
772
|
+
var PROJECT_EVENT_ORDER = [
|
|
773
|
+
EVENT_ID.PROJECT_TEMPO,
|
|
774
|
+
EVENT_ID.PROJECT_TITLE,
|
|
775
|
+
EVENT_ID.PROJECT_GENRE,
|
|
776
|
+
EVENT_ID.PROJECT_ARTISTS,
|
|
777
|
+
EVENT_ID.PROJECT_COMMENTS,
|
|
778
|
+
EVENT_ID.PROJECT_TIMESTAMP
|
|
779
|
+
];
|
|
780
|
+
function insertProjectEvent(parsed, event) {
|
|
781
|
+
const orderIndex = PROJECT_EVENT_ORDER.indexOf(event.id);
|
|
782
|
+
let insertionIndex = -1;
|
|
783
|
+
if (orderIndex >= 0) {
|
|
784
|
+
const laterIds = new Set(PROJECT_EVENT_ORDER.slice(orderIndex + 1));
|
|
785
|
+
insertionIndex = parsed.events.findIndex((candidate) => laterIds.has(candidate.id));
|
|
786
|
+
if (insertionIndex < 0) {
|
|
787
|
+
const previousIds = new Set(PROJECT_EVENT_ORDER.slice(0, orderIndex + 1));
|
|
788
|
+
for (let index = parsed.events.length - 1; index >= 0; index--) {
|
|
789
|
+
if (previousIds.has(parsed.events[index].id)) {
|
|
790
|
+
insertionIndex = index + 1;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (insertionIndex < 0) {
|
|
797
|
+
insertionIndex = parsed.events.findIndex((candidate) => candidate.id === EVENT_ID.CHANNEL_NEW);
|
|
798
|
+
}
|
|
799
|
+
if (insertionIndex < 0) {
|
|
800
|
+
const versionIndex = parsed.events.findIndex(
|
|
801
|
+
(candidate) => candidate.id === EVENT_ID.PROJECT_FL_VERSION
|
|
802
|
+
);
|
|
803
|
+
insertionIndex = versionIndex >= 0 ? versionIndex + 1 : parsed.events.length;
|
|
804
|
+
}
|
|
805
|
+
const events = [...parsed.events];
|
|
806
|
+
events.splice(insertionIndex, 0, event);
|
|
807
|
+
return { ...parsed, events };
|
|
808
|
+
}
|
|
809
|
+
function replaceEventPayload(parsed, target, payload) {
|
|
810
|
+
return patchEvents(parsed, (event) => event === target ? { ...event, payload } : event);
|
|
811
|
+
}
|
|
846
812
|
function readBpm(parsed) {
|
|
847
813
|
const tempoEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
|
|
848
814
|
if (tempoEvent) {
|
|
@@ -872,48 +838,97 @@ function readProjectMeta(parsed) {
|
|
|
872
838
|
bpm: readBpm(parsed)
|
|
873
839
|
};
|
|
874
840
|
}
|
|
841
|
+
function compareVersion(version, expected) {
|
|
842
|
+
const actual = version.split(".").map((part) => Number.parseInt(part, 10));
|
|
843
|
+
const length = Math.max(actual.length, expected.length);
|
|
844
|
+
for (let index = 0; index < length; index++) {
|
|
845
|
+
const difference = (actual[index] ?? 0) - (expected[index] ?? 0);
|
|
846
|
+
if (difference !== 0) return Math.sign(difference);
|
|
847
|
+
}
|
|
848
|
+
return 0;
|
|
849
|
+
}
|
|
850
|
+
function validateBpm(parsed, bpm) {
|
|
851
|
+
if (!Number.isFinite(bpm)) {
|
|
852
|
+
throw new RangeError("BPM must be a finite number");
|
|
853
|
+
}
|
|
854
|
+
const legacyHighTempo = compareVersion(parsed.flVersion, [1, 4, 2]) >= 0 && compareVersion(parsed.flVersion, [11]) < 0;
|
|
855
|
+
const maximum = legacyHighTempo ? 999 : 522;
|
|
856
|
+
if (bpm < 10 || bpm > maximum) {
|
|
857
|
+
throw new RangeError(`BPM must be between 10 and ${maximum} for FL Studio ${parsed.flVersion}`);
|
|
858
|
+
}
|
|
859
|
+
if (parsed.flVersion !== "0.0.0" && compareVersion(parsed.flVersion, [3, 4]) < 0 && !Number.isInteger(bpm)) {
|
|
860
|
+
throw new TypeError(`FL Studio ${parsed.flVersion} does not support decimal BPM values`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function writeTextProjectEvent(parsed, eventId, value) {
|
|
864
|
+
if (value === void 0) return parsed;
|
|
865
|
+
const payload = createTextPayload(value ?? "", parsed.useUnicode);
|
|
866
|
+
const existing = findFirstEvent(parsed, eventId);
|
|
867
|
+
return existing ? replaceEventPayload(parsed, existing, payload) : insertProjectEvent(parsed, createEvent(eventId, payload));
|
|
868
|
+
}
|
|
869
|
+
function writeProjectBpm(parsed, bpm) {
|
|
870
|
+
if (bpm === void 0 || bpm === null) return parsed;
|
|
871
|
+
validateBpm(parsed, bpm);
|
|
872
|
+
const scaledBpm = Math.round(bpm * 1e3);
|
|
873
|
+
const coarseBpm = Math.floor(scaledBpm / 1e3);
|
|
874
|
+
const fineBpm = scaledBpm % 1e3;
|
|
875
|
+
const modernEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
|
|
876
|
+
const coarseEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_COARSE);
|
|
877
|
+
const fineEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_FINE);
|
|
878
|
+
let result = parsed;
|
|
879
|
+
if (modernEvent) {
|
|
880
|
+
result = replaceEventPayload(result, modernEvent, createNumberPayload(scaledBpm, "u32"));
|
|
881
|
+
}
|
|
882
|
+
if (coarseEvent) {
|
|
883
|
+
result = replaceEventPayload(result, coarseEvent, createNumberPayload(coarseBpm, "u16"));
|
|
884
|
+
}
|
|
885
|
+
if (fineEvent) {
|
|
886
|
+
result = replaceEventPayload(result, fineEvent, createNumberPayload(fineBpm, "u16"));
|
|
887
|
+
}
|
|
888
|
+
if (!modernEvent && !coarseEvent && !fineEvent) {
|
|
889
|
+
result = insertProjectEvent(
|
|
890
|
+
result,
|
|
891
|
+
createEvent(EVENT_ID.PROJECT_TEMPO, createNumberPayload(scaledBpm, "u32"))
|
|
892
|
+
);
|
|
893
|
+
} else if (!modernEvent && coarseEvent && !fineEvent && fineBpm > 0) {
|
|
894
|
+
const updatedCoarse = findFirstEvent(result, EVENT_ID.PROJECT__TEMPO_COARSE);
|
|
895
|
+
const index = result.events.indexOf(updatedCoarse) + 1;
|
|
896
|
+
const events = [...result.events];
|
|
897
|
+
events.splice(
|
|
898
|
+
index,
|
|
899
|
+
0,
|
|
900
|
+
createEvent(EVENT_ID.PROJECT__TEMPO_FINE, createNumberPayload(fineBpm, "u16"))
|
|
901
|
+
);
|
|
902
|
+
result = { ...result, events };
|
|
903
|
+
} else if (!modernEvent && !coarseEvent && fineEvent) {
|
|
904
|
+
const updatedFine = findFirstEvent(result, EVENT_ID.PROJECT__TEMPO_FINE);
|
|
905
|
+
const index = result.events.indexOf(updatedFine);
|
|
906
|
+
const events = [...result.events];
|
|
907
|
+
events.splice(
|
|
908
|
+
index,
|
|
909
|
+
0,
|
|
910
|
+
createEvent(EVENT_ID.PROJECT__TEMPO_COARSE, createNumberPayload(coarseBpm, "u16"))
|
|
911
|
+
);
|
|
912
|
+
result = { ...result, events };
|
|
913
|
+
}
|
|
914
|
+
return result;
|
|
915
|
+
}
|
|
875
916
|
function writeProjectMeta(parsed, meta) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (meta.description !== void 0 && event.id === EVENT_ID.PROJECT_COMMENTS) {
|
|
884
|
-
return {
|
|
885
|
-
...event,
|
|
886
|
-
payload: createTextPayload(meta.description ?? "", parsed.useUnicode)
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
if (meta.artist !== void 0 && event.id === EVENT_ID.PROJECT_ARTISTS) {
|
|
890
|
-
return {
|
|
891
|
-
...event,
|
|
892
|
-
payload: createTextPayload(meta.artist ?? "", parsed.useUnicode)
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
if (meta.genre !== void 0 && event.id === EVENT_ID.PROJECT_GENRE) {
|
|
896
|
-
return {
|
|
897
|
-
...event,
|
|
898
|
-
payload: createTextPayload(meta.genre ?? "", parsed.useUnicode)
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
if (meta.bpm && Math.abs(meta.bpm) > 0 && event.id === EVENT_ID.PROJECT_TEMPO) {
|
|
902
|
-
return {
|
|
903
|
-
...event,
|
|
904
|
-
payload: createNumberPayload(Math.round(Math.abs(meta.bpm) * 1e3), "u32")
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
return event;
|
|
908
|
-
});
|
|
917
|
+
let result = parsed;
|
|
918
|
+
result = writeProjectBpm(result, meta.bpm);
|
|
919
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_TITLE, meta.name);
|
|
920
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_GENRE, meta.genre);
|
|
921
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_ARTISTS, meta.artist);
|
|
922
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_COMMENTS, meta.description);
|
|
923
|
+
return result;
|
|
909
924
|
}
|
|
910
|
-
var
|
|
925
|
+
var DELPHI_EPOCH_MS = Date.UTC(1899, 11, 30);
|
|
911
926
|
function delphiToDate(days) {
|
|
912
927
|
const ms = days * 24 * 60 * 60 * 1e3;
|
|
913
|
-
return new Date(
|
|
928
|
+
return new Date(DELPHI_EPOCH_MS + ms);
|
|
914
929
|
}
|
|
915
930
|
function dateToDelphiDays(date) {
|
|
916
|
-
const ms = date.getTime() -
|
|
931
|
+
const ms = date.getTime() - DELPHI_EPOCH_MS;
|
|
917
932
|
return ms / (24 * 60 * 60 * 1e3);
|
|
918
933
|
}
|
|
919
934
|
function readProjectTimeInfo(parsed) {
|
|
@@ -934,41 +949,33 @@ function readProjectTimeInfo(parsed) {
|
|
|
934
949
|
};
|
|
935
950
|
}
|
|
936
951
|
function writeProjectTimeInfo(parsed, info) {
|
|
952
|
+
if (info.creationDate === void 0 && info.workTimeSeconds === void 0) {
|
|
953
|
+
return parsed;
|
|
954
|
+
}
|
|
937
955
|
const current = readProjectTimeInfo(parsed);
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
writer.writeF64LE(info.workTimeSeconds / (24 * 60 * 60));
|
|
952
|
-
} else if (current.workTimeSeconds !== null) {
|
|
953
|
-
writer.writeF64LE(current.workTimeSeconds / (24 * 60 * 60));
|
|
954
|
-
} else {
|
|
955
|
-
writer.writeF64LE(0);
|
|
956
|
-
}
|
|
957
|
-
return {
|
|
958
|
-
...event,
|
|
959
|
-
payload: writer.toBuffer()
|
|
960
|
-
};
|
|
961
|
-
});
|
|
956
|
+
if (info.creationDate !== void 0 && info.creationDate !== null && Number.isNaN(info.creationDate.getTime())) {
|
|
957
|
+
throw new RangeError("creationDate must be a valid Date");
|
|
958
|
+
}
|
|
959
|
+
if (info.workTimeSeconds !== void 0 && info.workTimeSeconds !== null && (!Number.isFinite(info.workTimeSeconds) || info.workTimeSeconds < 0)) {
|
|
960
|
+
throw new RangeError("workTimeSeconds must be a finite, non-negative number");
|
|
961
|
+
}
|
|
962
|
+
const creationDateDays = info.creationDate === null ? 0 : info.creationDate !== void 0 ? dateToDelphiDays(info.creationDate) : current.creationDate !== null ? dateToDelphiDays(current.creationDate) : 0;
|
|
963
|
+
const workTimeDays = info.workTimeSeconds === null ? 0 : info.workTimeSeconds !== void 0 ? info.workTimeSeconds / (24 * 60 * 60) : current.workTimeSeconds !== null ? current.workTimeSeconds / (24 * 60 * 60) : 0;
|
|
964
|
+
const writer = new BinaryWriter();
|
|
965
|
+
writer.writeF64LE(creationDateDays);
|
|
966
|
+
writer.writeF64LE(workTimeDays);
|
|
967
|
+
const timestampEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TIMESTAMP);
|
|
968
|
+
return timestampEvent ? replaceEventPayload(parsed, timestampEvent, writer.toBuffer()) : insertProjectEvent(parsed, createEvent(EVENT_ID.PROJECT_TIMESTAMP, writer.toBuffer()));
|
|
962
969
|
}
|
|
963
970
|
function listSamples(parsed) {
|
|
964
971
|
const samples = [];
|
|
965
972
|
parsed.events.forEach((event, index) => {
|
|
966
973
|
if (event.id === EVENT_ID.CHANNEL_SAMPLE_PATH) {
|
|
967
|
-
const
|
|
968
|
-
if (
|
|
974
|
+
const path2 = getEventString(event, parsed.useUnicode);
|
|
975
|
+
if (path2 && path2.length > 0) {
|
|
969
976
|
samples.push({
|
|
970
977
|
eventIndex: index,
|
|
971
|
-
path
|
|
978
|
+
path: path2
|
|
972
979
|
});
|
|
973
980
|
}
|
|
974
981
|
}
|
|
@@ -1096,6 +1103,147 @@ function getPPQ(parsed) {
|
|
|
1096
1103
|
reader.seek(12);
|
|
1097
1104
|
return reader.readU16LE();
|
|
1098
1105
|
}
|
|
1106
|
+
|
|
1107
|
+
// src/samples/inspectReferences.ts
|
|
1108
|
+
import path from "path";
|
|
1109
|
+
import * as fs from "fs/promises";
|
|
1110
|
+
function pathType(p) {
|
|
1111
|
+
if (!p || Array.from(p).some((c) => c.charCodeAt(0) < 32)) return "unknown";
|
|
1112
|
+
if (/^[A-Za-z]:/.test(p) || p.startsWith("\\")) return "windows";
|
|
1113
|
+
if (p.startsWith("/")) return "posix";
|
|
1114
|
+
if (/^[A-Za-z][\w+.-]*:/.test(p) || /[%$]/.test(p)) return "daw";
|
|
1115
|
+
return "relative";
|
|
1116
|
+
}
|
|
1117
|
+
function nativeAbsolute(p) {
|
|
1118
|
+
const kind = pathType(p);
|
|
1119
|
+
return process.platform === "win32" ? kind === "windows" && path.win32.isAbsolute(p) && !/^[\\/][^\\/]/.test(p) : kind === "posix";
|
|
1120
|
+
}
|
|
1121
|
+
function localRelative(p) {
|
|
1122
|
+
return pathType(p) === "relative";
|
|
1123
|
+
}
|
|
1124
|
+
function abort(signal) {
|
|
1125
|
+
if (signal?.aborted) {
|
|
1126
|
+
const e = new Error("Sample inspection aborted");
|
|
1127
|
+
e.name = "AbortError";
|
|
1128
|
+
throw e;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function inspectReferences(refs, options = {}) {
|
|
1132
|
+
const mode = options.mode ?? "resolve";
|
|
1133
|
+
if (mode !== "resolve" && mode !== "check") throw new TypeError("Unknown sample inspection mode");
|
|
1134
|
+
const concurrency = options.concurrency ?? 8;
|
|
1135
|
+
const maxEntries = options.maxSearchEntries ?? 1e5;
|
|
1136
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 64) throw new RangeError("Concurrency must be between 1 and 64");
|
|
1137
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) throw new RangeError("Search limit must be a positive safe integer");
|
|
1138
|
+
for (const p of [options.projectPath, options.projectRoot, ...Object.values(options.relativeRoots ?? {}), ...options.searchRoots ?? []]) {
|
|
1139
|
+
if (p !== void 0 && !nativeAbsolute(p)) throw new TypeError("Inspection context paths must be native absolute paths");
|
|
1140
|
+
}
|
|
1141
|
+
abort(options.signal);
|
|
1142
|
+
const results = refs.map((ref) => {
|
|
1143
|
+
const kind = pathType(ref.path);
|
|
1144
|
+
const candidates = [];
|
|
1145
|
+
const add = (p, source) => {
|
|
1146
|
+
const normalized = path.normalize(p);
|
|
1147
|
+
if (!candidates.some((c) => c.path === normalized)) candidates.push({ path: normalized, source, confidence: "exact" });
|
|
1148
|
+
};
|
|
1149
|
+
if (ref.supported !== false) {
|
|
1150
|
+
if (nativeAbsolute(ref.path)) add(ref.path, "declared");
|
|
1151
|
+
const base = ref.relativeBase === "project-file" ? options.projectRoot ?? (options.projectPath ? path.dirname(options.projectPath) : void 0) : options.relativeRoots?.[ref.relativeType ?? ""] ?? (ref.relativeType === "3" ? options.projectRoot : void 0);
|
|
1152
|
+
const relative = ref.relativePath || (kind === "relative" ? ref.path : null);
|
|
1153
|
+
if (base && relative && localRelative(relative)) add(path.resolve(base, relative.replace(/\\/g, path.sep)), "relative");
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
referenceIndex: ref.referenceIndex,
|
|
1157
|
+
recordedPath: ref.path,
|
|
1158
|
+
pathType: ref.supported === false ? "unknown" : kind,
|
|
1159
|
+
relativePath: ref.relativePath ?? (kind === "relative" ? ref.path : null),
|
|
1160
|
+
candidates,
|
|
1161
|
+
resolvedPath: null,
|
|
1162
|
+
status: candidates.length ? "unchecked" : "unresolved",
|
|
1163
|
+
reason: candidates.length ? null : "No interpretable path in the supplied context"
|
|
1164
|
+
};
|
|
1165
|
+
});
|
|
1166
|
+
if (mode === "resolve") return results;
|
|
1167
|
+
const checks = /* @__PURE__ */ new Map();
|
|
1168
|
+
const check = (p) => {
|
|
1169
|
+
let pending = checks.get(p);
|
|
1170
|
+
if (!pending) {
|
|
1171
|
+
pending = fs.stat(p).then((s) => s.isFile() ? "file" : "absent", (e) => e.code === "ENOENT" || e.code === "ENOTDIR" ? "absent" : "inaccessible");
|
|
1172
|
+
checks.set(p, pending);
|
|
1173
|
+
}
|
|
1174
|
+
return pending;
|
|
1175
|
+
};
|
|
1176
|
+
let index = 0;
|
|
1177
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, results.length) }, async () => {
|
|
1178
|
+
while (index < results.length) {
|
|
1179
|
+
abort(options.signal);
|
|
1180
|
+
const result = results[index++];
|
|
1181
|
+
const found = [];
|
|
1182
|
+
let inaccessible = false;
|
|
1183
|
+
for (const candidate of result.candidates) {
|
|
1184
|
+
abort(options.signal);
|
|
1185
|
+
const status = await check(candidate.path);
|
|
1186
|
+
if (status === "file") found.push(candidate.path);
|
|
1187
|
+
if (status === "inaccessible") inaccessible = true;
|
|
1188
|
+
}
|
|
1189
|
+
result.status = found.length > 1 ? "ambiguous" : found.length === 1 ? "found" : inaccessible ? "inaccessible" : result.candidates.length ? "missing" : "unresolved";
|
|
1190
|
+
result.resolvedPath = result.status === "found" ? found[0] : null;
|
|
1191
|
+
if (result.status === "inaccessible") result.reason = "A candidate could not be checked";
|
|
1192
|
+
}
|
|
1193
|
+
}));
|
|
1194
|
+
abort(options.signal);
|
|
1195
|
+
const unresolved = results.filter((r) => r.status !== "found");
|
|
1196
|
+
if (unresolved.length && options.searchRoots?.length) {
|
|
1197
|
+
const names = /* @__PURE__ */ new Map();
|
|
1198
|
+
const needed = new Set(unresolved.map((r) => path.posix.basename(r.recordedPath.replace(/\\/g, "/")).normalize("NFC").toLowerCase()));
|
|
1199
|
+
const queue = [...new Set(options.searchRoots.map((r) => path.normalize(r)))];
|
|
1200
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1201
|
+
let entries = 0;
|
|
1202
|
+
let incomplete = false;
|
|
1203
|
+
while (queue.length) {
|
|
1204
|
+
abort(options.signal);
|
|
1205
|
+
const directory = queue.pop();
|
|
1206
|
+
if (visited.has(directory)) continue;
|
|
1207
|
+
visited.add(directory);
|
|
1208
|
+
try {
|
|
1209
|
+
const handle = await fs.opendir(directory);
|
|
1210
|
+
for await (const entry of handle) {
|
|
1211
|
+
abort(options.signal);
|
|
1212
|
+
if (++entries > maxEntries) throw new RangeError("Sample search entry limit exceeded");
|
|
1213
|
+
const p = path.join(directory, entry.name);
|
|
1214
|
+
if (entry.isDirectory()) queue.push(p);
|
|
1215
|
+
else if (entry.isFile()) {
|
|
1216
|
+
const name = entry.name.normalize("NFC").toLowerCase();
|
|
1217
|
+
if (needed.has(name)) names.set(name, [...names.get(name) ?? [], p]);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
if (e instanceof RangeError || e instanceof Error && e.name === "AbortError") throw e;
|
|
1222
|
+
incomplete = true;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
for (const result of unresolved) {
|
|
1226
|
+
const name = path.posix.basename(result.recordedPath.replace(/\\/g, "/")).normalize("NFC").toLowerCase();
|
|
1227
|
+
const matches = names.get(name) ?? [];
|
|
1228
|
+
for (const p of matches) if (!result.candidates.some((c) => c.path === p)) result.candidates.push({ path: p, source: "search", confidence: "name-only" });
|
|
1229
|
+
if (result.status === "ambiguous") continue;
|
|
1230
|
+
if (matches.length) {
|
|
1231
|
+
result.status = matches.length > 1 ? "ambiguous" : "unresolved";
|
|
1232
|
+
result.reason = "Name-only candidates require caller confirmation";
|
|
1233
|
+
} else if (incomplete && result.status !== "inaccessible") {
|
|
1234
|
+
result.status = "inaccessible";
|
|
1235
|
+
result.reason = "Search could not inspect every requested directory";
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
abort(options.signal);
|
|
1240
|
+
return results;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/api/inspectSamples.ts
|
|
1244
|
+
function inspectSamples(parsed, options = {}) {
|
|
1245
|
+
return inspectReferences(listSamples(parsed).map((s) => ({ referenceIndex: s.eventIndex, path: s.path, relativeBase: "project-file" })), options);
|
|
1246
|
+
}
|
|
1099
1247
|
export {
|
|
1100
1248
|
BinaryReader,
|
|
1101
1249
|
BinaryWriter,
|
|
@@ -1113,6 +1261,7 @@ export {
|
|
|
1113
1261
|
getEventString,
|
|
1114
1262
|
getFlVersion,
|
|
1115
1263
|
getPPQ,
|
|
1264
|
+
inspectSamples,
|
|
1116
1265
|
listPlugins,
|
|
1117
1266
|
listSamples,
|
|
1118
1267
|
parseFlp,
|