ts-flp 1.0.4 → 1.0.6

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/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) {
@@ -547,8 +555,8 @@ function parseFlp(buffer) {
547
555
  throw new Error(`Invalid header size: expected ${FLP_HEADER_SIZE}, got ${headerSize}`);
548
556
  }
549
557
  const format = reader.readI16LE();
550
- const channelCount = reader.readU16LE();
551
- const ppq = reader.readU16LE();
558
+ reader.readU16LE();
559
+ reader.readU16LE();
552
560
  if (format < -1 || format > 80) {
553
561
  throw new Error(`Invalid file format: ${format}`);
554
562
  }
@@ -568,7 +576,6 @@ function parseFlp(buffer) {
568
576
  const events = [];
569
577
  let flVersion = "0.0.0";
570
578
  let useUnicode = false;
571
- let detectedVersion = false;
572
579
  const eventsStart = reader.tell();
573
580
  const eventsEnd = eventsStart + eventsSize;
574
581
  while (reader.tell() < eventsEnd) {
@@ -576,14 +583,9 @@ function parseFlp(buffer) {
576
583
  const eventId = reader.readU8();
577
584
  let payloadSize;
578
585
  let headerSize2;
579
- if (eventId < WORD) {
580
- payloadSize = 1;
581
- headerSize2 = 1;
582
- } else if (eventId < DWORD) {
583
- payloadSize = 2;
584
- headerSize2 = 1;
585
- } else if (eventId < TEXT) {
586
- payloadSize = 4;
586
+ const fixedSize = getEventFixedSize(eventId);
587
+ if (fixedSize >= 0) {
588
+ payloadSize = fixedSize;
587
589
  headerSize2 = 1;
588
590
  } else {
589
591
  const sizeStart = reader.tell();
@@ -600,16 +602,14 @@ function parseFlp(buffer) {
600
602
  payload: Buffer.from(payload)
601
603
  };
602
604
  events.push(event);
603
- if (eventId === EVENT_ID.PROJECT_FL_VERSION && !detectedVersion) {
605
+ if (eventId === EVENT_ID.PROJECT_FL_VERSION && flVersion === "0.0.0") {
604
606
  const candidate = payload.toString("ascii").replace(/\0/g, "").trim();
605
- const isSemverLike = /^\d+(\.\d+)+$/.test(candidate);
606
- if (isSemverLike) {
607
+ if (/^\d+(\.\d+)+$/.test(candidate)) {
607
608
  flVersion = candidate;
608
609
  const parts = flVersion.split(".").map((p) => parseInt(p, 10));
609
610
  const major = parts[0] ?? 0;
610
611
  const minor = parts[1] ?? 0;
611
612
  useUnicode = parts.length >= 2 && (major > 11 || major === 11 && minor >= 5);
612
- detectedVersion = true;
613
613
  }
614
614
  }
615
615
  }
@@ -676,7 +676,7 @@ function createEvent(id, payload) {
676
676
  function patchEvents(parsed, patcher) {
677
677
  const patchedEvents = parsed.events.map((event, index) => {
678
678
  const patched = patcher(event, index);
679
- if (patched.payload !== event.payload && patched.id >= TEXT) {
679
+ if (patched.payload !== event.payload && event.header.length > 1) {
680
680
  const headerWriter = new BinaryWriter();
681
681
  headerWriter.writeU8(patched.id);
682
682
  headerWriter.writeVarInt(patched.payload.length);
@@ -769,62 +769,166 @@ function createNumberPayload(value, kind) {
769
769
  }
770
770
 
771
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
+ }
812
+ function readBpm(parsed) {
813
+ const tempoEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
814
+ if (tempoEvent) {
815
+ return getEventNumber(tempoEvent) / 1e3;
816
+ }
817
+ const coarseEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_COARSE);
818
+ if (coarseEvent) {
819
+ let bpm = coarseEvent.payload.readUInt16LE(0);
820
+ const fineEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_FINE);
821
+ if (fineEvent) {
822
+ bpm += fineEvent.payload.readUInt16LE(0) / 1e3;
823
+ }
824
+ return bpm;
825
+ }
826
+ return null;
827
+ }
772
828
  function readProjectMeta(parsed) {
773
829
  const titleEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TITLE);
774
830
  const commentsEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_COMMENTS);
775
831
  const artistsEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_ARTISTS);
776
832
  const genreEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_GENRE);
777
- const tempoEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
778
833
  return {
779
834
  name: titleEvent ? getEventString(titleEvent, parsed.useUnicode) : null,
780
835
  description: commentsEvent ? getEventString(commentsEvent, parsed.useUnicode) : null,
781
836
  artist: artistsEvent ? getEventString(artistsEvent, parsed.useUnicode) : null,
782
837
  genre: genreEvent ? getEventString(genreEvent, parsed.useUnicode) : null,
783
- bpm: tempoEvent ? getEventNumber(tempoEvent) / 1e3 : null
838
+ bpm: readBpm(parsed)
784
839
  };
785
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
+ }
786
916
  function writeProjectMeta(parsed, meta) {
787
- return patchEvents(parsed, (event) => {
788
- if (meta.name !== void 0 && event.id === EVENT_ID.PROJECT_TITLE) {
789
- return {
790
- ...event,
791
- payload: createTextPayload(meta.name ?? "", parsed.useUnicode)
792
- };
793
- }
794
- if (meta.description !== void 0 && event.id === EVENT_ID.PROJECT_COMMENTS) {
795
- return {
796
- ...event,
797
- payload: createTextPayload(meta.description ?? "", parsed.useUnicode)
798
- };
799
- }
800
- if (meta.artist !== void 0 && event.id === EVENT_ID.PROJECT_ARTISTS) {
801
- return {
802
- ...event,
803
- payload: createTextPayload(meta.artist ?? "", parsed.useUnicode)
804
- };
805
- }
806
- if (meta.genre !== void 0 && event.id === EVENT_ID.PROJECT_GENRE) {
807
- return {
808
- ...event,
809
- payload: createTextPayload(meta.genre ?? "", parsed.useUnicode)
810
- };
811
- }
812
- if (meta.bpm && Math.abs(meta.bpm) > 0 && event.id === EVENT_ID.PROJECT_TEMPO) {
813
- return {
814
- ...event,
815
- payload: createNumberPayload(Math.round(Math.abs(meta.bpm) * 1e3), "u32")
816
- };
817
- }
818
- return event;
819
- });
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;
820
924
  }
821
- var DELPHI_EPOCH = new Date(1899, 11, 30);
925
+ var DELPHI_EPOCH_MS = Date.UTC(1899, 11, 30);
822
926
  function delphiToDate(days) {
823
927
  const ms = days * 24 * 60 * 60 * 1e3;
824
- return new Date(DELPHI_EPOCH.getTime() + ms);
928
+ return new Date(DELPHI_EPOCH_MS + ms);
825
929
  }
826
930
  function dateToDelphiDays(date) {
827
- const ms = date.getTime() - DELPHI_EPOCH.getTime();
931
+ const ms = date.getTime() - DELPHI_EPOCH_MS;
828
932
  return ms / (24 * 60 * 60 * 1e3);
829
933
  }
830
934
  function readProjectTimeInfo(parsed) {
@@ -845,31 +949,23 @@ function readProjectTimeInfo(parsed) {
845
949
  };
846
950
  }
847
951
  function writeProjectTimeInfo(parsed, info) {
952
+ if (info.creationDate === void 0 && info.workTimeSeconds === void 0) {
953
+ return parsed;
954
+ }
848
955
  const current = readProjectTimeInfo(parsed);
849
- return patchEvents(parsed, (event) => {
850
- if (event.id !== EVENT_ID.PROJECT_TIMESTAMP) {
851
- return event;
852
- }
853
- const writer = new BinaryWriter();
854
- if (info.creationDate !== void 0 && info.creationDate !== null) {
855
- writer.writeF64LE(dateToDelphiDays(info.creationDate));
856
- } else if (current.creationDate !== null) {
857
- writer.writeF64LE(dateToDelphiDays(current.creationDate));
858
- } else {
859
- writer.writeF64LE(0);
860
- }
861
- if (info.workTimeSeconds !== void 0 && info.workTimeSeconds !== null) {
862
- writer.writeF64LE(info.workTimeSeconds / (24 * 60 * 60));
863
- } else if (current.workTimeSeconds !== null) {
864
- writer.writeF64LE(current.workTimeSeconds / (24 * 60 * 60));
865
- } else {
866
- writer.writeF64LE(0);
867
- }
868
- return {
869
- ...event,
870
- payload: writer.toBuffer()
871
- };
872
- });
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()));
873
969
  }
874
970
  function listSamples(parsed) {
875
971
  const samples = [];