@signalsandsorcery/plugin-sdk 2.35.8 → 2.36.2

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
@@ -111,6 +111,7 @@ __export(index_exports, {
111
111
  defaultVoiceSpecs: () => defaultVoiceSpecs,
112
112
  describeViolations: () => describeViolations,
113
113
  drawWaveform: () => drawWaveform,
114
+ effectivePxPerBeat: () => effectivePxPerBeat,
114
115
  enforceVoice: () => enforceVoice,
115
116
  foldPitchToRegister: () => foldPitchToRegister,
116
117
  formatConcurrentTracks: () => formatConcurrentTracks,
@@ -129,6 +130,7 @@ __export(index_exports, {
129
130
  pickTopKWeighted: () => pickTopKWeighted,
130
131
  pitchToName: () => pitchToName,
131
132
  pluginFxToToggleFx: () => pluginFxToToggleFx,
133
+ promptEnterToGenerate: () => promptEnterToGenerate,
132
134
  pxToCell: () => pxToCell,
133
135
  reconcileSlots: () => reconcileSlots,
134
136
  resizeNoteDuration: () => resizeNoteDuration,
@@ -604,25 +606,31 @@ function clamp(v, lo, hi) {
604
606
  function snapLabel(s) {
605
607
  return SNAP_LABELS[String(s)] ?? `${s}`;
606
608
  }
609
+ function effectivePxPerBeat(containerWidth, totalBeats) {
610
+ if (totalBeats <= 0) return PX_PER_BEAT;
611
+ const available = containerWidth - GUTTER_W;
612
+ if (available <= 0) return PX_PER_BEAT;
613
+ return Math.max(PX_PER_BEAT, available / totalBeats);
614
+ }
607
615
  function pitchToName(pitch) {
608
616
  const name = NOTE_NAMES[(pitch % 12 + 12) % 12];
609
617
  const octave = Math.floor(pitch / 12) - 1;
610
618
  return `${name}${octave}`;
611
619
  }
612
- function cellToPx(pitch, startBeat, hi) {
613
- return { left: startBeat * PX_PER_BEAT, top: (hi - pitch) * ROW_HEIGHT };
620
+ function cellToPx(pitch, startBeat, hi, pxPerBeat = PX_PER_BEAT) {
621
+ return { left: startBeat * pxPerBeat, top: (hi - pitch) * ROW_HEIGHT };
614
622
  }
615
- function pxToCell(localX, localY, hi, snap, bars, beatsPerBar) {
623
+ function pxToCell(localX, localY, hi, snap, bars, beatsPerBar, pxPerBeat = PX_PER_BEAT) {
616
624
  const totalBeats = bars * beatsPerBar;
617
625
  const pitch = clamp(hi - Math.floor(localY / ROW_HEIGHT), 0, 127);
618
- const rawBeat = localX / PX_PER_BEAT;
626
+ const rawBeat = localX / pxPerBeat;
619
627
  const snapped = Math.round(rawBeat / snap) * snap;
620
628
  const startBeat = clamp(snapped, 0, Math.max(0, totalBeats - snap));
621
629
  return { pitch, startBeat };
622
630
  }
623
- function resizeNoteDuration(startBeat, localX, snap, bars, beatsPerBar) {
631
+ function resizeNoteDuration(startBeat, localX, snap, bars, beatsPerBar, pxPerBeat = PX_PER_BEAT) {
624
632
  const totalBeats = bars * beatsPerBar;
625
- const snappedEnd = Math.round(localX / PX_PER_BEAT / snap) * snap;
633
+ const snappedEnd = Math.round(localX / pxPerBeat / snap) * snap;
626
634
  const end = clamp(snappedEnd, startBeat + snap, totalBeats);
627
635
  return end - startBeat;
628
636
  }
@@ -673,7 +681,19 @@ function PianoRollEditor({
673
681
  }, [autoFit, notes, minPitch, maxPitch]);
674
682
  const rowCount = hi - lo + 1;
675
683
  const totalBeats = bars * beatsPerBar;
676
- const gridWidth = totalBeats * PX_PER_BEAT;
684
+ const [containerW, setContainerW] = (0, import_react.useState)(0);
685
+ (0, import_react.useLayoutEffect)(() => {
686
+ const el = scrollRef.current;
687
+ if (!el) return;
688
+ const measure = () => setContainerW(el.clientWidth);
689
+ measure();
690
+ if (typeof ResizeObserver === "undefined") return void 0;
691
+ const ro = new ResizeObserver(measure);
692
+ ro.observe(el);
693
+ return () => ro.disconnect();
694
+ }, []);
695
+ const pxPerBeat = effectivePxPerBeat(containerW, totalBeats);
696
+ const gridWidth = totalBeats * pxPerBeat;
677
697
  const gridHeight = rowCount * ROW_HEIGHT;
678
698
  const stateRef = (0, import_react.useRef)({
679
699
  notes,
@@ -685,7 +705,8 @@ function PianoRollEditor({
685
705
  defaultVelocity,
686
706
  bpm,
687
707
  onAuditionNote,
688
- disabled
708
+ disabled,
709
+ pxPerBeat
689
710
  });
690
711
  stateRef.current = {
691
712
  notes,
@@ -697,7 +718,8 @@ function PianoRollEditor({
697
718
  defaultVelocity,
698
719
  bpm,
699
720
  onAuditionNote,
700
- disabled
721
+ disabled,
722
+ pxPerBeat
701
723
  };
702
724
  const localCoords = (0, import_react.useCallback)((clientX, clientY) => {
703
725
  const rect = gridRef.current?.getBoundingClientRect();
@@ -733,14 +755,14 @@ function PianoRollEditor({
733
755
  if (drag.mode === "resize") {
734
756
  const note = s.notes[drag.index];
735
757
  if (!note) return;
736
- const durationBeats = resizeNoteDuration(note.startBeat, x, s.snapState, s.bars, s.beatsPerBar);
758
+ const durationBeats = resizeNoteDuration(note.startBeat, x, s.snapState, s.bars, s.beatsPerBar, s.pxPerBeat);
737
759
  if (durationBeats === note.durationBeats) return;
738
760
  const next2 = s.notes.map((n, i) => i === drag.index ? { ...n, durationBeats } : n);
739
761
  s.onChange(next2);
740
762
  return;
741
763
  }
742
764
  if (drag.mode !== "drag") return;
743
- const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);
765
+ const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar, s.pxPerBeat);
744
766
  const next = s.notes.map((n, i) => i === drag.index ? { ...n, pitch, startBeat } : n);
745
767
  s.onChange(next);
746
768
  }, [localCoords]);
@@ -756,7 +778,7 @@ function PianoRollEditor({
756
778
  }
757
779
  if (drag.mode === "pending-add") {
758
780
  const { x, y } = localCoords(e.clientX, e.clientY);
759
- const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);
781
+ const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar, s.pxPerBeat);
760
782
  const note = {
761
783
  pitch,
762
784
  startBeat,
@@ -805,14 +827,14 @@ function PianoRollEditor({
805
827
  return out;
806
828
  }, [hi, lo]);
807
829
  const gridBg = (0, import_react.useMemo)(() => {
808
- const beatPx = PX_PER_BEAT;
809
- const barPx = PX_PER_BEAT * beatsPerBar;
830
+ const beatPx = pxPerBeat;
831
+ const barPx = pxPerBeat * beatsPerBar;
810
832
  return [
811
833
  `repeating-linear-gradient(to right, transparent 0 ${beatPx - 1}px, rgba(255,255,255,0.06) ${beatPx - 1}px ${beatPx}px)`,
812
834
  `repeating-linear-gradient(to right, transparent 0 ${barPx - 1}px, rgba(255,255,255,0.16) ${barPx - 1}px ${barPx}px)`,
813
835
  `repeating-linear-gradient(to bottom, transparent 0 ${ROW_HEIGHT - 1}px, rgba(255,255,255,0.04) ${ROW_HEIGHT - 1}px ${ROW_HEIGHT}px)`
814
836
  ].join(", ");
815
- }, [beatsPerBar]);
837
+ }, [beatsPerBar, pxPerBeat]);
816
838
  const octaveDisabled = disabled || notes.length === 0;
817
839
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `flex flex-col gap-1 ${className ?? ""}`, "data-testid": testId, children: [
818
840
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-1", "data-testid": "sdk-pr-toolbar", children: [
@@ -906,8 +928,8 @@ function PianoRollEditor({
906
928
  onPointerCancel: handlePointerCancel,
907
929
  children: [
908
930
  notes.map((n, i) => {
909
- const { left, top } = cellToPx(n.pitch, n.startBeat, hi);
910
- const width = Math.max(3, n.durationBeats * PX_PER_BEAT);
931
+ const { left, top } = cellToPx(n.pitch, n.startBeat, hi, pxPerBeat);
932
+ const width = Math.max(3, n.durationBeats * pxPerBeat);
911
933
  const handleW = Math.min(RESIZE_HANDLE_PX, width / 2);
912
934
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
913
935
  "div",
@@ -2385,6 +2407,71 @@ function SorceryProgressBar({
2385
2407
  );
2386
2408
  }
2387
2409
 
2410
+ // src/panel-core/panel-helpers.ts
2411
+ function promptEnterToGenerate(generate, disabled = false) {
2412
+ return (e) => {
2413
+ if (e.key === "Enter" && !e.shiftKey && !disabled) {
2414
+ e.preventDefault();
2415
+ generate();
2416
+ }
2417
+ };
2418
+ }
2419
+ function trackDataKey(dbId, suffix) {
2420
+ return `track:${dbId}:${suffix}`;
2421
+ }
2422
+ function pluginFxToToggleFx(sdkState) {
2423
+ const result = { ...EMPTY_FX_DETAIL_STATE };
2424
+ for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
2425
+ const sdkCat = sdkState[category];
2426
+ if (sdkCat) {
2427
+ result[category] = {
2428
+ enabled: sdkCat.enabled,
2429
+ presetIndex: sdkCat.presetIndex,
2430
+ dryWet: sdkCat.dryWet
2431
+ };
2432
+ }
2433
+ }
2434
+ return result;
2435
+ }
2436
+ function parseLLMNoteResponse(content) {
2437
+ try {
2438
+ let jsonStr = content.trim();
2439
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
2440
+ if (fenceMatch) {
2441
+ jsonStr = fenceMatch[1].trim();
2442
+ }
2443
+ const parsed = JSON.parse(jsonStr);
2444
+ if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
2445
+ return null;
2446
+ }
2447
+ const obj = parsed;
2448
+ if (!Array.isArray(obj.notes)) {
2449
+ return null;
2450
+ }
2451
+ const validNotes = [];
2452
+ for (const raw of obj.notes) {
2453
+ if (typeof raw !== "object" || raw === null) continue;
2454
+ const note = raw;
2455
+ const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
2456
+ const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
2457
+ const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
2458
+ const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
2459
+ if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
2460
+ validNotes.push({
2461
+ pitch: Math.round(pitch),
2462
+ startBeat,
2463
+ durationBeats,
2464
+ velocity: Math.round(velocity)
2465
+ });
2466
+ }
2467
+ }
2468
+ const role = typeof obj.role === "string" ? obj.role : void 0;
2469
+ return { notes: validNotes, role };
2470
+ } catch {
2471
+ return null;
2472
+ }
2473
+ }
2474
+
2388
2475
  // src/components/TrackRow.tsx
2389
2476
  var import_jsx_runtime12 = require("react/jsx-runtime");
2390
2477
  function TrackRow({
@@ -2453,12 +2540,7 @@ function TrackRow({
2453
2540
  );
2454
2541
  const fxTabOpen = drawerOpen && drawerTab === "fx";
2455
2542
  const soundTabOpen = drawerOpen && drawerTab !== "fx";
2456
- const handleKeyDown = (e) => {
2457
- if (e.key === "Enter" && !e.shiftKey && onGenerate) {
2458
- e.preventDefault();
2459
- onGenerate();
2460
- }
2461
- };
2543
+ const handleKeyDown = promptEnterToGenerate(() => onGenerate?.(), !onGenerate);
2462
2544
  const borderColorStyle = needsGeneration ? void 0 : accentColor;
2463
2545
  const borderClass = needsGeneration ? "border-amber-400 animate-pulse" : "border-sas-border";
2464
2546
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { "data-testid": "sdk-track-row-wrapper", className: "w-full", ...drag?.rowProps ?? {}, children: [
@@ -6046,63 +6128,6 @@ function newTrackState(handle, overrides = {}) {
6046
6128
  };
6047
6129
  }
6048
6130
 
6049
- // src/panel-core/panel-helpers.ts
6050
- function trackDataKey(dbId, suffix) {
6051
- return `track:${dbId}:${suffix}`;
6052
- }
6053
- function pluginFxToToggleFx(sdkState) {
6054
- const result = { ...EMPTY_FX_DETAIL_STATE };
6055
- for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
6056
- const sdkCat = sdkState[category];
6057
- if (sdkCat) {
6058
- result[category] = {
6059
- enabled: sdkCat.enabled,
6060
- presetIndex: sdkCat.presetIndex,
6061
- dryWet: sdkCat.dryWet
6062
- };
6063
- }
6064
- }
6065
- return result;
6066
- }
6067
- function parseLLMNoteResponse(content) {
6068
- try {
6069
- let jsonStr = content.trim();
6070
- const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
6071
- if (fenceMatch) {
6072
- jsonStr = fenceMatch[1].trim();
6073
- }
6074
- const parsed = JSON.parse(jsonStr);
6075
- if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
6076
- return null;
6077
- }
6078
- const obj = parsed;
6079
- if (!Array.isArray(obj.notes)) {
6080
- return null;
6081
- }
6082
- const validNotes = [];
6083
- for (const raw of obj.notes) {
6084
- if (typeof raw !== "object" || raw === null) continue;
6085
- const note = raw;
6086
- const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
6087
- const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
6088
- const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
6089
- const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
6090
- if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
6091
- validNotes.push({
6092
- pitch: Math.round(pitch),
6093
- startBeat,
6094
- durationBeats,
6095
- velocity: Math.round(velocity)
6096
- });
6097
- }
6098
- }
6099
- const role = typeof obj.role === "string" ? obj.role : void 0;
6100
- return { notes: validNotes, role };
6101
- } catch {
6102
- return null;
6103
- }
6104
- }
6105
-
6106
6131
  // src/panel-core/group-meta.ts
6107
6132
  function parseTrackGroups(sceneData, spec) {
6108
6133
  const pattern = new RegExp(`^track:(.+):${spec.metaKey}$`);
@@ -9048,7 +9073,7 @@ Your previous ensemble had these problems \u2014 fix them while keeping everythi
9048
9073
  }
9049
9074
 
9050
9075
  // src/constants/sdk-version.ts
9051
- var PLUGIN_SDK_VERSION = "2.43.0";
9076
+ var PLUGIN_SDK_VERSION = "2.45.0";
9052
9077
 
9053
9078
  // src/utils/format-concurrent-tracks.ts
9054
9079
  function formatConcurrentTracks(ctx) {
@@ -9286,6 +9311,7 @@ function pickTopKWeighted(scored, options = {}) {
9286
9311
  defaultVoiceSpecs,
9287
9312
  describeViolations,
9288
9313
  drawWaveform,
9314
+ effectivePxPerBeat,
9289
9315
  enforceVoice,
9290
9316
  foldPitchToRegister,
9291
9317
  formatConcurrentTracks,
@@ -9304,6 +9330,7 @@ function pickTopKWeighted(scored, options = {}) {
9304
9330
  pickTopKWeighted,
9305
9331
  pitchToName,
9306
9332
  pluginFxToToggleFx,
9333
+ promptEnterToGenerate,
9307
9334
  pxToCell,
9308
9335
  reconcileSlots,
9309
9336
  resizeNoteDuration,